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/22 09:43:16 UTC

[syncope] branch master updated: [SYNCOPE-1500] Reconciliation now supports single pull / push + [SYNCOPE-957] Reconciliation now supports Linked Accounts + [SYNCOPE-1499] Use Push correlation rule wherever it makes sense

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

ilgrosso pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/master by this push:
     new 64495ef  [SYNCOPE-1500] Reconciliation now supports single pull / push + [SYNCOPE-957] Reconciliation now supports Linked Accounts + [SYNCOPE-1499] Use Push correlation rule wherever it makes sense
64495ef is described below

commit 64495efbbc906599bf3c7de1c0692a7c3139b024
Author: Francesco Chicchiriccò <il...@apache.org>
AuthorDate: Tue Oct 22 09:13:42 2019 +0200

    [SYNCOPE-1500] Reconciliation now supports single pull / push + [SYNCOPE-957] Reconciliation now supports Linked Accounts + [SYNCOPE-1499] Use Push correlation rule wherever it makes sense
---
 .../client/console/commons/IdMStatusProvider.java  |   3 +-
 .../console/rest/ReconciliationRestClient.java     |  24 +-
 .../console/status/AnyStatusDirectoryPanel.java    |   8 +-
 .../client/console/status/ReconStatusPanel.java    |   9 +-
 .../client/console/status/ReconStatusUtils.java    |   9 +-
 .../client/console/status/ReconTaskPanel.java      |  28 +-
 .../status/ResourceStatusDirectoryPanel.java       |  29 +-
 .../client/ui/commons/status/StatusUtils.java      |   6 +-
 .../client/console/wizards/any/StatusPanel.java    |   6 +-
 .../apache/syncope/common/lib/to/ReconStatus.java  |  35 ++
 .../syncope/common/rest/api/beans/ReconQuery.java  |  96 ++++++
 .../rest/api/service/ReconciliationService.java    |  43 +--
 .../common/lib/collections/IteratorChain.java      | 281 ----------------
 .../apache/syncope/common/lib/types/MatchType.java |   9 +-
 core/idm/logic/pom.xml                             |   5 +
 .../syncope/core/logic/ReconciliationLogic.java    | 303 ++++++++++++------
 .../apache/syncope/core/logic/ResourceLogic.java   | 137 ++++----
 .../cxf/service/ReconciliationServiceImpl.java     |  49 ++-
 .../core/rest/cxf/service/ResourceServiceImpl.java |   2 +-
 .../apache/syncope/core/logic/AnyTypeLogic.java    |   1 -
 .../persistence/api/dao/PullCorrelationRule.java   |   5 +-
 .../core/persistence/api/dao/PullMatch.java        |  69 ++--
 .../syncope/core/persistence/api/dao/UserDAO.java  |   3 +
 .../core/persistence/jpa/dao/JPAUserDAO.java       |  15 +
 .../core/persistence/jpa/inner/PolicyTest.java     |   2 +-
 .../src/test/resources/domains/MasterContent.xml   |   2 +-
 .../core/provisioning/api/VirAttrHandler.java      |   9 +
 .../api/pushpull/SyncopeSinglePullExecutor.java    |   2 -
 .../api/pushpull/SyncopeSinglePushExecutor.java    |   8 +-
 .../api/pushpull/UserPushResultHandler.java        |   4 +
 .../java/DefaultAnyObjectProvisioningManager.java  |  11 +-
 .../java/DefaultGroupProvisioningManager.java      |  13 +-
 .../java/DefaultUserProvisioningManager.java       |  18 +-
 .../core/provisioning/java/MappingManagerImpl.java |  59 ++--
 .../core/provisioning/java/VirAttrHandlerImpl.java | 134 ++++----
 .../java/data/AbstractAnyDataBinder.java           |   4 +-
 .../java/data/AnyObjectDataBinderImpl.java         |   1 +
 .../java/data/AnyTypeDataBinderImpl.java           |   8 +-
 .../java/data/ResourceDataBinderImpl.java          |  13 +-
 .../core/provisioning/java/job/JobManagerImpl.java |  14 +-
 .../java/job/report/ReconciliationReportlet.java   |   2 +-
 .../AbstractPropagationTaskExecutor.java           |  67 +---
 .../java/propagation/AzurePropagationActions.java  |   4 +-
 .../propagation/DefaultPropagationReporter.java    |   4 +-
 .../propagation/GoogleAppsPropagationActions.java  |   4 +-
 .../java/propagation/PropagationManagerImpl.java   |  21 +-
 .../java/pushpull/ADMembershipPullActions.java     |  14 +-
 .../java/pushpull/AbstractPullResultHandler.java   | 139 ++++----
 .../java/pushpull/AbstractPushResultHandler.java   |  23 +-
 .../java/pushpull/AbstractRealmResultHandler.java  |   4 +
 .../pushpull/AbstractSyncopeResultHandler.java     |  11 +-
 .../DefaultAnyObjectPullResultHandler.java         |   8 +-
 .../DefaultAnyObjectPushResultHandler.java         |   4 +-
 .../pushpull/DefaultGroupPullResultHandler.java    |   8 +-
 .../pushpull/DefaultGroupPushResultHandler.java    |   4 +-
 .../pushpull/DefaultRealmPullResultHandler.java    | 355 +++++++++------------
 .../pushpull/DefaultRealmPushResultHandler.java    |  21 +-
 .../pushpull/DefaultUserPullResultHandler.java     |  60 ++--
 .../pushpull/DefaultUserPushResultHandler.java     | 250 ++++++++++++++-
 .../{PullUtils.java => InboundMatcher.java}        | 283 ++++++++--------
 .../java/pushpull/LDAPMembershipPullActions.java   |  33 +-
 .../java/pushpull/OutboundMatcher.java             | 229 +++++++++++++
 .../java/pushpull/PullJobDelegate.java             | 113 +++----
 .../core/provisioning/java/pushpull/PushUtils.java | 139 --------
 .../java/pushpull/SinglePullJobDelegate.java       |  36 +--
 .../java/pushpull/SinglePushJobDelegate.java       |  83 +++--
 .../provisioning/java/utils/ConnObjectUtils.java   |  50 +--
 .../core/provisioning/java/utils/MappingUtils.java |  35 +-
 .../LinkedAccountSamplePullCorrelationRule.java    |  17 +-
 .../org/apache/syncope/fit/AbstractITCase.java     |   2 +
 .../apache/syncope/fit/core/AnyObjectITCase.java   |   4 +-
 .../org/apache/syncope/fit/core/AnyTypeITCase.java |  12 +-
 .../org/apache/syncope/fit/core/GroupITCase.java   |  26 +-
 .../org/apache/syncope/fit/core/LoggerITCase.java  |   5 +-
 .../syncope/fit/core/MultitenancyITCase.java       |   4 +-
 .../syncope/fit/core/PropagationTaskITCase.java    |   2 +-
 .../apache/syncope/fit/core/PullTaskITCase.java    |   6 +-
 .../apache/syncope/fit/core/PushTaskITCase.java    |  89 +++---
 .../syncope/fit/core/ReconciliationITCase.java     |  63 +++-
 .../apache/syncope/fit/core/ResourceITCase.java    |  10 +-
 .../org/apache/syncope/fit/core/SearchITCase.java  |   8 +-
 81 files changed, 1949 insertions(+), 1780 deletions(-)

diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/commons/IdMStatusProvider.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/commons/IdMStatusProvider.java
index 9d60687..4c778dd 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/commons/IdMStatusProvider.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/commons/IdMStatusProvider.java
@@ -29,7 +29,6 @@ import org.apache.syncope.client.console.status.ReconStatusUtils;
 import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
 import org.apache.syncope.common.lib.to.AnyTO;
 import org.apache.syncope.common.lib.to.ConnObjectTO;
-import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.IdMEntitlement;
 
 public class IdMStatusProvider implements StatusProvider {
@@ -41,7 +40,7 @@ public class IdMStatusProvider implements StatusProvider {
             final AnyTO any, final Collection<String> resources) {
 
         return ReconStatusUtils.getReconStatuses(
-                AnyTypeKind.fromTOClass(any.getClass()), any.getKey(), any.getResources()).stream().
+                any.getType(), any.getKey(), any.getResources()).stream().
                 map(status -> Triple.<ConnObjectTO, ConnObjectWrapper, String>of(
                 status.getRight().getOnSyncope(),
                 new ConnObjectWrapper(any, status.getLeft(), status.getRight().getOnResource()),
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/rest/ReconciliationRestClient.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/rest/ReconciliationRestClient.java
index 7f43da4..4d7f94c 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/rest/ReconciliationRestClient.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/rest/ReconciliationRestClient.java
@@ -21,32 +21,22 @@ package org.apache.syncope.client.console.rest;
 import org.apache.syncope.common.lib.to.PullTaskTO;
 import org.apache.syncope.common.lib.to.PushTaskTO;
 import org.apache.syncope.common.lib.to.ReconStatus;
-import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.rest.api.beans.ReconQuery;
 import org.apache.syncope.common.rest.api.service.ReconciliationService;
 
 public class ReconciliationRestClient extends BaseRestClient {
 
     private static final long serialVersionUID = -3161863874876938094L;
 
-    public static ReconStatus status(final AnyTypeKind anyTypeKind, final String anyKey, final String resourceKey) {
-        return getService(ReconciliationService.class).status(anyTypeKind, anyKey, resourceKey);
+    public static ReconStatus status(final ReconQuery reconQuery) {
+        return getService(ReconciliationService.class).status(reconQuery);
     }
 
-    public static void push(
-        final AnyTypeKind anyTypeKind,
-        final String anyKey,
-        final String resourceKey,
-        final PushTaskTO pushTask) {
-
-        getService(ReconciliationService.class).push(anyTypeKind, anyKey, resourceKey, pushTask);
+    public static void push(final ReconQuery reconQuery, final PushTaskTO pushTask) {
+        getService(ReconciliationService.class).push(reconQuery, pushTask);
     }
 
-    public static void pull(
-        final AnyTypeKind anyTypeKind,
-        final String anyKey,
-        final String resourceKey,
-        final PullTaskTO pullTask) {
-
-        getService(ReconciliationService.class).pull(anyTypeKind, anyKey, resourceKey, pullTask);
+    public static void pull(final ReconQuery reconQuery, final PullTaskTO pullTask) {
+        getService(ReconciliationService.class).pull(reconQuery, pullTask);
     }
 }
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/AnyStatusDirectoryPanel.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/AnyStatusDirectoryPanel.java
index 6fad939..7081671 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/AnyStatusDirectoryPanel.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/AnyStatusDirectoryPanel.java
@@ -192,7 +192,7 @@ public class AnyStatusDirectoryPanel
                 @Override
                 public void onClick(final AjaxRequestTarget target, final StatusBean bean) {
                     multiLevelPanelRef.next(bean.getResource(),
-                            new ReconStatusPanel(bean.getResource(), anyTypeKind, anyTO.getKey()),
+                            new ReconStatusPanel(bean.getResource(), anyTO.getType(), anyTO.getKey()),
                             target);
                     target.add(multiLevelPanelRef);
                     AnyStatusDirectoryPanel.this.getTogglePanel().close(target);
@@ -211,7 +211,7 @@ public class AnyStatusDirectoryPanel
                             new ReconTaskPanel(
                                     bean.getResource(),
                                     new PushTaskTO(),
-                                    anyTypeKind,
+                                    anyTO.getType(),
                                     anyTO.getKey(),
                                     multiLevelPanelRef,
                                     pageRef),
@@ -231,7 +231,7 @@ public class AnyStatusDirectoryPanel
                             new ReconTaskPanel(
                                     bean.getResource(),
                                     new PullTaskTO(),
-                                    anyTypeKind,
+                                    anyTO.getType(),
                                     anyTO.getKey(),
                                     multiLevelPanelRef,
                                     pageRef),
@@ -288,7 +288,7 @@ public class AnyStatusDirectoryPanel
             List<StatusBean> statusBeans = actual.getResources().stream().map(resource -> {
                 List<Pair<String, ReconStatus>> statuses = List.of();
                 if (statusOnly) {
-                    statuses = ReconStatusUtils.getReconStatuses(anyTypeKind, anyTO.getKey(), List.of(resource));
+                    statuses = ReconStatusUtils.getReconStatuses(anyTO.getType(), anyTO.getKey(), List.of(resource));
                 }
 
                 return StatusUtils.getStatusBean(actual,
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconStatusPanel.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconStatusPanel.java
index a19ffbb..a7f2360 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconStatusPanel.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconStatusPanel.java
@@ -25,7 +25,6 @@ import org.apache.syncope.client.console.panels.RemoteObjectPanel;
 import org.apache.syncope.client.console.wizards.any.ConnObjectPanel;
 import org.apache.syncope.common.lib.to.ConnObjectTO;
 import org.apache.syncope.common.lib.to.ReconStatus;
-import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.wicket.model.IModel;
 import org.apache.wicket.model.Model;
 import org.apache.wicket.model.ResourceModel;
@@ -36,17 +35,17 @@ public class ReconStatusPanel extends RemoteObjectPanel {
 
     private final String resource;
 
-    private final AnyTypeKind anyTypeKind;
+    private final String anyTypeKey;
 
     private final String anyKey;
 
     public ReconStatusPanel(
             final String resource,
-            final AnyTypeKind anyTypeKind,
+            final String anyTypeKey,
             final String anyKey) {
 
         this.resource = resource;
-        this.anyTypeKind = anyTypeKind;
+        this.anyTypeKey = anyTypeKey;
         this.anyKey = anyKey;
 
         add(new ConnObjectPanel(
@@ -59,7 +58,7 @@ public class ReconStatusPanel extends RemoteObjectPanel {
     @Override
     protected Pair<ConnObjectTO, ConnObjectTO> getConnObjectTOs() {
         List<Pair<String, ReconStatus>> statuses =
-                ReconStatusUtils.getReconStatuses(anyTypeKind, anyKey, List.of(resource));
+                ReconStatusUtils.getReconStatuses(anyTypeKey, anyKey, List.of(resource));
 
         return statuses.isEmpty()
                 ? null
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconStatusUtils.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconStatusUtils.java
index 6f89e06..9da9dcf 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconStatusUtils.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconStatusUtils.java
@@ -26,7 +26,7 @@ import java.util.stream.Collectors;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.client.console.rest.ReconciliationRestClient;
 import org.apache.syncope.common.lib.to.ReconStatus;
-import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.rest.api.beans.ReconQuery;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -37,13 +37,14 @@ public final class ReconStatusUtils implements Serializable {
     private static final Logger LOG = LoggerFactory.getLogger(ReconStatusUtils.class);
 
     public static List<Pair<String, ReconStatus>> getReconStatuses(
-            final AnyTypeKind anyTypeKind, final String anyKey, final Collection<String> resources) {
+            final String anyTypeKey, final String anyKey, final Collection<String> resources) {
 
         return resources.stream().map(resource -> {
             try {
-                return Pair.of(resource, ReconciliationRestClient.status(anyTypeKind, anyKey, resource));
+                return Pair.of(resource, ReconciliationRestClient.status(
+                        new ReconQuery.Builder(anyTypeKey, resource).anyKey(anyKey).build()));
             } catch (Exception e) {
-                LOG.warn("Unexpected error for {} {} on {}", anyTypeKind, anyKey, resource, e);
+                LOG.warn("Unexpected error for {} {} on {}", anyTypeKey, anyKey, resource, e);
                 return null;
             }
         }).filter(Objects::nonNull).collect(Collectors.toList());
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java
index 9d50891..48228f6 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java
@@ -34,10 +34,10 @@ import org.apache.syncope.common.lib.to.EntityTO;
 import org.apache.syncope.common.lib.to.ProvisioningTaskTO;
 import org.apache.syncope.common.lib.to.PullTaskTO;
 import org.apache.syncope.common.lib.to.PushTaskTO;
-import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.IdMImplementationType;
 import org.apache.syncope.common.lib.types.MatchingRule;
 import org.apache.syncope.common.lib.types.UnmatchingRule;
+import org.apache.syncope.common.rest.api.beans.ReconQuery;
 import org.apache.wicket.PageReference;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.ajax.markup.html.form.AjaxSubmitLink;
@@ -81,7 +81,7 @@ public class ReconTaskPanel extends MultilevelPanel.SecondLevel {
     public ReconTaskPanel(
             final String resource,
             final ProvisioningTaskTO taskTO,
-            final AnyTypeKind anyTypeKind,
+            final String anyType,
             final String anyKey,
             final MultilevelPanel multiLevelPanelRef,
             final PageReference pageRef) {
@@ -92,41 +92,41 @@ public class ReconTaskPanel extends MultilevelPanel.SecondLevel {
         AjaxPalettePanel<String> actions = new AjaxPalettePanel.Builder<String>().
                 setAllowMoveAll(true).setAllowOrder(true).
                 build("actions",
-                        new PropertyModel<List<String>>(taskTO, "actions"),
+                        new PropertyModel<>(taskTO, "actions"),
                         new ListModel<>(taskTO instanceof PushTaskTO
                                 ? pushActions.getObject() : pullActions.getObject()));
         actions.setOutputMarkupId(true);
         form.add(actions);
 
         AjaxDropDownChoicePanel<MatchingRule> matchingRule = new AjaxDropDownChoicePanel<>(
-                "matchingRule", "matchingRule", new PropertyModel<MatchingRule>(taskTO, "matchingRule"), false);
+                "matchingRule", "matchingRule", new PropertyModel<>(taskTO, "matchingRule"), false);
         matchingRule.setChoices(List.of(MatchingRule.values()));
         form.add(matchingRule);
 
         AjaxDropDownChoicePanel<UnmatchingRule> unmatchingRule = new AjaxDropDownChoicePanel<>(
-                "unmatchingRule", "unmatchingRule", new PropertyModel<UnmatchingRule>(taskTO, "unmatchingRule"),
+                "unmatchingRule", "unmatchingRule", new PropertyModel<>(taskTO, "unmatchingRule"),
                 false);
         unmatchingRule.setChoices(List.of(UnmatchingRule.values()));
         form.add(unmatchingRule);
 
         taskTO.setPerformCreate(true);
         AjaxCheckBoxPanel performCreate = new AjaxCheckBoxPanel(
-                "performCreate", "performCreate", new PropertyModel<Boolean>(taskTO, "performCreate"), false);
+                "performCreate", "performCreate", new PropertyModel<>(taskTO, "performCreate"), false);
         form.add(performCreate);
 
         taskTO.setPerformUpdate(true);
         AjaxCheckBoxPanel performUpdate = new AjaxCheckBoxPanel(
-                "performUpdate", "performUpdate", new PropertyModel<Boolean>(taskTO, "performUpdate"), false);
+                "performUpdate", "performUpdate", new PropertyModel<>(taskTO, "performUpdate"), false);
         form.add(performUpdate);
 
         taskTO.setPerformDelete(true);
         AjaxCheckBoxPanel performDelete = new AjaxCheckBoxPanel(
-                "performDelete", "performDelete", new PropertyModel<Boolean>(taskTO, "performDelete"), false);
+                "performDelete", "performDelete", new PropertyModel<>(taskTO, "performDelete"), false);
         form.add(performDelete);
 
         taskTO.setSyncStatus(true);
         AjaxCheckBoxPanel syncStatus = new AjaxCheckBoxPanel(
-                "syncStatus", "syncStatus", new PropertyModel<Boolean>(taskTO, "syncStatus"), false);
+                "syncStatus", "syncStatus", new PropertyModel<>(taskTO, "syncStatus"), false);
         form.add(syncStatus);
 
         form.add(new AjaxSubmitLink("reconcile") {
@@ -135,19 +135,17 @@ public class ReconTaskPanel extends MultilevelPanel.SecondLevel {
 
             @Override
             protected void onSubmit(final AjaxRequestTarget target) {
+                ReconQuery reconQuery = new ReconQuery.Builder(anyType, resource).anyKey(anyKey).build();
                 try {
                     if (taskTO instanceof PushTaskTO) {
-                        ReconciliationRestClient.push(anyTypeKind, anyKey, resource,
-                            (PushTaskTO) form.getModelObject());
+                        ReconciliationRestClient.push(reconQuery, (PushTaskTO) form.getModelObject());
                     } else {
-                        ReconciliationRestClient.pull(anyTypeKind, anyKey, resource,
-                            (PullTaskTO) form.getModelObject());
+                        ReconciliationRestClient.pull(reconQuery, (PullTaskTO) form.getModelObject());
                     }
 
                     SyncopeConsoleSession.get().info(getString(Constants.OPERATION_SUCCEEDED));
                 } catch (Exception e) {
-                    LOG.error("While attempting reconciliation on {} {} {} {}",
-                            anyTypeKind, anyKey, resource, form.getModelObject(), e);
+                    LOG.error("While attempting reconciliation on {}", reconQuery, form.getModelObject(), e);
                     SyncopeConsoleSession.get().error(resource + ": "
                             + (StringUtils.isBlank(e.getMessage()) ? e.getClass().getName() : e.getMessage()));
                 }
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ResourceStatusDirectoryPanel.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ResourceStatusDirectoryPanel.java
index fbaf8b3..7660e30 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ResourceStatusDirectoryPanel.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ResourceStatusDirectoryPanel.java
@@ -103,23 +103,6 @@ public class ResourceStatusDirectoryPanel
         return columns;
     }
 
-    private AnyTypeKind getAnyTypeKind() {
-        if (StringUtils.isBlank(type)) {
-            return null;
-        }
-
-        switch (type) {
-            case "USER":
-                return AnyTypeKind.USER;
-
-            case "GROUP":
-                return AnyTypeKind.GROUP;
-
-            default:
-                return AnyTypeKind.ANY_OBJECT;
-        }
-    }
-
     @Override
     public ActionsPanel<StatusBean> getActions(final IModel<StatusBean> model) {
         final ActionsPanel<StatusBean> panel = super.getActions(model);
@@ -130,13 +113,13 @@ public class ResourceStatusDirectoryPanel
 
             @Override
             protected boolean statusCondition(final StatusBean bean) {
-                return getAnyTypeKind() != null;
+                return StringUtils.isNotBlank(type);
             }
 
             @Override
             public void onClick(final AjaxRequestTarget target, final StatusBean bean) {
                 multiLevelPanelRef.next(bean.getResource(),
-                        new ReconStatusPanel(bean.getResource(), getAnyTypeKind(), bean.getKey()),
+                        new ReconStatusPanel(bean.getResource(), type, bean.getKey()),
                         target);
                 target.add(multiLevelPanelRef);
                 getTogglePanel().close(target);
@@ -149,7 +132,7 @@ public class ResourceStatusDirectoryPanel
 
             @Override
             protected boolean statusCondition(final StatusBean bean) {
-                return getAnyTypeKind() != null;
+                return StringUtils.isNotBlank(type);
             }
 
             @Override
@@ -158,7 +141,7 @@ public class ResourceStatusDirectoryPanel
                         new ReconTaskPanel(
                                 bean.getResource(),
                                 new PushTaskTO(),
-                                getAnyTypeKind(),
+                                type,
                                 bean.getKey(),
                                 multiLevelPanelRef,
                                 pageRef),
@@ -174,7 +157,7 @@ public class ResourceStatusDirectoryPanel
 
             @Override
             protected boolean statusCondition(final StatusBean bean) {
-                return getAnyTypeKind() != null;
+                return StringUtils.isNotBlank(type);
             }
 
             @Override
@@ -183,7 +166,7 @@ public class ResourceStatusDirectoryPanel
                         new ReconTaskPanel(
                                 bean.getResource(),
                                 new PullTaskTO(),
-                                getAnyTypeKind(),
+                                type,
                                 bean.getKey(),
                                 multiLevelPanelRef,
                                 pageRef),
diff --git a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/status/StatusUtils.java b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/status/StatusUtils.java
index d964136..fe33e4e 100644
--- a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/status/StatusUtils.java
+++ b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/status/StatusUtils.java
@@ -50,8 +50,8 @@ public final class StatusUtils implements Serializable {
         if (connObjectTO != null) {
             Boolean enabled = isEnabled(connObjectTO);
             statusBean.setStatus(Optional.ofNullable(enabled).map(aBoolean -> aBoolean
-                ? Status.ACTIVE
-                : Status.SUSPENDED).orElseGet(() -> (notUser ? Status.ACTIVE : Status.UNDEFINED)));
+                    ? Status.ACTIVE
+                    : Status.SUSPENDED).orElseGet(() -> (notUser ? Status.ACTIVE : Status.UNDEFINED)));
 
             statusBean.setConnObjectLink(getConnObjectLink(connObjectTO));
         }
@@ -69,7 +69,7 @@ public final class StatusUtils implements Serializable {
         if (connObjectTO != null) {
             Boolean enabled = isEnabled(connObjectTO);
             statusBean.setStatus(Optional.ofNullable(enabled)
-                .filter(aBoolean -> !aBoolean).map(aBoolean -> Status.SUSPENDED).orElse(Status.ACTIVE));
+                    .filter(aBoolean -> !aBoolean).map(aBoolean -> Status.SUSPENDED).orElse(Status.ACTIVE));
 
             statusBean.setConnObjectLink(getConnObjectLink(connObjectTO));
         }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/StatusPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/StatusPanel.java
index 16abbe8..481eaa9 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/StatusPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/StatusPanel.java
@@ -201,9 +201,9 @@ public class StatusPanel extends Panel {
     }
 
     protected static Pair<ConnObjectTO, ConnObjectTO> getConnObjectTOs(
-        final String anyKey,
-        final String resource,
-        final List<Triple<ConnObjectTO, ConnObjectWrapper, String>> objects) {
+            final String anyKey,
+            final String resource,
+            final List<Triple<ConnObjectTO, ConnObjectWrapper, String>> objects) {
 
         for (Triple<ConnObjectTO, ConnObjectWrapper, String> object : objects) {
             if (anyKey.equals(object.getMiddle().getAny().getKey())
diff --git a/common/idm/lib/src/main/java/org/apache/syncope/common/lib/to/ReconStatus.java b/common/idm/lib/src/main/java/org/apache/syncope/common/lib/to/ReconStatus.java
index 04577c8..b7b4474 100644
--- a/common/idm/lib/src/main/java/org/apache/syncope/common/lib/to/ReconStatus.java
+++ b/common/idm/lib/src/main/java/org/apache/syncope/common/lib/to/ReconStatus.java
@@ -22,6 +22,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import java.io.Serializable;
 import javax.xml.bind.annotation.XmlRootElement;
 import javax.xml.bind.annotation.XmlType;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.lib.types.MatchType;
 
 /**
  * Reconciliation status.
@@ -32,11 +34,44 @@ public class ReconStatus implements Serializable {
 
     private static final long serialVersionUID = -8516345256596521490L;
 
+    private AnyTypeKind anyTypeKind;
+
+    private String anyKey;
+
+    private MatchType matchType;
+
     private ConnObjectTO onSyncope;
 
     private ConnObjectTO onResource;
 
     @Schema(accessMode = Schema.AccessMode.READ_ONLY)
+    public AnyTypeKind getAnyTypeKind() {
+        return anyTypeKind;
+    }
+
+    public void setAnyTypeKind(final AnyTypeKind anyTypeKind) {
+        this.anyTypeKind = anyTypeKind;
+    }
+
+    @Schema(accessMode = Schema.AccessMode.READ_ONLY)
+    public String getAnyKey() {
+        return anyKey;
+    }
+
+    public void setAnyKey(final String anyKey) {
+        this.anyKey = anyKey;
+    }
+
+    @Schema(accessMode = Schema.AccessMode.READ_ONLY)
+    public MatchType getMatchType() {
+        return matchType;
+    }
+
+    public void setMatchType(final MatchType matchType) {
+        this.matchType = matchType;
+    }
+
+    @Schema(accessMode = Schema.AccessMode.READ_ONLY)
     public ConnObjectTO getOnSyncope() {
         return onSyncope;
     }
diff --git a/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ReconQuery.java b/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ReconQuery.java
new file mode 100644
index 0000000..ddd6799
--- /dev/null
+++ b/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ReconQuery.java
@@ -0,0 +1,96 @@
+/*
+ * 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.common.rest.api.beans;
+
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.QueryParam;
+
+public class ReconQuery {
+
+    public static class Builder {
+
+        private final ReconQuery instance;
+
+        public Builder(final String anyTypeKey, final String resourceKey) {
+            instance = new ReconQuery();
+            instance.setAnyTypeKey(anyTypeKey);
+            instance.setResourceKey(resourceKey);
+        }
+
+        public Builder anyKey(final String anyKey) {
+            instance.setAnyKey(anyKey);
+            return this;
+        }
+
+        public Builder connObjectKeyValue(final String connObjectKeyValue) {
+            instance.setConnObjectKeyValue(connObjectKeyValue);
+            return this;
+        }
+
+        public ReconQuery build() {
+            return instance;
+        }
+    }
+
+    private String anyTypeKey;
+
+    private String anyKey;
+
+    private String resourceKey;
+
+    private String connObjectKeyValue;
+
+    public String getAnyTypeKey() {
+        return anyTypeKey;
+    }
+
+    @NotNull
+    @QueryParam("anyTypeKey")
+    public void setAnyTypeKey(final String anyTypeKey) {
+        this.anyTypeKey = anyTypeKey;
+    }
+
+    public String getAnyKey() {
+        return anyKey;
+    }
+
+    @QueryParam("anyKey")
+    public void setAnyKey(final String anyKey) {
+        this.anyKey = anyKey;
+    }
+
+    public String getResourceKey() {
+        return resourceKey;
+    }
+
+    @NotNull
+    @QueryParam("resourceKey")
+    public void setResourceKey(final String resourceKey) {
+        this.resourceKey = resourceKey;
+    }
+
+    public String getConnObjectKeyValue() {
+        return connObjectKeyValue;
+    }
+
+    @QueryParam("connObjectKeyValue")
+    public void setConnObjectKeyValue(final String connObjectKeyValue) {
+        this.connObjectKeyValue = connObjectKeyValue;
+    }
+}
diff --git a/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ReconciliationService.java b/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ReconciliationService.java
index 11bffe0..cb44ef9 100644
--- a/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ReconciliationService.java
+++ b/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ReconciliationService.java
@@ -24,18 +24,18 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.security.SecurityRequirements;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import javax.validation.constraints.NotNull;
+import javax.ws.rs.BeanParam;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.MediaType;
 import org.apache.syncope.common.lib.to.PullTaskTO;
 import org.apache.syncope.common.lib.to.PushTaskTO;
 import org.apache.syncope.common.lib.to.ReconStatus;
-import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.beans.ReconQuery;
 
 /**
  * REST operations for reconciliation.
@@ -48,29 +48,19 @@ import org.apache.syncope.common.rest.api.RESTHeaders;
 public interface ReconciliationService extends JAXRSService {
 
     /**
-     * Gets current attributes on Syncope and on the given External Resource, related to given user, group or
-     * any object.
+     * Gets compared status between attributes in Syncope and on the given External Resource.
      *
-     * @param anyTypeKind anyTypeKind
-     * @param anyKey user, group or any object: if value looks like a UUID then it is interpreted as key, otherwise as
-     * a (user)name
-     * @param resourceKey resource key
+     * @param query query conditions
      * @return reconciliation status
      */
     @GET
     @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
-    ReconStatus status(
-            @NotNull @QueryParam("anyTypeKind") AnyTypeKind anyTypeKind,
-            @NotNull @QueryParam("anyKey") String anyKey,
-            @NotNull @QueryParam("resourceKey") String resourceKey);
+    ReconStatus status(@BeanParam ReconQuery query);
 
     /**
-     * Pushes the given user, group or any object in Syncope onto the External Resource.
+     * Pushes the matching user, group, any object or linked account in Syncope onto the External Resource.
      *
-     * @param anyTypeKind anyTypeKind
-     * @param anyKey user, group or any object: if value looks like a UUID then it is interpreted as key, otherwise as
-     * a (user)name
-     * @param resourceKey resource key
+     * @param query query conditions
      * @param pushTask push specification
      */
     @ApiResponses(
@@ -79,19 +69,12 @@ public interface ReconciliationService extends JAXRSService {
     @Path("push")
     @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
     @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
-    void push(
-            @NotNull @QueryParam("anyTypeKind") AnyTypeKind anyTypeKind,
-            @NotNull @QueryParam("anyKey") String anyKey,
-            @NotNull @QueryParam("resourceKey") String resourceKey,
-            @NotNull PushTaskTO pushTask);
+    void push(@BeanParam ReconQuery query, @NotNull PushTaskTO pushTask);
 
     /**
-     * Pulls the given user, group or any object from the External Resource into Syncope.
+     * Pulls the matching user, group, any object or linked account from the External Resource into Syncope.
      *
-     * @param anyTypeKind anyTypeKind
-     * @param anyKey user, group or any object: if value looks like a UUID then it is interpreted as key, otherwise as
-     * a (user)name
-     * @param resourceKey resource key
+     * @param query query conditions
      * @param pullTask pull specification
      */
     @ApiResponses(
@@ -100,9 +83,5 @@ public interface ReconciliationService extends JAXRSService {
     @Path("pull")
     @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
     @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
-    void pull(
-            @NotNull @QueryParam("anyTypeKind") AnyTypeKind anyTypeKind,
-            @NotNull @QueryParam("anyKey") String anyKey,
-            @NotNull @QueryParam("resourceKey") String resourceKey,
-            @NotNull PullTaskTO pullTask);
+    void pull(@BeanParam ReconQuery query, @NotNull PullTaskTO pullTask);
 }
diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/collections/IteratorChain.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/collections/IteratorChain.java
deleted file mode 100644
index 6d4b822..0000000
--- a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/collections/IteratorChain.java
+++ /dev/null
@@ -1,281 +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.common.lib.collections;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.Queue;
-
-/**
- * An IteratorChain is an Iterator that wraps a number of Iterators.
- *
- * This class makes multiple iterators look like one to the caller. When any
- * method from the Iterator interface is called, the IteratorChain will delegate
- * to a single underlying Iterator. The IteratorChain will invoke the Iterators
- * in sequence until all Iterators are exhausted.
- *
- * Under many circumstances, linking Iterators together in this manner is more
- * efficient (and convenient) than reading out the contents of each Iterator
- * into a List and creating a new Iterator.
- *
- * Calling a method that adds new Iterator <i>after a method in the Iterator
- * interface has been called</i> will result in an UnsupportedOperationException.
- *
- * NOTE: As from version 3.0, the IteratorChain may contain no iterators. In
- * this case the class will function as an empty iterator.
- *
- * NOTE: As from version 4.0, the IteratorChain stores the iterators in a queue
- * and removes any reference to them as soon as they are not used anymore. Thus
- * the methods {@code setIterator(Iterator)} and {@code getIterators()} have been
- * removed and {@link #size()} will return the number of remaining iterators in
- * the queue.
- *
- * @param <E> the type of elements held in this collection
- */
-public class IteratorChain<E> implements Iterator<E> {
-
-    /** The chain of iterators. */
-    private final Queue<Iterator<? extends E>> iteratorChain = new LinkedList<>();
-
-    /** The current iterator. */
-    private Iterator<? extends E> currentIterator = null;
-
-    /**
-     * The "last used" Iterator is the Iterator upon which next() or hasNext()
-     * was most recently called used for the remove() operation only.
-     */
-    private Iterator<? extends E> lastUsedIterator = null;
-
-    /**
-     * ComparatorChain is "locked" after the first time compare(Object,Object)
-     * is called.
-     */
-    private boolean isLocked = false;
-
-    //-----------------------------------------------------------------------
-    /**
-     * Construct an IteratorChain with no Iterators.
-     *
-     * You will normally use {@link #addIterator(Iterator)} to add some
-     * iterators after using this constructor.
-     */
-    public IteratorChain() {
-        super();
-    }
-
-    /**
-     * Construct an IteratorChain with a single Iterator.
-     *
-     * This method takes one iterator. The newly constructed iterator will
-     * iterate through that iterator. Thus calling this constructor on its own
-     * will have no effect other than decorating the input iterator.
-     *
-     * You will normally use {@link #addIterator(Iterator)} to add some more
-     * iterators after using this constructor.
-     *
-     * @param iterator the first child iterator in the IteratorChain, not null
-     * @throws NullPointerException if the iterator is null
-     */
-    public IteratorChain(final Iterator<? extends E> iterator) {
-        super();
-        addIterator(iterator);
-    }
-
-    /**
-     * Constructs a new {@code IteratorChain} over the two given iterators.
-     *
-     * This method takes two iterators. The newly constructed iterator will
-     * iterate through each one of the input iterators in turn.
-     *
-     * @param first the first child iterator in the IteratorChain, not null
-     * @param second the second child iterator in the IteratorChain, not null
-     * @throws NullPointerException if either iterator is null
-     */
-    public IteratorChain(final Iterator<? extends E> first, final Iterator<? extends E> second) {
-        super();
-        addIterator(first);
-        addIterator(second);
-    }
-
-    /**
-     * Constructs a new {@code IteratorChain} over the array of iterators.
-     *
-     * This method takes an array of iterators. The newly constructed iterator
-     * will iterate through each one of the input iterators in turn.
-     *
-     * @param iteratorChain the array of iterators, not null
-     * @throws NullPointerException if iterators array is or contains null
-     */
-    @SafeVarargs
-    public IteratorChain(final Iterator<? extends E>... iteratorChain) {
-        super();
-        for (final Iterator<? extends E> element : iteratorChain) {
-            addIterator(element);
-        }
-    }
-
-    /**
-     * Constructs a new {@code IteratorChain} over the collection of
-     * iterators.
-     *
-     * This method takes a collection of iterators. The newly constructed
-     * iterator will iterate through each one of the input iterators in turn.
-     *
-     * @param iteratorChain the collection of iterators, not null
-     * @throws NullPointerException if iterators collection is or contains null
-     * @throws ClassCastException if iterators collection doesn't contain an
-     * iterator
-     */
-    public IteratorChain(final Collection<Iterator<? extends E>> iteratorChain) {
-        super();
-        iteratorChain.forEach(this::addIterator);
-    }
-
-    //-----------------------------------------------------------------------
-    /**
-     * Add an Iterator to the end of the chain
-     *
-     * @param iterator Iterator to add
-     * @throws IllegalStateException if I've already started iterating
-     * @throws NullPointerException if the iterator is null
-     */
-    private void addIterator(final Iterator<? extends E> iterator) {
-        checkLocked();
-        if (iterator == null) {
-            throw new NullPointerException("Iterator must not be null");
-        }
-        iteratorChain.add(iterator);
-    }
-
-    /**
-     * Returns the remaining number of Iterators in the current IteratorChain.
-     *
-     * @return Iterator count
-     */
-    public int size() {
-        return iteratorChain.size();
-    }
-
-    /**
-     * Determine if modifications can still be made to the IteratorChain.
-     * IteratorChains cannot be modified once they have executed a method from
-     * the Iterator interface.
-     *
-     * @return true if IteratorChain cannot be modified, false if it can
-     */
-    public boolean isLocked() {
-        return isLocked;
-    }
-
-    /**
-     * Checks whether the iterator chain is now locked and in use.
-     */
-    private void checkLocked() {
-        if (isLocked) {
-            throw new UnsupportedOperationException(
-                    "IteratorChain cannot be changed after the first use of a method from the Iterator interface");
-        }
-    }
-
-    /**
-     * Lock the chain so no more iterators can be added. This must be called
-     * from all Iterator interface methods.
-     */
-    private void lockChain() {
-        if (!isLocked) {
-            isLocked = true;
-        }
-    }
-
-    /**
-     * Updates the current iterator field to ensure that the current Iterator is
-     * not exhausted
-     */
-    protected void updateCurrentIterator() {
-        if (currentIterator == null) {
-            if (iteratorChain.isEmpty()) {
-                currentIterator = Collections.emptyListIterator();
-            } else {
-                currentIterator = iteratorChain.remove();
-            }
-            // set last used iterator here, in case the user calls remove
-            // before calling hasNext() or next() (although they shouldn't)
-            lastUsedIterator = currentIterator;
-        }
-
-        while (!currentIterator.hasNext() && !iteratorChain.isEmpty()) {
-            currentIterator = iteratorChain.remove();
-        }
-    }
-
-    //-----------------------------------------------------------------------
-    /**
-     * Return true if any Iterator in the IteratorChain has a remaining element.
-     *
-     * @return true if elements remain
-     */
-    @Override
-    public boolean hasNext() {
-        lockChain();
-        updateCurrentIterator();
-        lastUsedIterator = currentIterator;
-
-        return currentIterator.hasNext();
-    }
-
-    /**
-     * Returns the next Object of the current Iterator
-     *
-     * @return Object from the current Iterator
-     * @throws java.util.NoSuchElementException if all the Iterators are
-     * exhausted
-     */
-    @Override
-    public E next() {
-        lockChain();
-        updateCurrentIterator();
-        lastUsedIterator = currentIterator;
-
-        return currentIterator.next();
-    }
-
-    /**
-     * Removes from the underlying collection the last element returned by the
-     * Iterator. As with next() and hasNext(), this method calls remove() on the
-     * underlying Iterator. Therefore, this method may throw an
-     * UnsupportedOperationException if the underlying Iterator does not support
-     * this method.
-     *
-     * @throws UnsupportedOperationException if the remove operator is not
-     * supported by the underlying Iterator
-     * @throws IllegalStateException if the next method has not yet been called,
-     * or the remove method has already been called after the last call to the
-     * next method.
-     */
-    @Override
-    public void remove() {
-        lockChain();
-        if (currentIterator == null) {
-            updateCurrentIterator();
-        }
-        lastUsedIterator.remove();
-    }
-}
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/UserPushResultHandler.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/MatchType.java
similarity index 84%
copy from core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/UserPushResultHandler.java
copy to common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/MatchType.java
index f96c0ef..0885aac 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/UserPushResultHandler.java
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/MatchType.java
@@ -16,8 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.api.pushpull;
+package org.apache.syncope.common.lib.types;
 
-public interface UserPushResultHandler extends SyncopePushResultHandler {
+import javax.xml.bind.annotation.XmlEnum;
+
+@XmlEnum
+public enum MatchType {
+    ANY,
+    LINKED_ACCOUNT;
 
 }
diff --git a/core/idm/logic/pom.xml b/core/idm/logic/pom.xml
index 78d8001..c34b10c 100644
--- a/core/idm/logic/pom.xml
+++ b/core/idm/logic/pom.xml
@@ -43,6 +43,11 @@ under the License.
       <artifactId>syncope-core-idrepo-logic</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.apache.syncope.common.idm</groupId>
+      <artifactId>syncope-common-idm-rest-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
   </dependencies>
 
   <build>
diff --git a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ReconciliationLogic.java b/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ReconciliationLogic.java
index 941c2ca..490864f 100644
--- a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ReconciliationLogic.java
+++ b/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ReconciliationLogic.java
@@ -19,14 +19,15 @@
 package org.apache.syncope.core.logic;
 
 import java.lang.reflect.Method;
-import java.util.Iterator;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
-import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
-import org.apache.syncope.common.lib.SyncopeClientException;
-import org.apache.syncope.common.lib.collections.IteratorChain;
 import org.apache.syncope.common.lib.Attr;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.to.ConnObjectTO;
 import org.apache.syncope.common.lib.to.EntityTO;
 import org.apache.syncope.common.lib.to.PullTaskTO;
@@ -36,29 +37,32 @@ import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.lib.types.IdMEntitlement;
 import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+import org.apache.syncope.common.lib.types.MatchType;
+import org.apache.syncope.common.rest.api.beans.ReconQuery;
+import org.apache.syncope.core.persistence.api.dao.AnyDAO;
+import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
 import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
-import org.apache.syncope.core.persistence.api.dao.VirSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.entity.Any;
-import org.apache.syncope.core.persistence.api.entity.AnyUtils;
+import org.apache.syncope.core.persistence.api.entity.AnyType;
 import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
-import org.apache.syncope.core.persistence.api.entity.VirSchema;
 import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
 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.provisioning.api.Connector;
+import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount;
 import org.apache.syncope.core.provisioning.api.ConnectorFactory;
 import org.apache.syncope.core.provisioning.api.MappingManager;
+import org.apache.syncope.core.provisioning.api.VirAttrHandler;
 import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopeSinglePullExecutor;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopeSinglePushExecutor;
+import org.apache.syncope.core.provisioning.java.pushpull.InboundMatcher;
+import org.apache.syncope.core.provisioning.java.pushpull.OutboundMatcher;
 import org.apache.syncope.core.provisioning.java.utils.ConnObjectUtils;
 import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
 import org.identityconnectors.framework.common.objects.Attribute;
-import org.identityconnectors.framework.common.objects.AttributeBuilder;
-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.Uid;
 import org.quartz.JobExecutionException;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -72,15 +76,27 @@ public class ReconciliationLogic extends AbstractTransactionalLogic<EntityTO> {
     private AnyUtilsFactory anyUtilsFactory;
 
     @Autowired
+    private AnyTypeDAO anyTypeDAO;
+
+    @Autowired
     private ExternalResourceDAO resourceDAO;
 
     @Autowired
-    private VirSchemaDAO virSchemaDAO;
+    private RealmDAO realmDAO;
+
+    @Autowired
+    private VirAttrHandler virAttrHandler;
 
     @Autowired
     private MappingManager mappingManager;
 
     @Autowired
+    private InboundMatcher inboundMatcher;
+
+    @Autowired
+    private OutboundMatcher outboundMatcher;
+
+    @Autowired
     private ConnectorFactory connFactory;
 
     @Autowired
@@ -89,118 +105,190 @@ public class ReconciliationLogic extends AbstractTransactionalLogic<EntityTO> {
     @Autowired
     private SyncopeSinglePushExecutor singlePushExecutor;
 
-    @SuppressWarnings("unchecked")
-    private Pair<Any<?>, Provision> init(final AnyTypeKind anyTypeKind, final String anyKey, final String resourceKey) {
-        AnyUtils anyUtils = anyUtilsFactory.getInstance(anyTypeKind);
-
-        Any<?> any = anyUtils.dao().authFind(anyKey);
-        if (any == null) {
-            throw new NotFoundException(anyTypeKind + " '" + anyKey + '\'');
+    private Provision getProvision(final String anyTypeKey, final String resourceKey) {
+        AnyType anyType = anyTypeDAO.find(anyTypeKey);
+        if (anyType == null) {
+            throw new NotFoundException("AnyType '" + anyTypeKey + "'");
         }
 
         ExternalResource resource = resourceDAO.find(resourceKey);
         if (resource == null) {
-            throw new NotFoundException("Resource '" + resourceKey + '\'');
+            throw new NotFoundException("Resource '" + resourceKey + "'");
         }
-        Provision provision = resource.getProvision(any.getType()).orElseThrow(()
-                -> new NotFoundException("Provision for " + any.getType() + " on Resource '" + resourceKey + '\''));
+        Provision provision = resource.getProvision(anyType).
+                orElseThrow(() -> new NotFoundException(
+                "Provision for " + anyType + " on Resource '" + resourceKey + "'"));
         if (provision.getMapping() == null) {
-            throw new NotFoundException("Mapping for " + any.getType() + " on Resource '" + resourceKey + '\'');
+            throw new NotFoundException("Mapping for " + anyType + " on Resource '" + resourceKey + "'");
         }
 
-        return (Pair<Any<?>, Provision>) Pair.of(any, provision);
+        return provision;
     }
 
-    private ConnObjectTO getOnSyncope(final Any<?> any, final Provision provision, final String resourceKey) {
-        Pair<String, Set<Attribute>> attrs = mappingManager.prepareAttrs(any, null, false, true, provision);
+    private ConnObjectTO getOnSyncope(
+            final MappingItem connObjectKeyItem,
+            final String connObjectKeyValue,
+            final Set<Attribute> attrs) {
 
-        MappingItem connObjectKey = provision.getMapping().getConnObjectKeyItem().orElseThrow(()
-                -> new NotFoundException("No RemoteKey set for " + resourceKey));
-
-        ConnObjectTO connObjectTO = ConnObjectUtils.getConnObjectTO(attrs.getRight());
-        if (attrs.getLeft() != null) {
-            connObjectTO.getAttrs().add(new Attr.Builder(connObjectKey.getExtAttrName()).
-                    value(attrs.getLeft()).build());
-            connObjectTO.getAttrs().add(new Attr.Builder(Uid.NAME).
-                    value(attrs.getLeft()).build());
-        }
+        ConnObjectTO connObjectTO = ConnObjectUtils.getConnObjectTO(attrs);
+        connObjectTO.getAttrs().add(new Attr.Builder(connObjectKeyItem.getExtAttrName()).
+                value(connObjectKeyValue).build());
+        connObjectTO.getAttrs().add(new Attr.Builder(Uid.NAME).
+                value(connObjectKeyValue).build());
 
         return connObjectTO;
     }
 
-    private ConnObjectTO getOnResource(final Any<?> any, final Provision provision) {
-        // 1. build connObjectKeyItem
-        MappingItem connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision).orElseThrow(()
-                -> new NotFoundException("ConnObjectKey for " + any.getType()
-                        + " on resource '" + provision.getResource().getKey() + '\''));
-        String connObjectKeyValue = mappingManager.getConnObjectKeyValue(any, provision).orElse(null);
-        if (connObjectKeyValue == null) {
-            return null;
-        }
+    private ConnObjectTO getOnSyncope(
+            final Any<?> any,
+            final MappingItem connObjectKeyItem,
+            final Provision provision) {
 
-        // 2. determine attributes to query
-        Set<MappingItem> linkinMappingItems = virSchemaDAO.findByProvision(provision).stream().
-                map(VirSchema::asLinkingMappingItem).collect(Collectors.toSet());
-        Iterator<MappingItem> mapItems = new IteratorChain<>(
-                provision.getMapping().getItems().iterator(),
-                linkinMappingItems.iterator());
-
-        // 3. read from the underlying connector
-        ConnObjectTO connObjectTO = null;
-
-        Connector connector = connFactory.getConnector(provision.getResource());
-        ConnectorObject connectorObject = connector.getObject(
-                provision.getObjectClass(),
-                AttributeBuilder.build(connObjectKeyItem.getExtAttrName(), connObjectKeyValue),
-                provision.isIgnoreCaseMatch(),
-                MappingUtils.buildOperationOptions(mapItems));
-        if (connectorObject != null) {
-            Set<Attribute> attributes = connectorObject.getAttributes();
-            if (AttributeUtil.find(Uid.NAME, attributes) == null) {
-                attributes.add(connectorObject.getUid());
-            }
-            if (AttributeUtil.find(Name.NAME, attributes) == null) {
-                attributes.add(connectorObject.getName());
-            }
+        Pair<String, Set<Attribute>> prepared = mappingManager.prepareAttrs(any, null, false, true, provision);
+        return getOnSyncope(connObjectKeyItem, prepared.getLeft(), prepared.getRight());
+    }
 
-            connObjectTO = ConnObjectUtils.getConnObjectTO(attributes);
-        }
+    private ConnObjectTO getOnSyncope(
+            final LinkedAccount account,
+            final MappingItem connObjectKeyItem,
+            final Provision provision) {
 
-        return connObjectTO;
+        Set<Attribute> attrs = mappingManager.prepareAttrs(account.getOwner(), account, null, false, provision);
+        return getOnSyncope(connObjectKeyItem, account.getConnObjectKeyValue(), attrs);
+    }
+
+    private Any<?> getAny(final Provision provision, final String anyKey) {
+        AnyDAO<Any<?>> dao = anyUtilsFactory.getInstance(provision.getAnyType().getKind()).dao();
+        Any<?> any = SyncopeConstants.UUID_PATTERN.matcher(anyKey).matches()
+                ? dao.authFind(anyKey)
+                : dao.authFind(dao.findKey(anyKey));
+        if (any == null) {
+            throw new NotFoundException(provision.getAnyType().getKey() + " '" + anyKey + "'");
+        }
+        return any;
     }
 
     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_GET_CONNOBJECT + "')")
-    public ReconStatus status(final AnyTypeKind anyTypeKind, final String anyKey, final String resourceKey) {
-        Pair<Any<?>, Provision> init = init(anyTypeKind, anyKey, resourceKey);
+    public ReconStatus status(final ReconQuery query) {
+        Provision provision = getProvision(query.getAnyTypeKey(), query.getResourceKey());
+
+        MappingItem connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision).
+                orElseThrow(() -> new NotFoundException(
+                "ConnObjectKey for " + provision.getAnyType().getKey()
+                + " on resource '" + provision.getResource().getKey() + "'"));
 
         ReconStatus status = new ReconStatus();
-        status.setOnSyncope(getOnSyncope(init.getLeft(), init.getRight(), resourceKey));
-        status.setOnResource(getOnResource(init.getLeft(), init.getRight()));
+
+        if (query.getConnObjectKeyValue() != null) {
+            inboundMatcher.matchByConnObjectKeyValue(connObjectKeyItem, query.getConnObjectKeyValue(), provision).
+                    stream().findFirst().ifPresent(match -> {
+                        if (match.getAny() != null) {
+                            status.setMatchType(MatchType.ANY);
+                            status.setAnyTypeKind(match.getAny().getType().getKind());
+                            status.setAnyKey(match.getAny().getKey());
+                            status.setOnSyncope(getOnSyncope(match.getAny(), connObjectKeyItem, provision));
+                        } else if (match.getLinkedAccount() != null) {
+                            status.setMatchType(MatchType.LINKED_ACCOUNT);
+                            status.setAnyTypeKind(AnyTypeKind.USER);
+                            status.setAnyKey(match.getLinkedAccount().getOwner().getKey());
+                            status.setOnSyncope(getOnSyncope(match.getLinkedAccount(), connObjectKeyItem, provision));
+                        }
+                    });
+
+            outboundMatcher.matchByConnObjectKeyValue(
+                    connFactory.getConnector(provision.getResource()),
+                    connObjectKeyItem,
+                    query.getConnObjectKeyValue(),
+                    provision).
+                    ifPresent(connObj -> {
+                        status.setOnResource(ConnObjectUtils.getConnObjectTO(connObj.getAttributes()));
+
+                        if (status.getMatchType() == MatchType.ANY && StringUtils.isNotBlank(status.getAnyKey())) {
+                            virAttrHandler.setValues(getAny(provision, status.getAnyKey()), connObj);
+                        }
+                    });
+        }
+        if (query.getAnyKey() != null) {
+            Any<?> any = getAny(provision, query.getAnyKey());
+            status.setMatchType(MatchType.ANY);
+            status.setAnyTypeKind(any.getType().getKind());
+            status.setAnyKey(any.getKey());
+            status.setOnSyncope(getOnSyncope(any, connObjectKeyItem, provision));
+
+            List<ConnectorObject> connObjs = outboundMatcher.match(
+                    connFactory.getConnector(provision.getResource()), any, provision);
+            if (!connObjs.isEmpty()) {
+                status.setOnResource(ConnObjectUtils.getConnObjectTO(connObjs.get(0).getAttributes()));
+
+                if (connObjs.size() > 1) {
+                    LOG.warn("Expected single match, found {}", connObjs);
+                } else {
+                    virAttrHandler.setValues(any, connObjs.get(0));
+                }
+            }
+        }
 
         return status;
     }
 
     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_EXECUTE + "')")
-    public void push(
-            final AnyTypeKind anyTypeKind,
-            final String anyKey,
-            final String resourceKey,
-            final PushTaskTO pushTask) {
+    public void push(final ReconQuery query, final PushTaskTO pushTask) {
+        Provision provision = getProvision(query.getAnyTypeKey(), query.getResourceKey());
 
-        Pair<Any<?>, Provision> init = init(anyTypeKind, anyKey, resourceKey);
+        MappingItem connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision).
+                orElseThrow(() -> new NotFoundException(
+                "ConnObjectKey for " + provision.getAnyType().getKey()
+                + " on resource '" + provision.getResource().getKey() + "'"));
 
         SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Reconciliation);
-        try {
-            List<ProvisioningReport> results = singlePushExecutor.push(
-                    init.getRight(),
-                    connFactory.getConnector(init.getRight().getResource()),
-                    init.getLeft(),
-                    pushTask);
-            if (!results.isEmpty() && results.get(0).getStatus() == ProvisioningReport.Status.FAILURE) {
-                sce.getElements().add(results.get(0).getMessage());
+        List<ProvisioningReport> results = new ArrayList<>();
+
+        if (query.getConnObjectKeyValue() != null) {
+            inboundMatcher.matchByConnObjectKeyValue(connObjectKeyItem, query.getConnObjectKeyValue(), provision).
+                    stream().findFirst().ifPresent(match -> {
+                        try {
+                            if (match.getMatchTarget() == MatchType.ANY) {
+                                results.addAll(singlePushExecutor.push(
+                                        provision,
+                                        connFactory.getConnector(provision.getResource()),
+                                        match.getAny(),
+                                        pushTask));
+                                if (!results.isEmpty()
+                                        && results.get(0).getStatus() == ProvisioningReport.Status.FAILURE) {
+
+                                    sce.getElements().add(results.get(0).getMessage());
+                                }
+                            } else {
+                                ProvisioningReport result = singlePushExecutor.push(
+                                        provision,
+                                        connFactory.getConnector(provision.getResource()),
+                                        match.getLinkedAccount(),
+                                        pushTask);
+                                if (result.getStatus() == ProvisioningReport.Status.FAILURE) {
+                                    sce.getElements().add(results.get(0).getMessage());
+                                } else {
+                                    results.add(result);
+                                }
+                            }
+                        } catch (JobExecutionException e) {
+                            sce.getElements().add(e.getMessage());
+                        }
+                    });
+        }
+
+        if (sce.isEmpty() && results.isEmpty() && query.getAnyKey() != null) {
+            try {
+                results.addAll(singlePushExecutor.push(
+                        provision,
+                        connFactory.getConnector(provision.getResource()),
+                        getAny(provision, query.getAnyKey()),
+                        pushTask));
+                if (!results.isEmpty() && results.get(0).getStatus() == ProvisioningReport.Status.FAILURE) {
+                    sce.getElements().add(results.get(0).getMessage());
+                }
+            } catch (JobExecutionException e) {
+                sce.getElements().add(e.getMessage());
             }
-        } catch (JobExecutionException e) {
-            sce.getElements().add(e.getMessage());
         }
 
         if (!sce.isEmpty()) {
@@ -209,22 +297,31 @@ public class ReconciliationLogic extends AbstractTransactionalLogic<EntityTO> {
     }
 
     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_EXECUTE + "')")
-    public void pull(
-            final AnyTypeKind anyTypeKind,
-            final String anyKey,
-            final String resourceKey,
-            final PullTaskTO pullTask) {
+    public void pull(final ReconQuery query, final PullTaskTO pullTask) {
+        Provision provision = getProvision(query.getAnyTypeKey(), query.getResourceKey());
 
-        Pair<Any<?>, Provision> init = init(anyTypeKind, anyKey, resourceKey);
+        Optional<String> connObjectKeyValue = Optional.ofNullable(query.getConnObjectKeyValue());
+        if (query.getAnyKey() != null) {
+            Any<?> any = getAny(provision, query.getAnyKey());
+            connObjectKeyValue = mappingManager.getConnObjectKeyValue(any, provision);
+        }
+        if (!connObjectKeyValue.isPresent()) {
+            throw new NotFoundException(
+                    "ConnObjectKey for " + provision.getAnyType().getKey()
+                    + " on resource '" + provision.getResource().getKey() + "'");
+        }
+
+        if (pullTask.getDestinationRealm() == null || realmDAO.findByFullPath(pullTask.getDestinationRealm()) == null) {
+            throw new NotFoundException("Realm " + pullTask.getDestinationRealm());
+        }
 
         SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Reconciliation);
         try {
             List<ProvisioningReport> results = singlePullExecutor.pull(
-                    init.getRight(),
-                    connFactory.getConnector(init.getRight().getResource()),
-                    init.getRight().getMapping().getConnObjectKeyItem().get().getExtAttrName(),
-                    mappingManager.getConnObjectKeyValue(init.getLeft(), init.getRight()).get(),
-                    init.getLeft().getRealm(),
+                    provision,
+                    connFactory.getConnector(provision.getResource()),
+                    provision.getMapping().getConnObjectKeyItem().get().getExtAttrName(),
+                    connObjectKeyValue.get(),
                     pullTask);
             if (!results.isEmpty() && results.get(0).getStatus() == ProvisioningReport.Status.FAILURE) {
                 sce.getElements().add(results.get(0).getMessage());
diff --git a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ResourceLogic.java b/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ResourceLogic.java
index 9dd0c93..29befbf 100644
--- a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ResourceLogic.java
+++ b/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ResourceLogic.java
@@ -20,15 +20,14 @@ package org.apache.syncope.core.logic;
 
 import java.lang.reflect.Method;
 import java.util.ArrayList;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.tuple.Pair;
-import org.apache.syncope.common.lib.collections.IteratorChain;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.to.ConnObjectTO;
@@ -54,21 +53,17 @@ import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.ConnInstance;
 import org.apache.syncope.core.persistence.api.entity.VirSchema;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
-import org.apache.syncope.core.provisioning.api.MappingManager;
+import org.apache.syncope.core.provisioning.api.VirAttrHandler;
 import org.apache.syncope.core.provisioning.api.data.ConnInstanceDataBinder;
 import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
+import org.apache.syncope.core.provisioning.java.pushpull.OutboundMatcher;
 import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.apache.syncope.core.spring.security.DelegatedAdministrationException;
-import org.identityconnectors.framework.common.objects.Attribute;
-import org.identityconnectors.framework.common.objects.AttributeBuilder;
-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.ObjectClass;
 import org.identityconnectors.framework.common.objects.OperationOptions;
 import org.identityconnectors.framework.common.objects.SearchResult;
-import org.identityconnectors.framework.common.objects.Uid;
 import org.identityconnectors.framework.common.objects.filter.Filter;
 import org.identityconnectors.framework.spi.SearchResultsHandler;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -92,13 +87,16 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
     private VirSchemaDAO virSchemaDAO;
 
     @Autowired
+    private VirAttrHandler virAttrHandler;
+
+    @Autowired
     private ResourceDataBinder binder;
 
     @Autowired
     private ConnInstanceDataBinder connInstanceDataBinder;
 
     @Autowired
-    private MappingManager mappingManager;
+    private OutboundMatcher outboundMatcher;
 
     @Autowired
     private ConnectorFactory connFactory;
@@ -267,9 +265,7 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
         return resourceDAO.findAll().stream().map(binder::getResourceTO).collect(Collectors.toList());
     }
 
-    private Pair<AnyType, Provision> connObjectInit(
-            final String resourceKey, final String anyTypeKey) {
-
+    private Provision getProvision(final String resourceKey, final String anyTypeKey) {
         ExternalResource resource = resourceDAO.authFind(resourceKey);
         if (resource == null) {
             throw new NotFoundException("Resource '" + resourceKey + '\'');
@@ -280,53 +276,9 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
             throw new NotFoundException("AnyType '" + anyTypeKey + '\'');
         }
 
-        Provision provision = resource.getProvision(anyType).
+        return resource.getProvision(anyType).
                 orElseThrow(() -> new NotFoundException(
                 "Provision on resource '" + resourceKey + "' for type '" + anyTypeKey + "'"));
-
-        return Pair.of(anyType, provision);
-    }
-
-    private ConnObjectTO readConnObject(
-            final Provision provision,
-            final String connObjectKeyValue) {
-
-        // 0. build connObjectKeyItem
-        MappingItem connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision).
-                orElseThrow(() -> new NotFoundException(
-                "ConnObjectKey mapping for " + provision.getAnyType().getKey()
-                + " on resource '" + provision.getResource().getKey() + "'"));
-
-        // 1. determine attributes to query
-        Set<MappingItem> linkinMappingItems = virSchemaDAO.findByProvision(provision).stream().
-                map(virSchema -> virSchema.asLinkingMappingItem()).collect(Collectors.toSet());
-        Iterator<MappingItem> mapItems = new IteratorChain<>(
-                provision.getMapping().getItems().iterator(),
-                linkinMappingItems.iterator());
-
-        // 2. read from the underlying connector
-        Connector connector = connFactory.getConnector(provision.getResource());
-        ConnectorObject connectorObject = connector.getObject(
-                provision.getObjectClass(),
-                AttributeBuilder.build(connObjectKeyItem.getExtAttrName(), connObjectKeyValue),
-                provision.isIgnoreCaseMatch(),
-                MappingUtils.buildOperationOptions(mapItems));
-        if (connectorObject == null) {
-            throw new NotFoundException(
-                    "Object " + connObjectKeyValue + " with class " + provision.getObjectClass()
-                    + " not found on resource " + provision.getResource().getKey());
-        }
-
-        // 3. build result
-        Set<Attribute> attributes = connectorObject.getAttributes();
-        if (AttributeUtil.find(Uid.NAME, attributes) == null) {
-            attributes.add(connectorObject.getUid());
-        }
-        if (AttributeUtil.find(Name.NAME, attributes) == null) {
-            attributes.add(connectorObject.getName());
-        }
-
-        return ConnObjectUtils.getConnObjectTO(connectorObject);
     }
 
     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_GET_CONNOBJECT + "')")
@@ -336,31 +288,58 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
             final String anyTypeKey,
             final String anyKey) {
 
-        Pair<AnyType, Provision> init = connObjectInit(key, anyTypeKey);
+        Provision provision = getProvision(key, anyTypeKey);
 
         // 1. find any
-        Any<?> any = anyUtilsFactory.getInstance(init.getLeft().getKind()).dao().authFind(anyKey);
+        Any<?> any = anyUtilsFactory.getInstance(provision.getAnyType().getKind()).dao().authFind(anyKey);
         if (any == null) {
-            throw new NotFoundException(init.getLeft() + " " + anyKey);
+            throw new NotFoundException(provision.getAnyType() + " " + anyKey);
         }
 
-        // 2. find connObjectKeyValue
-        String connObjectKeyValue = mappingManager.getConnObjectKeyValue(any, init.getRight()).
-                orElseThrow(() -> new NotFoundException(
-                "ConnObjectKey value for " + init.getLeft() + " " + anyKey + " on resource '" + key + "'"));
+        // 2. find on resource
+        List<ConnectorObject> connObjs = outboundMatcher.match(
+                connFactory.getConnector(provision.getResource()), any, provision);
+        if (connObjs.isEmpty()) {
+            throw new NotFoundException(
+                    "Object " + any + " with class " + provision.getObjectClass()
+                    + " not found on resource " + provision.getResource().getKey());
+        }
+
+        if (connObjs.size() > 1) {
+            LOG.warn("Expected single match, found {}", connObjs);
+        } else {
+            virAttrHandler.setValues(any, connObjs.get(0));
+        }
 
-        return readConnObject(init.getRight(), connObjectKeyValue);
+        return ConnObjectUtils.getConnObjectTO(connObjs.get(0).getAttributes());
     }
 
     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_GET_CONNOBJECT + "')")
     @Transactional(readOnly = true)
-    public ConnObjectTO readConnObjectByConnObjectKey(
+    public ConnObjectTO readConnObjectByConnObjectKeyValue(
             final String key,
             final String anyTypeKey,
             final String connObjectKeyValue) {
 
-        Pair<AnyType, Provision> init = connObjectInit(key, anyTypeKey);
-        return readConnObject(init.getRight(), connObjectKeyValue);
+        Provision provision = getProvision(key, anyTypeKey);
+
+        MappingItem connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision).
+                orElseThrow(() -> new NotFoundException(
+                "ConnObjectKey mapping for " + provision.getAnyType().getKey()
+                + " on resource '" + provision.getResource().getKey() + "'"));
+
+        Optional<ConnectorObject> connObj = outboundMatcher.matchByConnObjectKeyValue(
+                connFactory.getConnector(provision.getResource()),
+                connObjectKeyItem,
+                connObjectKeyValue,
+                provision);
+        if (connObj.isPresent()) {
+            return ConnObjectUtils.getConnObjectTO(connObj.get().getAttributes());
+        }
+
+        throw new NotFoundException(
+                "Object " + connObjectKeyValue + " with class " + provision.getObjectClass()
+                + " not found on resource " + provision.getResource().getKey());
     }
 
     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_LIST_CONNOBJECT + "')")
@@ -386,19 +365,15 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
             }
 
             objectClass = resource.getOrgUnit().getObjectClass();
-            options = MappingUtils.buildOperationOptions(
-                    MappingUtils.getPropagationItems(resource.getOrgUnit().getItems()).iterator());
+            options = MappingUtils.buildOperationOptions(resource.getOrgUnit().getItems().stream());
         } else {
-            Pair<AnyType, Provision> init = connObjectInit(key, anyTypeKey);
-            resource = init.getRight().getResource();
-            objectClass = init.getRight().getObjectClass();
-            init.getRight().getMapping().getItems();
-
-            Set<MappingItem> linkinMappingItems = virSchemaDAO.findByProvision(init.getRight()).stream().
-                    map(VirSchema::asLinkingMappingItem).collect(Collectors.toSet());
-            Iterator<MappingItem> mapItems = new IteratorChain<>(
-                    init.getRight().getMapping().getItems().iterator(),
-                    linkinMappingItems.iterator());
+            Provision provision = getProvision(key, anyTypeKey);
+            resource = provision.getResource();
+            objectClass = provision.getObjectClass();
+
+            Stream<MappingItem> mapItems = Stream.concat(
+                    provision.getMapping().getItems().stream(),
+                    virSchemaDAO.findByProvision(provision).stream().map(VirSchema::asLinkingMappingItem));
             options = MappingUtils.buildOperationOptions(mapItems);
         }
 
@@ -410,7 +385,7 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
 
                     @Override
                     public boolean handle(final ConnectorObject connectorObject) {
-                        connObjects.add(ConnObjectUtils.getConnObjectTO(connectorObject));
+                        connObjects.add(ConnObjectUtils.getConnObjectTO(connectorObject.getAttributes()));
                         // safety protection against uncontrolled result size
                         count++;
                         return count < size;
diff --git a/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ReconciliationServiceImpl.java b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ReconciliationServiceImpl.java
index ed24be5..7b77c2f 100644
--- a/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ReconciliationServiceImpl.java
+++ b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ReconciliationServiceImpl.java
@@ -18,13 +18,13 @@
  */
 package org.apache.syncope.core.rest.cxf.service;
 
+import javax.validation.ValidationException;
 import org.apache.syncope.common.lib.to.PullTaskTO;
 import org.apache.syncope.common.lib.to.PushTaskTO;
 import org.apache.syncope.common.lib.to.ReconStatus;
-import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.rest.api.beans.ReconQuery;
 import org.apache.syncope.common.rest.api.service.ReconciliationService;
 import org.apache.syncope.core.logic.ReconciliationLogic;
-import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -34,42 +34,29 @@ public class ReconciliationServiceImpl extends AbstractServiceImpl implements Re
     @Autowired
     private ReconciliationLogic logic;
 
-    @Autowired
-    private AnyUtilsFactory anyUtilsFactory;
+    private void validate(final ReconQuery reconQuery) {
+        if ((reconQuery.getAnyKey() == null && reconQuery.getConnObjectKeyValue() == null)
+                || (reconQuery.getAnyKey() != null && reconQuery.getConnObjectKeyValue() != null)) {
 
-    @Override
-    public ReconStatus status(final AnyTypeKind anyTypeKind, final String anyKey, final String resourceKey) {
-        return logic.status(
-                anyTypeKind,
-                getActualKey(anyUtilsFactory.getInstance(anyTypeKind).dao(), anyKey),
-                resourceKey);
+            throw new ValidationException("Either provide anyKey or connObjectKeyValue, not both");
+        }
     }
 
     @Override
-    public void push(
-            final AnyTypeKind anyTypeKind,
-            final String anyKey,
-            final String resourceKey,
-            final PushTaskTO pushTask) {
-
-        logic.push(
-                anyTypeKind,
-                getActualKey(anyUtilsFactory.getInstance(anyTypeKind).dao(), anyKey),
-                resourceKey,
-                pushTask);
+    public ReconStatus status(final ReconQuery reconQuery) {
+        validate(reconQuery);
+        return logic.status(reconQuery);
     }
 
     @Override
-    public void pull(
-            final AnyTypeKind anyTypeKind,
-            final String anyKey,
-            final String resourceKey,
-            final PullTaskTO pullTask) {
+    public void push(final ReconQuery reconQuery, final PushTaskTO pushTask) {
+        validate(reconQuery);
+        logic.push(reconQuery, pushTask);
+    }
 
-        logic.pull(
-                anyTypeKind,
-                getActualKey(anyUtilsFactory.getInstance(anyTypeKind).dao(), anyKey),
-                resourceKey,
-                pullTask);
+    @Override
+    public void pull(final ReconQuery reconQuery, final PullTaskTO pullTask) {
+        validate(reconQuery);
+        logic.pull(reconQuery, pullTask);
     }
 }
diff --git a/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java
index ea255de..3eb8954 100644
--- a/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java
+++ b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java
@@ -94,7 +94,7 @@ public class ResourceServiceImpl extends AbstractServiceImpl implements Resource
     public ConnObjectTO readConnObject(final String key, final String anyTypeKey, final String value) {
         return SyncopeConstants.UUID_PATTERN.matcher(value).matches()
                 ? logic.readConnObjectByAnyKey(key, anyTypeKey, value)
-                : logic.readConnObjectByConnObjectKey(key, anyTypeKey, value);
+                : logic.readConnObjectByConnObjectKeyValue(key, anyTypeKey, value);
     }
 
     @Override
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AnyTypeLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AnyTypeLogic.java
index 49bf59c..1503b42 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AnyTypeLogic.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AnyTypeLogic.java
@@ -94,7 +94,6 @@ public class AnyTypeLogic extends AbstractTransactionalLogic<AnyTypeTO> {
         }
 
         binder.update(anyType, anyTypeTO);
-        anyType = anyTypeDAO.save(anyType);
 
         return binder.getAnyTypeTO(anyTypeDAO.save(anyType));
     }
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 332baab..47b2bd7 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
@@ -20,6 +20,7 @@ package org.apache.syncope.core.persistence.api.dao;
 
 import java.util.Optional;
 import org.apache.syncope.common.lib.policy.PullCorrelationRuleConf;
+import org.apache.syncope.common.lib.types.MatchType;
 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;
@@ -31,7 +32,7 @@ import org.identityconnectors.framework.common.objects.SyncDelta;
 @FunctionalInterface
 public interface PullCorrelationRule {
 
-    PullMatch NO_MATCH = new PullMatch.Builder().build();
+    PullMatch NO_MATCH = new PullMatch(MatchType.ANY, null);
 
     default void setConf(PullCorrelationRuleConf conf) {
     }
@@ -57,7 +58,7 @@ public interface PullCorrelationRule {
      * @return matching information
      */
     default PullMatch matching(Any<?> any, SyncDelta syncDelta, Provision provision) {
-        return new PullMatch.Builder().matchingKey(any.getKey()).build();
+        return new PullMatch(MatchType.ANY, any);
     }
 
     /**
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
index f66906f..75d292a 100644
--- 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
@@ -21,69 +21,49 @@ 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;
+import org.apache.syncope.common.lib.types.MatchType;
+import org.apache.syncope.core.persistence.api.entity.Any;
+import org.apache.syncope.core.persistence.api.entity.Entity;
+import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount;
 
 public final class PullMatch implements Serializable {
 
     private static final long serialVersionUID = 6515473131174179932L;
 
-    public enum MatchTarget {
-        ANY,
-        LINKED_ACCOUNT;
+    private final MatchType matchTarget;
 
-    }
-
-    public static class Builder {
-
-        private final PullMatch instance = new PullMatch();
+    private Any<?> any;
 
-        public Builder matchingKey(final String matchingKey) {
-            instance.matchingKey = matchingKey;
-            return this;
-        }
-
-        public Builder matchTarget(final MatchTarget matchTarget) {
-            instance.matchTarget = matchTarget;
-            return this;
-        }
+    private LinkedAccount linkedAccount;
 
-        public Builder linkingUserKey(final String linkingUserKey) {
-            instance.linkingUserKey = linkingUserKey;
-            return this;
-        }
+    public PullMatch(final MatchType matchTarget, final Entity entity) {
+        this.matchTarget = matchTarget;
 
-        public PullMatch build() {
-            return instance;
+        if (entity instanceof Any) {
+            any = (Any<?>) entity;
+        } else if (entity instanceof LinkedAccount) {
+            linkedAccount = (LinkedAccount) entity;
         }
     }
 
-    private MatchTarget matchTarget = MatchTarget.ANY;
-
-    private String matchingKey;
-
-    private String linkingUserKey;
-
-    private PullMatch() {
-        // private constructor
-    }
-
-    public MatchTarget getMatchTarget() {
+    public MatchType getMatchTarget() {
         return matchTarget;
     }
 
-    public String getMatchingKey() {
-        return matchingKey;
+    public Any<?> getAny() {
+        return any;
     }
 
-    public String getLinkingUserKey() {
-        return linkingUserKey;
+    public LinkedAccount getLinkedAccount() {
+        return linkedAccount;
     }
 
     @Override
     public int hashCode() {
         return new HashCodeBuilder().
                 append(matchTarget).
-                append(matchingKey).
-                append(linkingUserKey).
+                append(any).
+                append(linkedAccount).
                 build();
     }
 
@@ -100,9 +80,9 @@ public final class PullMatch implements Serializable {
         }
         final PullMatch other = (PullMatch) obj;
         return new EqualsBuilder().
-                append(matchingKey, other.matchingKey).
                 append(matchTarget, other.matchTarget).
-                append(linkingUserKey, other.linkingUserKey).
+                append(any, other.any).
+                append(linkedAccount, other.linkedAccount).
                 build();
     }
 
@@ -110,7 +90,8 @@ public final class PullMatch implements Serializable {
     public String toString() {
         return "PullMatch{"
                 + "matchTarget=" + matchTarget
-                + ", matchingKey=" + matchingKey
-                + ", linkingUserKey=" + linkingUserKey + '}';
+                + ", any=" + any
+                + ", linkedAccount=" + linkedAccount
+                + '}';
     }
 }
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/UserDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/UserDAO.java
index 008791e..dacb88c 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/UserDAO.java
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/UserDAO.java
@@ -21,6 +21,7 @@ package org.apache.syncope.core.persistence.api.dao;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.core.persistence.api.entity.Privilege;
@@ -62,6 +63,8 @@ public interface UserDAO extends AnyDAO<User> {
 
     boolean linkedAccountExists(String userKey, String connObjectKeyValue);
 
+    Optional<? extends LinkedAccount> findLinkedAccount(ExternalResource resource, String connObjectKeyValue);
+
     List<LinkedAccount> findLinkedAccounts(String userKey);
 
     List<LinkedAccount> findLinkedAccountsByResource(ExternalResource resource);
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAUserDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAUserDAO.java
index a27495b..5698043 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAUserDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAUserDAO.java
@@ -25,6 +25,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -554,6 +555,20 @@ public class JPAUserDAO extends AbstractAnyDAO<User> implements UserDAO {
         return !query.getResultList().isEmpty();
     }
 
+    @Override
+    public Optional<? extends LinkedAccount> findLinkedAccount(
+            final ExternalResource resource, final String connObjectKeyValue) {
+
+        TypedQuery<LinkedAccount> query = entityManager().createQuery(
+                "SELECT e FROM " + JPALinkedAccount.class.getSimpleName() + " e "
+                + "WHERE e.resource=:resource AND e.connObjectKeyValue=:connObjectKeyValue", LinkedAccount.class);
+        query.setParameter("resource", resource);
+        query.setParameter("connObjectKeyValue", connObjectKeyValue);
+
+        List<LinkedAccount> result = query.getResultList();
+        return query.getResultList().isEmpty() ? Optional.empty() : Optional.of(result.get(0));
+    }
+
     @Transactional(readOnly = true)
     @Override
     public List<LinkedAccount> findLinkedAccounts(final String userKey) {
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/PolicyTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/PolicyTest.java
index 0b2ed8f..34ea16e 100644
--- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/PolicyTest.java
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/PolicyTest.java
@@ -92,7 +92,7 @@ public class PolicyTest extends AbstractTest {
                 POJOHelper.deserialize(pushCR.getImplementation().getBody(), DefaultPushCorrelationRuleConf.class);
         assertNotNull(pushCRConf);
         assertEquals(1, pushCRConf.getSchemas().size());
-        assertTrue(pushCRConf.getSchemas().contains("email"));
+        assertTrue(pushCRConf.getSchemas().contains("surname"));
     }
 
     @Test
diff --git a/core/persistence-jpa/src/test/resources/domains/MasterContent.xml b/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
index 0809c01..9043a95 100644
--- a/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
+++ b/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
@@ -511,7 +511,7 @@ under the License.
   <!-- push policies -->
   <PushPolicy id="fb6530e5-892d-4f47-a46b-180c5b6c5c83" description="a push policy" conflictResolutionAction="IGNORE"/>
   <Implementation id="TestPushCorrelationRule" type="PUSH_CORRELATION_RULE" engine="JAVA"
-                  body='{"@class":"org.apache.syncope.common.lib.policy.DefaultPushCorrelationRuleConf","name":"org.apache.syncope.common.lib.policy.DefaultPushCorrelationRuleConf","schemas":["email"]}'/>
+                  body='{"@class":"org.apache.syncope.common.lib.policy.DefaultPushCorrelationRuleConf","name":"org.apache.syncope.common.lib.policy.DefaultPushCorrelationRuleConf","schemas":["surname"]}'/>
   <PushCorrelationRuleEntity id="24463935-32a0-4272-bc78-04d6d0adc69e" pushPolicy_id="fb6530e5-892d-4f47-a46b-180c5b6c5c83" 
                              anyType_id="USER" implementation_id="TestPushCorrelationRule"/>
   
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/VirAttrHandler.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/VirAttrHandler.java
index 72bb198..886457c 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/VirAttrHandler.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/VirAttrHandler.java
@@ -23,10 +23,19 @@ import java.util.Map;
 import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.Membership;
 import org.apache.syncope.core.persistence.api.entity.VirSchema;
+import org.identityconnectors.framework.common.objects.ConnectorObject;
 
 public interface VirAttrHandler {
 
     /**
+     * Updates cache with values from external resource.
+     *
+     * @param any any object
+     * @param connObj connector object from external resource
+     */
+    void setValues(Any<?> any, ConnectorObject connObj);
+
+    /**
      * Query external resource (or cache, if configured) associated to the given any for values associated to the given
      * virtual schema, not related to any membership.
      *
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePullExecutor.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePullExecutor.java
index 5913036..c0ca069 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePullExecutor.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePullExecutor.java
@@ -20,7 +20,6 @@ package org.apache.syncope.core.provisioning.api.pushpull;
 
 import java.util.List;
 import org.apache.syncope.common.lib.to.PullTaskTO;
-import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
 import org.apache.syncope.core.provisioning.api.Connector;
 import org.quartz.JobExecutionException;
@@ -33,6 +32,5 @@ public interface SyncopeSinglePullExecutor {
             Connector connector,
             String connObjectKey,
             String connObjectValue,
-            Realm realm,
             PullTaskTO pullTaskTO) throws JobExecutionException;
 }
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePushExecutor.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePushExecutor.java
index 1f8c344..8053a65 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePushExecutor.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePushExecutor.java
@@ -22,10 +22,10 @@ import java.util.List;
 import org.apache.syncope.common.lib.to.PushTaskTO;
 import org.apache.syncope.core.persistence.api.entity.Any;
 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.quartz.JobExecutionException;
 
-@FunctionalInterface
 public interface SyncopeSinglePushExecutor {
 
     List<ProvisioningReport> push(
@@ -33,4 +33,10 @@ public interface SyncopeSinglePushExecutor {
             Connector connector,
             Any<?> any,
             PushTaskTO pushTaskTO) throws JobExecutionException;
+
+    ProvisioningReport push(
+            Provision provision,
+            Connector connector,
+            LinkedAccount account,
+            PushTaskTO pushTaskTO) throws JobExecutionException;
 }
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/UserPushResultHandler.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/UserPushResultHandler.java
index f96c0ef..06adc0b 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/UserPushResultHandler.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/UserPushResultHandler.java
@@ -18,6 +18,10 @@
  */
 package org.apache.syncope.core.provisioning.api.pushpull;
 
+import org.apache.syncope.core.persistence.api.entity.resource.Provision;
+import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount;
+
 public interface UserPushResultHandler extends SyncopePushResultHandler {
 
+    boolean handle(LinkedAccount account, Provision provision);
 }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultAnyObjectProvisioningManager.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultAnyObjectProvisioningManager.java
index 1efe3b6..8a7813a 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultAnyObjectProvisioningManager.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultAnyObjectProvisioningManager.java
@@ -42,7 +42,6 @@ import org.apache.syncope.core.workflow.api.AnyObjectWorkflowAdapter;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
-
 import javax.annotation.Resource;
 
 public class DefaultAnyObjectProvisioningManager implements AnyObjectProvisioningManager {
@@ -86,7 +85,7 @@ public class DefaultAnyObjectProvisioningManager implements AnyObjectProvisionin
                 created.getPropByRes(),
                 anyObjectCR.getVirAttrs(),
                 excludedResources);
-        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, adminUser);
 
         return Pair.of(created.getResult(), propagationReporter.getStatuses());
     }
@@ -114,7 +113,7 @@ public class DefaultAnyObjectProvisioningManager implements AnyObjectProvisionin
                 null,
                 anyObjectUR.getVirAttrs(),
                 excludedResources);
-        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, adminUser);
 
         return Pair.of(updated.getResult(), propagationReporter.getStatuses());
     }
@@ -143,7 +142,7 @@ public class DefaultAnyObjectProvisioningManager implements AnyObjectProvisionin
                 propByRes,
                 null,
                 excludedResources);
-        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, adminUser);
 
         try {
             awfAdapter.delete(key);
@@ -180,7 +179,7 @@ public class DefaultAnyObjectProvisioningManager implements AnyObjectProvisionin
                 null,
                 null,
                 null);
-        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, adminUser);
 
         return propagationReporter.getStatuses();
     }
@@ -200,7 +199,7 @@ public class DefaultAnyObjectProvisioningManager implements AnyObjectProvisionin
                 anyObjectDAO.findAllResourceKeys(key).stream().
                         filter(resource -> !resources.contains(resource)).
                         collect(Collectors.toList()));
-        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, adminUser);
 
         return propagationReporter.getStatuses();
     }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultGroupProvisioningManager.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultGroupProvisioningManager.java
index 08783bb..a0ec90f 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultGroupProvisioningManager.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultGroupProvisioningManager.java
@@ -45,7 +45,6 @@ import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
 import org.apache.syncope.core.workflow.api.GroupWorkflowAdapter;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
-
 import javax.annotation.Resource;
 
 public class DefaultGroupProvisioningManager implements GroupProvisioningManager {
@@ -82,7 +81,7 @@ public class DefaultGroupProvisioningManager implements GroupProvisioningManager
                 created.getPropByRes(),
                 groupCR.getVirAttrs(),
                 Set.of());
-        PropagationReporter propagationReporter = taskExecutor.execute(tasks, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(tasks, nullPriorityAsync, adminUser);
 
         return Pair.of(created.getResult(), propagationReporter.getStatuses());
     }
@@ -108,7 +107,7 @@ public class DefaultGroupProvisioningManager implements GroupProvisioningManager
                 created.getPropByRes(),
                 groupCR.getVirAttrs(),
                 excludedResources);
-        PropagationReporter propagationReporter = taskExecutor.execute(tasks, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(tasks, nullPriorityAsync, adminUser);
 
         return Pair.of(created.getResult(), propagationReporter.getStatuses());
     }
@@ -136,7 +135,7 @@ public class DefaultGroupProvisioningManager implements GroupProvisioningManager
                 null,
                 groupUR.getVirAttrs(),
                 excludedResources);
-        PropagationReporter propagationReporter = taskExecutor.execute(tasks, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(tasks, nullPriorityAsync, adminUser);
 
         return Pair.of(updated.getResult(), propagationReporter.getStatuses());
     }
@@ -180,7 +179,7 @@ public class DefaultGroupProvisioningManager implements GroupProvisioningManager
                 null,
                 null));
 
-        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, adminUser);
 
         gwfAdapter.delete(key);
 
@@ -208,7 +207,7 @@ public class DefaultGroupProvisioningManager implements GroupProvisioningManager
                 null,
                 null,
                 null);
-        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, adminUser);
 
         return propagationReporter.getStatuses();
     }
@@ -228,7 +227,7 @@ public class DefaultGroupProvisioningManager implements GroupProvisioningManager
                 groupDAO.findAllResourceKeys(key).stream().
                         filter(resource -> !resources.contains(resource)).
                         collect(Collectors.toList()));
-        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, adminUser);
 
         return propagationReporter.getStatuses();
     }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultUserProvisioningManager.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultUserProvisioningManager.java
index 4439bc6..bb86889 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultUserProvisioningManager.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultUserProvisioningManager.java
@@ -101,7 +101,7 @@ public class DefaultUserProvisioningManager implements UserProvisioningManager {
                 created.getPropByLinkedAccount(),
                 userCR.getVirAttrs(),
                 excludedResources);
-        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, adminUser);
 
         return Pair.of(created.getResult().getLeft(), propagationReporter.getStatuses());
     }
@@ -111,7 +111,7 @@ public class DefaultUserProvisioningManager implements UserProvisioningManager {
         UserWorkflowResult<Pair<UserUR, Boolean>> updated = uwfAdapter.update(userUR);
 
         List<PropagationTaskInfo> taskInfos = propagationManager.getUserUpdateTasks(updated);
-        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, adminUser);
 
         return Pair.of(updated.getResult().getLeft(), propagationReporter.getStatuses());
     }
@@ -172,7 +172,7 @@ public class DefaultUserProvisioningManager implements UserProvisioningManager {
 
         List<PropagationTaskInfo> taskInfos = propagationManager.getUserUpdateTasks(
                 updated, updated.getResult().getLeft().getPassword() != null, excludedResources);
-        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, adminUser);
 
         return Pair.of(updated.getResult().getLeft(), propagationReporter.getStatuses());
     }
@@ -205,7 +205,7 @@ public class DefaultUserProvisioningManager implements UserProvisioningManager {
                 propByRes,
                 propByLinkedAccount,
                 excludedResources);
-        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, adminUser);
 
         try {
             uwfAdapter.delete(key);
@@ -266,7 +266,7 @@ public class DefaultUserProvisioningManager implements UserProvisioningManager {
                 null,
                 null,
                 null);
-        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, adminUser);
 
         return propagationReporter.getStatuses();
     }
@@ -285,7 +285,7 @@ public class DefaultUserProvisioningManager implements UserProvisioningManager {
                     updated.getLeft().getPropByRes(),
                     updated.getLeft().getPropByLinkedAccount(),
                     updated.getLeft().getPerformedTasks()));
-            taskExecutor.execute(taskInfos, false, this.adminUser);
+            taskExecutor.execute(taskInfos, false, adminUser);
         }
     }
 
@@ -318,7 +318,7 @@ public class DefaultUserProvisioningManager implements UserProvisioningManager {
                 Pair.of(userUR, (Boolean) null), propByRes, null, "update");
 
         List<PropagationTaskInfo> taskInfos = propagationManager.getUserUpdateTasks(wfResult, changePwd, null);
-        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, adminUser);
 
         return propagationReporter.getStatuses();
     }
@@ -345,7 +345,7 @@ public class DefaultUserProvisioningManager implements UserProvisioningManager {
                 userDAO.findAllResourceKeys(key).stream().
                         filter(resource -> !resources.contains(resource)).
                         collect(Collectors.toList()));
-        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, this.adminUser);
+        PropagationReporter propagationReporter = taskExecutor.execute(taskInfos, nullPriorityAsync, adminUser);
 
         return propagationReporter.getStatuses();
     }
@@ -360,6 +360,6 @@ public class DefaultUserProvisioningManager implements UserProvisioningManager {
         UserWorkflowResult<Pair<UserUR, Boolean>> updated = uwfAdapter.confirmPasswordReset(key, token, password);
 
         List<PropagationTaskInfo> taskInfos = propagationManager.getUserUpdateTasks(updated);
-        taskExecutor.execute(taskInfos, false, this.adminUser);
+        taskExecutor.execute(taskInfos, false, adminUser);
     }
 }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/MappingManagerImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/MappingManagerImpl.java
index 6c9118f..63fd67d 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/MappingManagerImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/MappingManagerImpl.java
@@ -195,13 +195,13 @@ public class MappingManagerImpl implements MappingManager {
                 any, provision, any.getPlainAttrs());
 
         Set<Attribute> attributes = new HashSet<>();
-        String connObjectKey = null;
+        String[] connObjectKeyValue = new String[1];
 
-        for (Item mapItem : MappingUtils.getPropagationItems(provision.getMapping().getItems())) {
+        MappingUtils.getPropagationItems(provision.getMapping().getItems().stream()).forEach(mapItem -> {
             LOG.debug("Processing expression '{}'", mapItem.getIntAttrName());
 
             try {
-                String processedConnObjectKey = processPreparedAttr(
+                String processedConnObjectKeyValue = processPreparedAttr(
                         prepareAttr(
                                 provision,
                                 mapItem,
@@ -211,27 +211,28 @@ public class MappingManagerImpl implements MappingManager {
                                 AccountGetter.DEFAULT,
                                 PlainAttrGetter.DEFAULT),
                         attributes);
-                if (processedConnObjectKey != null) {
-                    connObjectKey = processedConnObjectKey;
+                if (processedConnObjectKeyValue != null) {
+                    connObjectKeyValue[0] = processedConnObjectKeyValue;
                 }
             } catch (Exception e) {
                 LOG.error("Expression '{}' processing failed", mapItem.getIntAttrName(), e);
             }
-        }
+        });
 
-        Optional<? extends MappingItem> connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision);
-        if (connObjectKeyItem.isPresent()) {
-            Attribute connObjectKeyExtAttr = AttributeUtil.find(connObjectKeyItem.get().getExtAttrName(), attributes);
-            if (connObjectKeyExtAttr != null) {
-                attributes.remove(connObjectKeyExtAttr);
-                attributes.add(AttributeBuilder.build(connObjectKeyItem.get().getExtAttrName(), connObjectKey));
+        MappingUtils.getConnObjectKeyItem(provision).ifPresent(connObjectKeyItem -> {
+            Attribute connObjectKeyAttr = AttributeUtil.find(connObjectKeyItem.getExtAttrName(), attributes);
+            if (connObjectKeyAttr != null) {
+                attributes.remove(connObjectKeyAttr);
+                attributes.add(AttributeBuilder.build(connObjectKeyItem.getExtAttrName(), connObjectKeyValue[0]));
             }
-            Name name = MappingUtils.evaluateNAME(any, provision, connObjectKey);
+            Name name = MappingUtils.evaluateNAME(any, provision, connObjectKeyValue[0]);
             attributes.add(name);
-            if (connObjectKey != null && !connObjectKey.equals(name.getNameValue()) && connObjectKeyExtAttr == null) {
-                attributes.add(AttributeBuilder.build(connObjectKeyItem.get().getExtAttrName(), connObjectKey));
+            if (connObjectKeyAttr == null
+                    && connObjectKeyValue[0] != null && !connObjectKeyValue[0].equals(name.getNameValue())) {
+
+                attributes.add(AttributeBuilder.build(connObjectKeyItem.getExtAttrName(), connObjectKeyValue[0]));
             }
-        }
+        });
 
         if (enable != null) {
             attributes.add(AttributeBuilder.buildEnabled(enable));
@@ -243,7 +244,7 @@ public class MappingManagerImpl implements MappingManager {
             }
         }
 
-        return Pair.of(connObjectKey, attributes);
+        return Pair.of(connObjectKeyValue[0], attributes);
     }
 
     @Transactional(readOnly = true)
@@ -261,7 +262,7 @@ public class MappingManagerImpl implements MappingManager {
 
         Set<Attribute> attributes = new HashSet<>();
 
-        for (Item mapItem : MappingUtils.getPropagationItems(provision.getMapping().getItems())) {
+        MappingUtils.getPropagationItems(provision.getMapping().getItems().stream()).forEach(mapItem -> {
             LOG.debug("Processing expression '{}'", mapItem.getIntAttrName());
 
             try {
@@ -290,7 +291,7 @@ public class MappingManagerImpl implements MappingManager {
             } catch (Exception e) {
                 LOG.error("Expression '{}' processing failed", mapItem.getIntAttrName(), e);
             }
-        }
+        });
 
         String connObjectKey = account.getConnObjectKeyValue();
         MappingUtils.getConnObjectKeyItem(provision).ifPresent(connObjectKeyItem -> {
@@ -339,15 +340,15 @@ public class MappingManagerImpl implements MappingManager {
         LOG.debug("Preparing resource attributes for {} with orgUnit {}", realm, orgUnit);
 
         Set<Attribute> attributes = new HashSet<>();
-        String connObjectKey = null;
+        String[] connObjectKeyValue = new String[1];
 
-        for (Item orgUnitItem : MappingUtils.getPropagationItems(orgUnit.getItems())) {
+        MappingUtils.getPropagationItems(orgUnit.getItems().stream()).forEach(orgUnitItem -> {
             LOG.debug("Processing expression '{}'", orgUnitItem.getIntAttrName());
 
             String value = getIntValue(realm, orgUnitItem);
 
             if (orgUnitItem.isConnObjectKey()) {
-                connObjectKey = value;
+                connObjectKeyValue[0] = value;
             }
 
             Attribute alreadyAdded = AttributeUtil.find(orgUnitItem.getExtAttrName(), attributes);
@@ -368,19 +369,19 @@ public class MappingManagerImpl implements MappingManager {
 
                 attributes.add(AttributeBuilder.build(orgUnitItem.getExtAttrName(), values));
             }
-        }
+        });
 
         Optional<? extends OrgUnitItem> connObjectKeyItem = orgUnit.getConnObjectKeyItem();
         if (connObjectKeyItem.isPresent()) {
-            Attribute connObjectKeyExtAttr = AttributeUtil.find(connObjectKeyItem.get().getExtAttrName(), attributes);
-            if (connObjectKeyExtAttr != null) {
-                attributes.remove(connObjectKeyExtAttr);
-                attributes.add(AttributeBuilder.build(connObjectKeyItem.get().getExtAttrName(), connObjectKey));
+            Attribute connObjectKeyAttr = AttributeUtil.find(connObjectKeyItem.get().getExtAttrName(), attributes);
+            if (connObjectKeyAttr != null) {
+                attributes.remove(connObjectKeyAttr);
+                attributes.add(AttributeBuilder.build(connObjectKeyItem.get().getExtAttrName(), connObjectKeyValue[0]));
             }
-            attributes.add(MappingUtils.evaluateNAME(realm, orgUnit, connObjectKey));
+            attributes.add(MappingUtils.evaluateNAME(realm, orgUnit, connObjectKeyValue[0]));
         }
 
-        return Pair.of(connObjectKey, attributes);
+        return Pair.of(connObjectKeyValue[0], attributes);
     }
 
     protected String getPasswordAttrValue(final Provision provision, final Account account, final String defaultValue) {
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 e41eeb0..11cdc39 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
@@ -22,25 +22,22 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.syncope.core.persistence.api.dao.AllowedSchemas;
 import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
+import org.apache.syncope.core.persistence.api.entity.LinkingMappingItem;
 import org.apache.syncope.core.persistence.api.entity.Membership;
 import org.apache.syncope.core.persistence.api.entity.VirSchema;
 import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
-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.provisioning.api.Connector;
 import org.apache.syncope.core.provisioning.api.ConnectorFactory;
-import org.apache.syncope.core.provisioning.api.MappingManager;
 import org.apache.syncope.core.provisioning.api.VirAttrHandler;
 import org.apache.syncope.core.provisioning.api.cache.VirAttrCache;
 import org.apache.syncope.core.provisioning.api.cache.VirAttrCacheValue;
-import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
+import org.apache.syncope.core.provisioning.java.pushpull.OutboundMatcher;
 import org.identityconnectors.framework.common.objects.Attribute;
-import org.identityconnectors.framework.common.objects.AttributeBuilder;
 import org.identityconnectors.framework.common.objects.ConnectorObject;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -61,86 +58,85 @@ public class VirAttrHandlerImpl implements VirAttrHandler {
     private VirAttrCache virAttrCache;
 
     @Autowired
-    private MappingManager mappingManager;
+    private OutboundMatcher outboundMatcher;
 
     @Autowired
     private AnyUtilsFactory anyUtilsFactory;
 
+    @Override
+    public void setValues(final Any<?> any, final ConnectorObject connObj) {
+        if (any == null) {
+            LOG.warn("Null any passed, ignoring");
+            return;
+        }
+
+        AllowedSchemas<VirSchema> schemas =
+                anyUtilsFactory.getInstance(any).dao().findAllowedSchemas(any, VirSchema.class);
+        Stream.concat(
+                schemas.getForSelf().stream(),
+                schemas.getForMemberships().values().stream().flatMap(Set::stream)).forEach(schema -> {
+            Attribute attr = connObj.getAttributeByName(schema.getExtAttrName());
+            if (attr == null) {
+                virAttrCache.expire(any.getType().getKey(), any.getKey(), schema.getKey());
+            } else {
+                VirAttrCacheValue virAttrCacheValue = new VirAttrCacheValue(attr.getValue());
+                virAttrCache.put(
+                        any.getType().getKey(),
+                        any.getKey(),
+                        schema.getKey(),
+                        virAttrCacheValue);
+                LOG.debug("Values for {} set in cache: {}", schema, virAttrCacheValue);
+            }
+        });
+    }
+
     private Map<VirSchema, List<String>> getValues(final Any<?> any, final Set<VirSchema> schemas) {
-        Set<ExternalResource> ownedResources = anyUtilsFactory.getInstance(any).getAllResources(any);
+        Set<ExternalResource> resources = anyUtilsFactory.getInstance(any).getAllResources(any);
 
         Map<VirSchema, List<String>> result = new HashMap<>();
 
         Map<Provision, Set<VirSchema>> toRead = new HashMap<>();
 
-        schemas.forEach(schema -> {
-            if (ownedResources.contains(schema.getProvision().getResource())) {
-                VirAttrCacheValue virAttrCacheValue =
-                        virAttrCache.get(any.getType().getKey(), any.getKey(), schema.getKey());
-
-                if (virAttrCache.isValidEntry(virAttrCacheValue)) {
-                    LOG.debug("Values for {} found in cache: {}", schema, virAttrCacheValue);
-                    result.put(schema, virAttrCacheValue.getValues());
-                } else if (schema.getProvision().getAnyType().equals(any.getType())) {
-                    Set<VirSchema> schemasToRead = toRead.get(schema.getProvision());
-                    if (schemasToRead == null) {
-                        schemasToRead = new HashSet<>();
-                        toRead.put(schema.getProvision(), schemasToRead);
-                    }
-                    schemasToRead.add(schema);
+        schemas.stream().filter(schema -> resources.contains(schema.getProvision().getResource())).forEach(schema -> {
+            VirAttrCacheValue virAttrCacheValue =
+                    virAttrCache.get(any.getType().getKey(), any.getKey(), schema.getKey());
+
+            if (virAttrCache.isValidEntry(virAttrCacheValue)) {
+                LOG.debug("Values for {} found in cache: {}", schema, virAttrCacheValue);
+                result.put(schema, virAttrCacheValue.getValues());
+            } else if (schema.getProvision().getAnyType().equals(any.getType())) {
+                Set<VirSchema> schemasToRead = toRead.get(schema.getProvision());
+                if (schemasToRead == null) {
+                    schemasToRead = new HashSet<>();
+                    toRead.put(schema.getProvision(), schemasToRead);
                 }
-            } else {
-                LOG.debug("Not considering {} since {} is not assigned to {}",
-                        schema, any, schema.getProvision().getResource());
+                schemasToRead.add(schema);
             }
         });
 
         toRead.forEach((provision, schemasToRead) -> {
             LOG.debug("About to read from {}: {}", provision, schemasToRead);
 
-            Optional<? extends MappingItem> connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision);
-            String connObjectKeyValue = connObjectKeyItem.isPresent()
-                    ? mappingManager.getConnObjectKeyValue(any, provision).orElse(null)
-                    : null;
-            if (connObjectKeyItem.isEmpty() || connObjectKeyValue == null) {
-                LOG.error("No ConnObjectKey or value found for {}, ignoring...", provision);
-            } else {
-                Set<MappingItem> linkingMappingItems = new HashSet<>();
-                linkingMappingItems.add(connObjectKeyItem.get());
-                linkingMappingItems.addAll(schemasToRead.stream().
-                        map(VirSchema::asLinkingMappingItem).collect(Collectors.toSet()));
-
-                Connector connector = connFactory.getConnector(provision.getResource());
-                try {
-                    ConnectorObject connectorObject = connector.getObject(
-                            provision.getObjectClass(),
-                            AttributeBuilder.build(connObjectKeyItem.get().getExtAttrName(), connObjectKeyValue),
-                            provision.isIgnoreCaseMatch(),
-                            MappingUtils.buildOperationOptions(linkingMappingItems.iterator()));
-
-                    if (connectorObject == null) {
-                        LOG.debug("No read from {} with filter '{} == {}'",
-                                provision, connObjectKeyItem.get().getExtAttrName(), connObjectKeyValue);
-                    } else {
-                        schemasToRead.forEach(schema -> {
-                            Attribute attr = connectorObject.getAttributeByName(schema.getExtAttrName());
-                            if (attr != null) {
-                                VirAttrCacheValue virAttrCacheValue = new VirAttrCacheValue(attr.getValue());
-                                virAttrCache.put(
-                                        any.getType().getKey(),
-                                        any.getKey(),
-                                        schema.getKey(),
-                                        virAttrCacheValue);
-                                LOG.debug("Values for {} set in cache: {}", schema, virAttrCacheValue);
-
-                                result.put(schema, virAttrCacheValue.getValues());
-                            }
-                        });
-                    }
-                } catch (Exception e) {
-                    LOG.error("Error reading from {}", provision, e);
+            outboundMatcher.match(
+                    connFactory.getConnector(provision.getResource()),
+                    any,
+                    provision,
+                    schemasToRead.stream().map(VirSchema::asLinkingMappingItem).toArray(LinkingMappingItem[]::new)).
+                    forEach(connObj -> schemasToRead.forEach(schema -> {
+
+                Attribute attr = connObj.getAttributeByName(schema.getExtAttrName());
+                if (attr != null) {
+                    VirAttrCacheValue virAttrCacheValue = new VirAttrCacheValue(attr.getValue());
+                    virAttrCache.put(
+                            any.getType().getKey(),
+                            any.getKey(),
+                            schema.getKey(),
+                            virAttrCacheValue);
+                    LOG.debug("Values for {} set in cache: {}", schema, virAttrCacheValue);
+
+                    result.put(schema, virAttrCacheValue.getValues());
                 }
-            }
+            }));
         });
 
         return result;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java
index 406b484..4f9649a 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java
@@ -206,7 +206,7 @@ abstract class AbstractAnyDataBinder {
     private List<String> evaluateMandatoryCondition(final Provision provision, final Any<?> any) {
         List<String> missingAttrNames = new ArrayList<>();
 
-        MappingUtils.getPropagationItems(provision.getMapping().getItems()).forEach(mapItem -> {
+        MappingUtils.getPropagationItems(provision.getMapping().getItems().stream()).forEach(mapItem -> {
             IntAttrName intAttrName = null;
             try {
                 intAttrName = intAttrNameParser.parse(mapItem.getIntAttrName(), provision.getAnyType().getKind());
@@ -344,7 +344,7 @@ abstract class AbstractAnyDataBinder {
                 filter(resource -> resource.getProvision(any.getType()).isPresent()
                 && resource.getProvision(any.getType()).get().getMapping() != null).
                 forEach(resource -> MappingUtils.getPropagationItems(
-                resource.getProvision(any.getType()).get().getMapping().getItems()).stream().
+                resource.getProvision(any.getType()).get().getMapping().getItems().stream()).
                 filter(item -> (schema.getKey().equals(item.getIntAttrName()))).
                 forEach(item -> {
                     propByRes.add(ResourceOperation.UPDATE, resource.getKey());
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyObjectDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyObjectDataBinderImpl.java
index 4cfae47..284abeb 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyObjectDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyObjectDataBinderImpl.java
@@ -72,6 +72,7 @@ public class AnyObjectDataBinderImpl extends AbstractAnyDataBinder implements An
         return getAnyObjectTO(anyObjectDAO.authFind(key), true);
     }
 
+    @Transactional(readOnly = true)
     @Override
     public AnyObjectTO getAnyObjectTO(final AnyObject anyObject, final boolean details) {
         AnyObjectTO anyObjectTO = new AnyObjectTO();
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyTypeDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyTypeDataBinderImpl.java
index dcdef11..6cf9a19 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyTypeDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyTypeDataBinderImpl.java
@@ -37,6 +37,7 @@ import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.AnyType;
 import org.apache.syncope.core.persistence.api.entity.AnyTypeClass;
 import org.apache.syncope.common.lib.types.EntitlementsHolder;
+import org.apache.syncope.core.persistence.api.entity.Entity;
 import org.apache.syncope.core.provisioning.api.data.AnyTypeDataBinder;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
@@ -85,7 +86,7 @@ public class AnyTypeDataBinderImpl implements AnyTypeDataBinder {
                 }));
 
                 added.forEach(entitlement -> authorities.add(
-                    new SyncopeGrantedAuthority(entitlement, SyncopeConstants.ROOT_REALM)));
+                        new SyncopeGrantedAuthority(entitlement, SyncopeConstants.ROOT_REALM)));
 
                 accessToken.setAuthorities(ENCRYPTOR.encode(
                         POJOHelper.serialize(authorities), CipherAlgorithm.AES).
@@ -160,12 +161,9 @@ public class AnyTypeDataBinderImpl implements AnyTypeDataBinder {
     @Override
     public AnyTypeTO getAnyTypeTO(final AnyType anyType) {
         AnyTypeTO anyTypeTO = new AnyTypeTO();
-
         anyTypeTO.setKey(anyType.getKey());
         anyTypeTO.setKind(anyType.getKind());
-        anyType.getClasses().forEach(anyTypeClass -> anyTypeTO.getClasses().add(anyTypeClass.getKey()));
-
+        anyTypeTO.getClasses().addAll(anyType.getClasses().stream().map(Entity::getKey).collect(Collectors.toList()));
         return anyTypeTO;
     }
-
 }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java
index 480a5ae..133f171 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java
@@ -26,9 +26,9 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
-import org.apache.syncope.common.lib.collections.IteratorChain;
 import org.apache.syncope.common.lib.SyncopeClientCompositeException;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.to.AnyTypeClassTO;
@@ -230,18 +230,17 @@ public class ResourceDataBinderImpl implements ResourceDataBinder {
                     }
 
                     AnyTypeClassTO allowedSchemas = new AnyTypeClassTO();
-                    for (Iterator<AnyTypeClass> itor = new IteratorChain<>(
-                            provision.getAnyType().getClasses().iterator(),
-                            provision.getAuxClasses().iterator()); itor.hasNext();) {
+                    Stream.concat(
+                            provision.getAnyType().getClasses().stream(),
+                            provision.getAuxClasses().stream()).forEach(anyTypeClass -> {
 
-                        AnyTypeClass anyTypeClass = itor.next();
                         allowedSchemas.getPlainSchemas().addAll(anyTypeClass.getPlainSchemas().stream().
                                 map(Entity::getKey).collect(Collectors.toList()));
                         allowedSchemas.getDerSchemas().addAll(anyTypeClass.getDerSchemas().stream().
                                 map(Entity::getKey).collect(Collectors.toList()));
                         allowedSchemas.getVirSchemas().addAll(anyTypeClass.getVirSchemas().stream().
                                 map(Entity::getKey).collect(Collectors.toList()));
-                    }
+                    });
 
                     populateMapping(
                             provisionTO.getMapping(),
@@ -627,7 +626,7 @@ public class ResourceDataBinderImpl implements ResourceDataBinder {
 
         resourceTO.setConnector(Optional.ofNullable(connector).map(Entity::getKey).orElse(null));
         resourceTO.setConnectorDisplayName(Optional.ofNullable(connector)
-            .map(ConnInstance::getDisplayName).orElse(null));
+                .map(ConnInstance::getDisplayName).orElse(null));
 
         // set the provision information
         resource.getProvisions().forEach(provision -> {
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/JobManagerImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/JobManagerImpl.java
index b14bf09..257efae 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/JobManagerImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/JobManagerImpl.java
@@ -102,7 +102,7 @@ public class JobManagerImpl implements JobManager, SyncopeCoreLoader {
 
     @Resource(name = "adminUser")
     private String adminUser;
-    
+
     private boolean disableQuartzInstance;
 
     public void setDisableQuartzInstance(final boolean disableQuartzInstance) {
@@ -222,7 +222,7 @@ public class JobManagerImpl implements JobManager, SyncopeCoreLoader {
 
     @Override
     public Map<String, Object> register(final SchedTask task, final Date startAt, final long interruptMaxRetries,
-                                        final String executor)
+            final String executor)
             throws SchedulerException {
 
         TaskJob job = createSpringBean(TaskJob.class);
@@ -258,7 +258,7 @@ public class JobManagerImpl implements JobManager, SyncopeCoreLoader {
 
     @Override
     public void register(final Report report, final Date startAt, final long interruptMaxRetries,
-                         final String executor) throws SchedulerException {
+            final String executor) throws SchedulerException {
 
         ReportJob job = createSpringBean(ReportJob.class);
         job.setReportKey(report.getKey());
@@ -347,7 +347,7 @@ public class JobManagerImpl implements JobManager, SyncopeCoreLoader {
             for (Iterator<SchedTask> it = tasks.iterator(); it.hasNext() && !loadException;) {
                 SchedTask task = it.next();
                 try {
-                    register(task, task.getStartAt(), conf.getRight(), this.adminUser);
+                    register(task, task.getStartAt(), conf.getRight(), adminUser);
                 } catch (Exception e) {
                     LOG.error("While loading job instance for task " + task.getKey(), e);
                     loadException = true;
@@ -361,7 +361,7 @@ public class JobManagerImpl implements JobManager, SyncopeCoreLoader {
                 for (Iterator<Report> it = reportDAO.findAll().iterator(); it.hasNext() && !loadException;) {
                     Report report = it.next();
                     try {
-                        register(report, null, conf.getRight(), this.adminUser);
+                        register(report, null, conf.getRight(), adminUser);
                     } catch (Exception e) {
                         LOG.error("While loading job instance for report " + report.getName(), e);
                         loadException = true;
@@ -387,7 +387,7 @@ public class JobManagerImpl implements JobManager, SyncopeCoreLoader {
 
                 try {
                     NotificationJob job = createSpringBean(NotificationJob.class);
-                    Map<String, Object> jobData = createJobMapForExecutionContext(this.adminUser);
+                    Map<String, Object> jobData = createJobMapForExecutionContext(adminUser);
                     registerJob(
                             NOTIFICATION_JOB.getName(),
                             job,
@@ -403,7 +403,7 @@ public class JobManagerImpl implements JobManager, SyncopeCoreLoader {
             LOG.debug("Registering {}", SystemLoadReporterJob.class);
             try {
                 SystemLoadReporterJob job = createSpringBean(SystemLoadReporterJob.class);
-                Map<String, Object> jobData = createJobMapForExecutionContext(this.adminUser);
+                Map<String, Object> jobData = createJobMapForExecutionContext(adminUser);
                 registerJob(
                         StringUtils.uncapitalize(SystemLoadReporterJob.class.getSimpleName()),
                         job,
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReconciliationReportlet.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReconciliationReportlet.java
index 8b830ac..70077db 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReconciliationReportlet.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReconciliationReportlet.java
@@ -288,7 +288,7 @@ public class ReconciliationReportlet extends AbstractReportlet {
                             provision.getObjectClass(),
                             AttributeBuilder.build(connObjectKeyItem.get().getExtAttrName(), connObjectKeyValue),
                             provision.isIgnoreCaseMatch(),
-                            MappingUtils.buildOperationOptions(provision.getMapping().getItems().iterator()));
+                            MappingUtils.buildOperationOptions(provision.getMapping().getItems().stream()));
 
                     if (connectorObject == null) {
                         // 2. not found on resource?
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 40e9943..bbfc20d 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
@@ -30,9 +30,7 @@ import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.concurrent.atomic.AtomicReference;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.syncope.common.lib.collections.IteratorChain;
 import org.apache.syncope.common.lib.to.ExecTO;
-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.ExecStatus;
@@ -42,7 +40,6 @@ import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.TaskDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
-import org.apache.syncope.core.persistence.api.entity.VirSchema;
 import org.apache.syncope.core.persistence.api.entity.task.PropagationTask;
 import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
 import org.apache.syncope.core.provisioning.api.Connector;
@@ -55,22 +52,19 @@ import org.apache.syncope.core.provisioning.java.utils.ConnObjectUtils;
 import org.apache.syncope.core.provisioning.api.utils.ExceptionUtils2;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
-import org.apache.syncope.core.persistence.api.dao.VirSchemaDAO;
 import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
-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.TaskUtilsFactory;
 import org.apache.syncope.core.provisioning.api.AuditManager;
-import org.apache.syncope.core.provisioning.api.cache.VirAttrCache;
-import org.apache.syncope.core.provisioning.api.cache.VirAttrCacheValue;
 import org.apache.syncope.core.provisioning.api.data.TaskDataBinder;
 import org.apache.syncope.core.provisioning.api.notification.NotificationManager;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationException;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
+import org.apache.syncope.core.provisioning.java.pushpull.OutboundMatcher;
 import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
 import org.apache.syncope.core.spring.ImplementationManager;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
@@ -132,9 +126,6 @@ public abstract class AbstractPropagationTaskExecutor implements PropagationTask
     @Autowired
     protected ExternalResourceDAO resourceDAO;
 
-    @Autowired
-    protected VirSchemaDAO virSchemaDAO;
-
     /**
      * Notification Manager.
      */
@@ -163,7 +154,7 @@ public abstract class AbstractPropagationTaskExecutor implements PropagationTask
     protected EntityFactory entityFactory;
 
     @Autowired
-    protected VirAttrCache virAttrCache;
+    protected OutboundMatcher outboundMatcher;
 
     protected List<PropagationActions> getPropagationActions(final ExternalResource resource) {
         List<PropagationActions> result = new ArrayList<>();
@@ -348,7 +339,7 @@ public abstract class AbstractPropagationTaskExecutor implements PropagationTask
 
     @Override
     public TaskExec execute(final PropagationTaskInfo taskInfo, final PropagationReporter reporter,
-                            final String executor) {
+            final String executor) {
         PropagationTask task;
         if (taskInfo.getKey() == null) {
             task = entityFactory.newEntity(PropagationTask.class);
@@ -626,53 +617,14 @@ public abstract class AbstractPropagationTaskExecutor implements PropagationTask
             final Provision provision,
             final boolean latest) {
 
-        String connObjectKey = latest || task.getOldConnObjectKey() == null
+        String connObjectKeyValue = latest || task.getOldConnObjectKey() == null
                 ? task.getConnObjectKey()
                 : task.getOldConnObjectKey();
 
-        boolean isLinkedAccount = task.getAnyTypeKind() == AnyTypeKind.USER
-                && userDAO.linkedAccountExists(task.getEntityKey(), connObjectKey);
-
-        Set<MappingItem> linkingMappingItems = isLinkedAccount
-                ? Set.of()
-                : virSchemaDAO.findByProvision(provision).stream().
-                        map(VirSchema::asLinkingMappingItem).collect(Collectors.toSet());
-
-        Optional<? extends MappingItem> connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision);
-        String connObjectKeyName = connObjectKeyItem.isPresent()
-                ? connObjectKeyItem.get().getExtAttrName()
-                : Name.NAME;
-
-        ConnectorObject obj = null;
-        try {
-            obj = connector.getObject(
-                    new ObjectClass(task.getObjectClassName()),
-                    AttributeBuilder.build(connObjectKeyName, connObjectKey),
-                    provision.isIgnoreCaseMatch(),
-                    MappingUtils.buildOperationOptions(new IteratorChain<>(
-                            MappingUtils.getPropagationItems(provision.getMapping().getItems()).iterator(),
-                            linkingMappingItems.iterator())));
-
-            for (MappingItem item : linkingMappingItems) {
-                Attribute attr = obj.getAttributeByName(item.getExtAttrName());
-                if (attr == null) {
-                    virAttrCache.expire(task.getAnyType(), task.getEntityKey(), item.getIntAttrName());
-                } else {
-                    virAttrCache.put(
-                            task.getAnyType(),
-                            task.getEntityKey(),
-                            item.getIntAttrName(),
-                            new VirAttrCacheValue(attr.getValue()));
-                }
-            }
-        } catch (TimeoutException toe) {
-            LOG.debug("Request timeout", toe);
-            throw toe;
-        } catch (RuntimeException ignore) {
-            LOG.debug("While resolving {}", connObjectKey, ignore);
-        }
+        List<ConnectorObject> matches = outboundMatcher.match(task, connector, provision, connObjectKeyValue);
+        LOG.debug("Found for propagation task {}: {}", task, matches);
 
-        return obj;
+        return matches.isEmpty() ? null : matches.get(0);
     }
 
     /**
@@ -698,11 +650,12 @@ public abstract class AbstractPropagationTaskExecutor implements PropagationTask
         Optional<? extends OrgUnitItem> connObjectKeyItem = orgUnit.getConnObjectKeyItem();
         if (connObjectKeyItem.isPresent()) {
             try {
-                obj = connector.getObject(new ObjectClass(task.getObjectClassName()),
+                obj = connector.getObject(
+                        new ObjectClass(task.getObjectClassName()),
                         AttributeBuilder.build(connObjectKeyItem.get().getExtAttrName(), connObjectKey),
                         orgUnit.isIgnoreCaseMatch(),
                         MappingUtils.buildOperationOptions(
-                                MappingUtils.getPropagationItems(orgUnit.getItems()).iterator()));
+                                MappingUtils.getPropagationItems(orgUnit.getItems().stream())));
             } catch (TimeoutException toe) {
                 LOG.debug("Request timeout", toe);
                 throw toe;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AzurePropagationActions.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AzurePropagationActions.java
index 24a2f50..b96cc3a 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AzurePropagationActions.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AzurePropagationActions.java
@@ -35,7 +35,7 @@ import org.springframework.transaction.annotation.Transactional;
  * This class is required during setup of an External Resource based on the ConnId
  * <a href="https://github.com/Tirasa/ConnIdAzureBundle">Azure connector</a>.
  *
- * It ensures to send the configured e-mail address as <pre>__NAME__</pre>.
+ * It ensures to send the configured e-mail address as {@code __NAME__}.
  */
 public class AzurePropagationActions implements PropagationActions {
 
@@ -70,7 +70,7 @@ public class AzurePropagationActions implements PropagationActions {
         Set<Attribute> attrs = new HashSet<>(task.getAttributes());
 
         if (AttributeUtil.find(getEmailAttrName(), attrs) == null) {
-            LOG.warn("Can't find {} attribute to set as __NAME__ attribute value, skipping...", getEmailAttrName());
+            LOG.warn("Can't find {} to set as {} attribute value, skipping...", getEmailAttrName(), Name.NAME);
             return;
         }
 
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationReporter.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationReporter.java
index 53b9fe8..5fbb4b0 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationReporter.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationReporter.java
@@ -59,11 +59,11 @@ public class DefaultPropagationReporter implements PropagationReporter {
         status.setFailureReason(failureReason);
 
         if (beforeObj != null) {
-            status.setBeforeObj(ConnObjectUtils.getConnObjectTO(beforeObj));
+            status.setBeforeObj(ConnObjectUtils.getConnObjectTO(beforeObj.getAttributes()));
         }
 
         if (afterObj != null) {
-            status.setAfterObj(ConnObjectUtils.getConnObjectTO(afterObj));
+            status.setAfterObj(ConnObjectUtils.getConnObjectTO(afterObj.getAttributes()));
         }
 
         add(status);
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/GoogleAppsPropagationActions.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/GoogleAppsPropagationActions.java
index 0f1ff74..a106842 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/GoogleAppsPropagationActions.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/GoogleAppsPropagationActions.java
@@ -36,7 +36,7 @@ import org.springframework.transaction.annotation.Transactional;
  * This class is required during setup of an External Resource based on the ConnId
  * <a href="https://github.com/Tirasa/ConnIdGoogleAppsBundle">GoogleApps connector</a>.
  *
- * It ensures to send the configured e-mail address as <pre>__NAME__</pre>.
+ * It ensures to send the configured e-mail address as {@code __NAME__}.
  */
 public class GoogleAppsPropagationActions implements PropagationActions {
 
@@ -59,7 +59,7 @@ public class GoogleAppsPropagationActions implements PropagationActions {
         Set<Attribute> attrs = new HashSet<>(task.getAttributes());
 
         if (AttributeUtil.find(getEmailAttrName(), attrs) == null) {
-            LOG.warn("Can't find {} attribute to set as __NAME__ attribute value, skipping...", getEmailAttrName());
+            LOG.warn("Can't find {} to set as {} attribute value, skipping...", getEmailAttrName(), Name.NAME);
             return;
         }
 
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 47957d6..1c38712 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
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.core.provisioning.java.propagation;
 
+import java.util.stream.Stream;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.Attr;
@@ -393,7 +394,7 @@ public class PropagationManagerImpl implements PropagationManager {
             final ResourceOperation operation,
             final Provision provision,
             final boolean deleteOnResource,
-            final List<? extends Item> mappingItems,
+            final Stream<? extends Item> mappingItems,
             final Pair<String, Set<Attribute>> preparedAttrs) {
 
         PropagationTaskInfo task = new PropagationTaskInfo();
@@ -411,7 +412,7 @@ public class PropagationManagerImpl implements PropagationManager {
         // if so, add special attributes that will be evaluated by PropagationTaskExecutor
         List<String> mandatoryMissing = new ArrayList<>();
         List<String> mandatoryNullOrEmpty = new ArrayList<>();
-        mappingItems.stream().filter(item -> (!item.isConnObjectKey()
+        mappingItems.filter(item -> (!item.isConnObjectKey()
                 && JexlUtils.evaluateMandatoryCondition(item.getMandatoryCondition(), any))).forEach(item -> {
 
             Attribute attr = AttributeUtil.find(item.getExtAttrName(), preparedAttrs.getRight());
@@ -507,16 +508,16 @@ public class PropagationManagerImpl implements PropagationManager {
             Provision provision = Optional.ofNullable(resource).
                     map(externalResource -> externalResource.getProvision(any.getType()).
                     orElse(null)).orElse(null);
-            List<? extends Item> mappingItems = provision == null
-                    ? List.of()
-                    : MappingUtils.getPropagationItems(provision.getMapping().getItems());
+            Stream<? extends Item> mappingItems = provision == null
+                    ? Stream.empty()
+                    : MappingUtils.getPropagationItems(provision.getMapping().getItems().stream());
 
             if (resource == null) {
                 LOG.error("Invalid resource name specified: {}, ignoring...", resourceKey);
             } else if (provision == null) {
                 LOG.error("No provision specified on resource {} for type {}, ignoring...",
                         resource, any.getType());
-            } else if (mappingItems.isEmpty()) {
+            } else if (provision.getMapping() == null || provision.getMapping().getItems().isEmpty()) {
                 LOG.warn("Requesting propagation for {} but no propagation mapping provided for {}",
                         any.getType(), resource);
             } else {
@@ -553,9 +554,9 @@ public class PropagationManagerImpl implements PropagationManager {
                 Provision provision = account == null || account.getResource() == null
                         ? null
                         : account.getResource().getProvision(AnyTypeKind.USER.name()).orElse(null);
-                List<? extends Item> mappingItems = provision == null
-                        ? List.of()
-                        : MappingUtils.getPropagationItems(provision.getMapping().getItems());
+                Stream<? extends Item> mappingItems = provision == null
+                        ? Stream.empty()
+                        : MappingUtils.getPropagationItems(provision.getMapping().getItems().stream());
 
                 if (account == null) {
                     LOG.error("Invalid operation {} on deleted account {} on resource {}, ignoring...",
@@ -565,7 +566,7 @@ public class PropagationManagerImpl implements PropagationManager {
                 } else if (provision == null) {
                     LOG.error("No provision specified on resource {} for type {}, ignoring...",
                             account.getResource(), AnyTypeKind.USER.name());
-                } else if (mappingItems.isEmpty()) {
+                } else if (provision.getMapping() == null || provision.getMapping().getItems().isEmpty()) {
                     LOG.warn("Requesting propagation for {} but no propagation mapping provided for {}",
                             AnyTypeKind.USER.name(), account.getResource());
                 } else {
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/ADMembershipPullActions.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/ADMembershipPullActions.java
index db1b6ac..7229f07 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/ADMembershipPullActions.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/ADMembershipPullActions.java
@@ -18,8 +18,6 @@
  */
 package org.apache.syncope.core.provisioning.java.pushpull;
 
-import java.util.Optional;
-import org.apache.syncope.common.lib.types.ConnConfProperty;
 import org.apache.syncope.core.provisioning.api.Connector;
 
 /**
@@ -38,14 +36,10 @@ public class ADMembershipPullActions extends LDAPMembershipPullActions {
      */
     @Override
     protected String getGroupMembershipAttrName(final Connector connector) {
-        Optional<ConnConfProperty> groupMembership = connector.getConnInstance().getConf().stream().
+        return connector.getConnInstance().getConf().stream().
                 filter(property -> "groupMemberReferenceAttribute".equals(property.getSchema().getName())
-                && !property.getValues().isEmpty()).
-                findFirst();
-
-        return groupMembership.isPresent()
-                ? (String) groupMembership.get().getValues().get(0)
-                : "member";
+                && !property.getValues().isEmpty()).findFirst().
+                map(groupMembership -> (String) groupMembership.getValues().get(0)).
+                orElse("member");
     }
-
 }
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 5097e9e..7afe3a5 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
@@ -30,8 +30,10 @@ import org.apache.syncope.common.lib.request.AnyCR;
 import org.apache.syncope.common.lib.request.AnyUR;
 import org.apache.syncope.common.lib.request.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.MatchType;
 import org.apache.syncope.common.lib.types.MatchingRule;
 import org.apache.syncope.common.lib.types.PatchOperation;
 import org.apache.syncope.core.provisioning.api.PropagationByResource;
@@ -44,7 +46,6 @@ import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationException;
 import org.apache.syncope.core.spring.security.DelegatedAdministrationException;
 import org.apache.syncope.core.persistence.api.dao.VirSchemaDAO;
-import org.apache.syncope.core.persistence.api.entity.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.resource.Provision;
@@ -69,17 +70,12 @@ import org.quartz.JobExecutionException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Transactional;
 
-import javax.annotation.Resource;
-
 @Transactional(rollbackFor = Throwable.class)
 public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHandler<PullTask, PullActions>
         implements SyncopePullResultHandler {
 
-    @Resource(name = "adminUser")
-    protected String adminUser;
-    
     @Autowired
-    protected PullUtils pullUtils;
+    protected InboundMatcher inboundMatcher;
 
     @Autowired
     protected NotificationManager notificationManager;
@@ -130,8 +126,8 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
         try {
             provision = profile.getTask().getResource().getProvision(delta.getObject().getObjectClass()).
                     orElseThrow(() -> new JobExecutionException(
-                    "No provision found on " + profile.getTask().getResource() + " for "
-                    + delta.getObject().getObjectClass()));
+                    "No provision found on " + profile.getTask().getResource()
+                    + " for " + delta.getObject().getObjectClass()));
 
             doHandle(delta, provision);
             executor.reportHandled(delta.getObjectClass(), delta.getObject().getName());
@@ -188,16 +184,15 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
     protected List<ProvisioningReport> provision(
             final UnmatchingRule rule,
             final SyncDelta delta,
-            final Provision provision,
-            final AnyUtils anyUtils) throws JobExecutionException {
+            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 List.of();
+            end(provision.getAnyType().getKind(), UnmatchingRule.toEventName(rule), Result.SUCCESS, null, null, delta);
+            return Collections.<ProvisioningReport>emptyList();
         }
 
-        AnyCR anyCR = connObjectUtils.getAnyCR(delta.getObject(), profile.getTask(), provision, anyUtils, true);
+        AnyCR anyCR = connObjectUtils.getAnyCR(delta.getObject(), profile.getTask(), provision, true);
         if (rule == UnmatchingRule.ASSIGN) {
             anyCR.getResources().add(profile.getTask().getResource().getKey());
         }
@@ -211,7 +206,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
 
         if (profile.isDryRun()) {
             result.setKey(null);
-            finalize(UnmatchingRule.toEventName(rule), Result.SUCCESS, null, null, delta);
+            end(provision.getAnyType().getKind(), UnmatchingRule.toEventName(rule), Result.SUCCESS, null, null, delta);
         } else {
             for (PullActions action : profile.getActions()) {
                 if (rule == UnmatchingRule.ASSIGN) {
@@ -266,7 +261,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                 }
             }
 
-            finalize(UnmatchingRule.toEventName(rule), resultStatus, null, output, delta);
+            end(provision.getAnyType().getKind(), UnmatchingRule.toEventName(rule), resultStatus, null, output, delta);
         }
 
         return Collections.singletonList(result);
@@ -297,7 +292,8 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
 
         if (!profile.getTask().isPerformUpdate()) {
             LOG.debug("PullTask not configured for update");
-            finalize(MatchingRule.toEventName(MatchingRule.UPDATE), Result.SUCCESS, null, null, delta);
+            end(provision.getAnyType().getKind(),
+                    MatchingRule.toEventName(MatchingRule.UPDATE), Result.SUCCESS, null, null, delta);
             return Collections.<ProvisioningReport>emptyList();
         }
 
@@ -312,9 +308,9 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
             result.setOperation(ResourceOperation.UPDATE);
             result.setAnyType(provision.getAnyType().getKey());
             result.setStatus(ProvisioningReport.Status.SUCCESS);
-            result.setKey(match.getMatchingKey());
+            result.setKey(match.getAny().getKey());
 
-            AnyTO before = getAnyTO(match.getMatchingKey());
+            AnyTO before = getAnyTO(match.getAny());
             if (before == null) {
                 result.setStatus(ProvisioningReport.Status.FAILURE);
                 result.setMessage(String.format("Any '%s(%s)' not found", provision.getAnyType().getKey(), match));
@@ -338,8 +334,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                                 delta.getObject(),
                                 before,
                                 profile.getTask(),
-                                provision,
-                                getAnyUtils());
+                                provision);
 
                         for (PullActions action : profile.getActions()) {
                             action.beforeUpdate(profile, delta, before, anyUR);
@@ -388,7 +383,8 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                         }
                     }
                 }
-                finalize(MatchingRule.toEventName(MatchingRule.UPDATE),
+                end(provision.getAnyType().getKind(),
+                        MatchingRule.toEventName(MatchingRule.UPDATE),
                         resultStatus, before, output, delta, effectiveReq);
             }
             results.add(result);
@@ -405,7 +401,8 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
 
         if (!profile.getTask().isPerformUpdate()) {
             LOG.debug("PullTask not configured for update");
-            finalize(MatchingRule.toEventName(matchingRule), Result.SUCCESS, null, null, delta);
+            end(provision.getAnyType().getKind(),
+                    MatchingRule.toEventName(matchingRule), Result.SUCCESS, null, null, delta);
             return Collections.<ProvisioningReport>emptyList();
         }
 
@@ -420,9 +417,9 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
             result.setOperation(ResourceOperation.DELETE);
             result.setAnyType(provision.getAnyType().getKey());
             result.setStatus(ProvisioningReport.Status.SUCCESS);
-            result.setKey(match.getMatchingKey());
+            result.setKey(match.getAny().getKey());
 
-            AnyTO before = getAnyTO(match.getMatchingKey());
+            AnyTO before = getAnyTO(match.getAny());
 
             if (before == null) {
                 result.setStatus(ProvisioningReport.Status.FAILURE);
@@ -455,22 +452,22 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
 
                         taskExecutor.execute(propagationManager.getDeleteTasks(
                                 provision.getAnyType().getKind(),
-                                match.getMatchingKey(),
+                                match.getAny().getKey(),
                                 propByRes,
                                 null,
                                 null),
                                 false,
-                                this.adminUser);
+                                adminUser);
 
                         AnyUR anyUR = null;
                         if (matchingRule == MatchingRule.UNASSIGN) {
-                            anyUR = getAnyUtils().newAnyUR(match.getMatchingKey());
+                            anyUR = getAnyUtils().newAnyUR(match.getAny().getKey());
                             anyUR.getResources().add(new StringPatchItem.Builder().
                                     operation(PatchOperation.DELETE).
                                     value(profile.getTask().getResource().getKey()).build());
                         }
                         if (anyUR == null) {
-                            output = getAnyTO(match.getMatchingKey());
+                            output = getAnyTO(match.getAny());
                         } else {
                             output = doUpdate(before, anyUR, delta, result);
                         }
@@ -500,7 +497,8 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                         resultStatus = Result.FAILURE;
                     }
                 }
-                finalize(MatchingRule.toEventName(matchingRule), resultStatus, before, output, delta);
+                end(provision.getAnyType().getKind(),
+                        MatchingRule.toEventName(matchingRule), resultStatus, before, output, delta);
             }
             results.add(result);
         }
@@ -517,9 +515,11 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
 
         if (!profile.getTask().isPerformUpdate()) {
             LOG.debug("PullTask not configured for update");
-            finalize(unlink
-                    ? MatchingRule.toEventName(MatchingRule.UNLINK)
-                    : MatchingRule.toEventName(MatchingRule.LINK), Result.SUCCESS, null, null, delta);
+            end(provision.getAnyType().getKind(),
+                    unlink
+                            ? MatchingRule.toEventName(MatchingRule.UNLINK)
+                            : MatchingRule.toEventName(MatchingRule.LINK),
+                    Result.SUCCESS, null, null, delta);
             return Collections.<ProvisioningReport>emptyList();
         }
 
@@ -534,9 +534,9 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
             result.setOperation(ResourceOperation.NONE);
             result.setAnyType(provision.getAnyType().getKey());
             result.setStatus(ProvisioningReport.Status.SUCCESS);
-            result.setKey(match.getMatchingKey());
+            result.setKey(match.getAny().getKey());
 
-            AnyTO before = getAnyTO(match.getMatchingKey());
+            AnyTO before = getAnyTO(match.getAny());
 
             if (before == null) {
                 result.setStatus(ProvisioningReport.Status.FAILURE);
@@ -598,9 +598,10 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                         resultStatus = Result.FAILURE;
                     }
                 }
-                finalize(unlink
-                        ? MatchingRule.toEventName(MatchingRule.UNLINK)
-                        : MatchingRule.toEventName(MatchingRule.LINK),
+                end(provision.getAnyType().getKind(),
+                        unlink
+                                ? MatchingRule.toEventName(MatchingRule.UNLINK)
+                                : MatchingRule.toEventName(MatchingRule.LINK),
                         resultStatus, before, output, delta, effectiveReq);
             }
             results.add(result);
@@ -617,7 +618,8 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
 
         if (!profile.getTask().isPerformDelete()) {
             LOG.debug("PullTask not configured for delete");
-            finalize(ResourceOperation.DELETE.name().toLowerCase(), Result.SUCCESS, null, null, delta);
+            end(provision.getAnyType().getKind(),
+                    ResourceOperation.DELETE.name().toLowerCase(), Result.SUCCESS, null, null, delta);
             return Collections.<ProvisioningReport>emptyList();
         }
 
@@ -632,9 +634,9 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
             ProvisioningReport result = new ProvisioningReport();
 
             try {
-                AnyTO before = getAnyTO(match.getMatchingKey());
+                AnyTO before = getAnyTO(match.getAny());
 
-                result.setKey(match.getMatchingKey());
+                result.setKey(match.getAny().getKey());
                 result.setName(getName(before));
                 result.setOperation(ResourceOperation.DELETE);
                 result.setAnyType(provision.getAnyType().getKey());
@@ -647,7 +649,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
 
                     try {
                         getProvisioningManager().delete(
-                                match.getMatchingKey(),
+                                match.getAny().getKey(),
                                 Collections.singleton(profile.getTask().getResource().getKey()),
                                 true);
                         output = null;
@@ -668,7 +670,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                             Remediation entity = entityFactory.newEntity(Remediation.class);
                             entity.setAnyType(provision.getAnyType());
                             entity.setOperation(ResourceOperation.DELETE);
-                            entity.setPayload(match.getMatchingKey());
+                            entity.setPayload(match.getAny().getKey());
                             entity.setError(result.getMessage());
                             entity.setInstant(new Date());
                             entity.setRemoteName(delta.getObject().getName().getNameValue());
@@ -678,7 +680,8 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                         }
                     }
 
-                    finalize(ResourceOperation.DELETE.name().toLowerCase(), resultStatus, before, output, delta);
+                    end(provision.getAnyType().getKind(),
+                            ResourceOperation.DELETE.name().toLowerCase(), resultStatus, before, output, delta);
                 }
 
                 results.add(result);
@@ -721,7 +724,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
         } else {
             matches.forEach(match -> {
                 ProvisioningReport report = new ProvisioningReport();
-                report.setKey(match.getMatchingKey());
+                report.setKey(match.getAny().getKey());
                 report.setName(delta.getObject().getUid().getUidValue());
                 report.setOperation(ResourceOperation.NONE);
                 report.setAnyType(provision.getAnyType().getKey());
@@ -734,9 +737,10 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
             });
         }
 
-        finalize(matching
-                ? MatchingRule.toEventName(MatchingRule.IGNORE)
-                : UnmatchingRule.toEventName(UnmatchingRule.IGNORE), Result.SUCCESS, null, null, delta);
+        end(provision.getAnyType().getKind(),
+                matching
+                        ? MatchingRule.toEventName(MatchingRule.IGNORE)
+                        : UnmatchingRule.toEventName(UnmatchingRule.IGNORE), Result.SUCCESS, null, null, delta);
 
         return results;
     }
@@ -744,8 +748,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
     protected void handleAnys(
             final SyncDelta delta,
             final List<PullMatch> matches,
-            final Provision provision,
-            final AnyUtils anyUtils) throws JobExecutionException {
+            final Provision provision) throws JobExecutionException {
 
         if (matches.isEmpty()) {
             LOG.debug("Nothing to do");
@@ -753,12 +756,12 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
         }
 
         if (SyncDeltaType.CREATE_OR_UPDATE == delta.getDeltaType()) {
-            if (matches.get(0).getMatchingKey() == null) {
+            if (matches.get(0).getAny() == null) {
                 switch (profile.getTask().getUnmatchingRule()) {
                     case ASSIGN:
                     case PROVISION:
                         profile.getResults().addAll(
-                                provision(profile.getTask().getUnmatchingRule(), delta, provision, anyUtils));
+                                provision(profile.getTask().getUnmatchingRule(), delta, provision));
                         break;
 
                     case IGNORE:
@@ -776,12 +779,12 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                         if (attr == null) {
                             virAttrCache.expire(
                                     provision.getAnyType().getKey(),
-                                    match.getMatchingKey(),
+                                    match.getAny().getKey(),
                                     virSchema.getKey());
                         } else {
                             virAttrCache.put(
                                     provision.getAnyType().getKey(),
-                                    match.getMatchingKey(),
+                                    match.getAny().getKey(),
                                     virSchema.getKey(),
                                     new VirAttrCacheValue(attr.getValue()));
                         }
@@ -823,8 +826,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
     protected void handleLinkedAccounts(
             final SyncDelta delta,
             final List<PullMatch> matches,
-            final Provision provision,
-            final AnyUtils anyUtils) throws JobExecutionException {
+            final Provision provision) throws JobExecutionException {
 
         if (matches.isEmpty()) {
             LOG.debug("Nothing to do");
@@ -832,7 +834,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
         }
 
         // nothing to do in the general case
-        LOG.warn("Unexpected linked accounts found for {}: {}", anyUtils.anyTypeKind(), matches);
+        LOG.warn("Unexpected linked accounts found for {}: {}", provision.getAnyType().getKind(), matches);
     }
 
     /**
@@ -843,8 +845,6 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
      * @throws JobExecutionException in case of pull failure.
      */
     protected void doHandle(final SyncDelta delta, final Provision provision) throws JobExecutionException {
-        AnyUtils anyUtils = getAnyUtils();
-
         LOG.debug("Process {} for {} as {}",
                 delta.getDeltaType(), delta.getUid().getUidValue(), delta.getObject().getObjectClass());
 
@@ -857,7 +857,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                 finalDelta.getDeltaType(), finalDelta.getUid().getUidValue(), finalDelta.getObject().getObjectClass());
 
         try {
-            List<PullMatch> matches = pullUtils.match(finalDelta, provision, anyUtils);
+            List<PullMatch> matches = inboundMatcher.match(finalDelta, provision);
             LOG.debug("Match(es) found for {} as {}: {}",
                     finalDelta.getUid().getUidValue(), finalDelta.getObject().getObjectClass(), matches);
 
@@ -884,23 +884,22 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
             handleAnys(
                     finalDelta,
                     matches.stream().
-                            filter(match -> match.getMatchTarget() == PullMatch.MatchTarget.ANY).
-                            collect(Collectors.toList()), provision,
-                    anyUtils);
+                            filter(match -> match.getMatchTarget() == MatchType.ANY).
+                            collect(Collectors.toList()), provision);
 
             // linked accounts
             handleLinkedAccounts(
                     finalDelta,
                     matches.stream().
-                            filter(match -> match.getMatchTarget() == PullMatch.MatchTarget.LINKED_ACCOUNT).
-                            collect(Collectors.toList()), provision,
-                    anyUtils);
+                            filter(match -> match.getMatchTarget() == MatchType.LINKED_ACCOUNT).
+                            collect(Collectors.toList()), provision);
         } catch (IllegalStateException | IllegalArgumentException e) {
             LOG.warn(e.getMessage());
         }
     }
 
-    protected void finalize(
+    protected void end(
+            final AnyTypeKind anyTypeKind,
             final String event,
             final Result result,
             final Object before,
@@ -912,12 +911,10 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
             this.latestResult = result;
         }
 
-        AnyUtils anyUtils = getAnyUtils();
-
         notificationManager.createTasks(
                 AuthContextUtils.getUsername(),
                 AuditElements.EventCategoryType.PULL,
-                anyUtils.anyTypeKind().name().toLowerCase(),
+                anyTypeKind.name().toLowerCase(),
                 profile.getTask().getResource().getKey(),
                 event,
                 result,
@@ -929,7 +926,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
         auditManager.audit(
                 AuthContextUtils.getUsername(),
                 AuditElements.EventCategoryType.PULL,
-                anyUtils.anyTypeKind().name().toLowerCase(),
+                anyTypeKind.name().toLowerCase(),
                 profile.getTask().getResource().getKey(),
                 event,
                 result,
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractPushResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractPushResultHandler.java
index 73ced5d..f8a4e1b 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractPushResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractPushResultHandler.java
@@ -62,16 +62,11 @@ import org.springframework.scheduling.quartz.SchedulerFactoryBean;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 
-import javax.annotation.Resource;
-
 public abstract class AbstractPushResultHandler extends AbstractSyncopeResultHandler<PushTask, PushActions>
         implements SyncopePushResultHandler {
 
     @Autowired
-    protected PushUtils pushUtils;
-
-    @Resource(name = "adminUser")
-    protected String adminUser;
+    protected OutboundMatcher outboundMatcher;
 
     /**
      * Notification Manager.
@@ -129,13 +124,13 @@ public abstract class AbstractPushResultHandler extends AbstractSyncopeResultHan
         if (!taskInfos.isEmpty()) {
             taskInfos.get(0).setBeforeObj(Optional.of(beforeObj));
             PropagationReporter reporter = new DefaultPropagationReporter();
-            taskExecutor.execute(taskInfos.get(0), reporter, this.adminUser);
+            taskExecutor.execute(taskInfos.get(0), reporter, adminUser);
             reportPropagation(result, reporter);
         }
     }
 
     protected void deprovision(final Any<?> any, final ConnectorObject beforeObj, final ProvisioningReport result) {
-        AnyTO before = getAnyTO(any.getKey());
+        AnyTO before = getAnyTO(any);
 
         List<String> noPropResources = new ArrayList<>(before.getResources());
         noPropResources.remove(profile.getTask().getResource().getKey());
@@ -153,13 +148,13 @@ public abstract class AbstractPushResultHandler extends AbstractSyncopeResultHan
         if (!taskInfos.isEmpty()) {
             taskInfos.get(0).setBeforeObj(Optional.of(beforeObj));
             PropagationReporter reporter = new DefaultPropagationReporter();
-            taskExecutor.execute(taskInfos.get(0), reporter, this.adminUser);
+            taskExecutor.execute(taskInfos.get(0), reporter, adminUser);
             reportPropagation(result, reporter);
         }
     }
 
     protected void provision(final Any<?> any, final Boolean enable, final ProvisioningReport result) {
-        AnyTO before = getAnyTO(any.getKey());
+        AnyTO before = getAnyTO(any);
 
         List<String> noPropResources = new ArrayList<>(before.getResources());
         noPropResources.remove(profile.getTask().getResource().getKey());
@@ -177,7 +172,7 @@ public abstract class AbstractPushResultHandler extends AbstractSyncopeResultHan
         if (!taskInfos.isEmpty()) {
             taskInfos.get(0).setBeforeObj(Optional.ofNullable(null));
             PropagationReporter reporter = new DefaultPropagationReporter();
-            taskExecutor.execute(taskInfos.get(0), reporter, this.adminUser);
+            taskExecutor.execute(taskInfos.get(0), reporter, adminUser);
             reportPropagation(result, reporter);
         }
     }
@@ -263,11 +258,11 @@ public abstract class AbstractPushResultHandler extends AbstractSyncopeResultHan
         result.setAnyType(any.getType().getKey());
         result.setName(getName(any));
 
-        LOG.debug("Propagating {} with key {} towards {}",
+        LOG.debug("Pushing {} with key {} towards {}",
                 any.getType().getKind(), any.getKey(), profile.getTask().getResource());
 
         // Try to read remote object BEFORE any actual operation
-        List<ConnectorObject> connObjs = pushUtils.match(profile.getConnector(), any, provision);
+        List<ConnectorObject> connObjs = outboundMatcher.match(profile.getConnector(), any, provision);
         LOG.debug("Match(es) found for {} as {}: {}", any, provision.getObjectClass(), connObjs);
 
         if (connObjs.size() > 1) {
@@ -460,7 +455,7 @@ public abstract class AbstractPushResultHandler extends AbstractSyncopeResultHan
 
                 if (notificationsAvailable || auditRequested) {
                     resultStatus = AuditElements.Result.SUCCESS;
-                    output = pushUtils.findByConnObjectKey(profile.getConnector(), any, provision);
+                    output = outboundMatcher.match(profile.getConnector(), any, provision);
                 }
             } catch (IgnoreProvisionException e) {
                 throw e;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractRealmResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractRealmResultHandler.java
index e914ce3..35a1180 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractRealmResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractRealmResultHandler.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.core.provisioning.java.pushpull;
 
+import javax.annotation.Resource;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.entity.task.ProvisioningTask;
 import org.apache.syncope.core.provisioning.api.AuditManager;
@@ -69,6 +70,9 @@ public abstract class AbstractRealmResultHandler<T extends ProvisioningTask, A e
     @Autowired
     protected PropagationTaskExecutor taskExecutor;
 
+    @Resource(name = "adminUser")
+    protected String adminUser;
+
     /**
      * Provisioning profile.
      */
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractSyncopeResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractSyncopeResultHandler.java
index b8b0df9..3b5e8f2 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractSyncopeResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractSyncopeResultHandler.java
@@ -18,8 +18,12 @@
  */
 package org.apache.syncope.core.provisioning.java.pushpull;
 
+import javax.annotation.Resource;
 import org.apache.syncope.common.lib.request.AnyUR;
 import org.apache.syncope.common.lib.to.AnyTO;
+import org.apache.syncope.core.persistence.api.entity.Any;
+import org.apache.syncope.core.persistence.api.entity.AnyUtils;
+import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.task.ProvisioningTask;
 import org.apache.syncope.core.provisioning.api.data.GroupDataBinder;
 import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
@@ -27,8 +31,6 @@ import org.apache.syncope.core.provisioning.api.propagation.PropagationManager;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskExecutor;
 import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningProfile;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopeResultHandler;
-import org.apache.syncope.core.persistence.api.entity.AnyUtils;
-import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.provisioning.api.WorkflowResult;
 import org.apache.syncope.core.provisioning.api.data.AnyObjectDataBinder;
 import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningActions;
@@ -83,6 +85,9 @@ public abstract class AbstractSyncopeResultHandler<T extends ProvisioningTask, A
     @Autowired
     protected AnyUtilsFactory anyUtilsFactory;
 
+    @Resource(name = "adminUser")
+    protected String adminUser;
+
     /**
      * Provisioning profile.
      */
@@ -90,7 +95,7 @@ public abstract class AbstractSyncopeResultHandler<T extends ProvisioningTask, A
 
     protected abstract AnyUtils getAnyUtils();
 
-    protected abstract AnyTO getAnyTO(String key);
+    protected abstract AnyTO getAnyTO(Any<?> any);
 
     protected abstract WorkflowResult<? extends AnyUR> update(AnyUR req);
 
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultAnyObjectPullResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultAnyObjectPullResultHandler.java
index 6e728ae..a87fff4 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultAnyObjectPullResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultAnyObjectPullResultHandler.java
@@ -31,7 +31,9 @@ import org.apache.syncope.common.lib.to.AnyTO;
 import org.apache.syncope.common.lib.to.PropagationStatus;
 import org.apache.syncope.common.lib.to.AnyObjectTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.AnyUtils;
+import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
 import org.apache.syncope.core.provisioning.api.AnyObjectProvisioningManager;
 import org.apache.syncope.core.provisioning.api.ProvisioningManager;
 import org.apache.syncope.core.provisioning.api.WorkflowResult;
@@ -66,8 +68,8 @@ public class DefaultAnyObjectPullResultHandler extends AbstractPullResultHandler
     }
 
     @Override
-    protected AnyTO getAnyTO(final String key) {
-        return anyObjectDataBinder.getAnyObjectTO(key);
+    protected AnyTO getAnyTO(final Any<?> any) {
+        return anyObjectDataBinder.getAnyObjectTO((AnyObject) any, true);
     }
 
     @Override
@@ -82,7 +84,7 @@ public class DefaultAnyObjectPullResultHandler extends AbstractPullResultHandler
         Map.Entry<String, List<PropagationStatus>> created = anyObjectProvisioningManager.create(
                 anyObjectCR, Set.of(profile.getTask().getResource().getKey()), true);
 
-        return getAnyTO(created.getKey());
+        return anyObjectDataBinder.getAnyObjectTO(created.getKey());
     }
 
     @Override
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultAnyObjectPushResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultAnyObjectPushResultHandler.java
index 98e4d6c..dc7ab63 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultAnyObjectPushResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultAnyObjectPushResultHandler.java
@@ -41,8 +41,8 @@ public class DefaultAnyObjectPushResultHandler extends AbstractPushResultHandler
     }
 
     @Override
-    protected AnyTO getAnyTO(final String key) {
-        return anyObjectDataBinder.getAnyObjectTO(key);
+    protected AnyTO getAnyTO(final Any<?> any) {
+        return anyObjectDataBinder.getAnyObjectTO((AnyObject) any, true);
     }
 
     @Override
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultGroupPullResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultGroupPullResultHandler.java
index 77a2363..96c765a 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultGroupPullResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultGroupPullResultHandler.java
@@ -34,7 +34,9 @@ import org.apache.syncope.common.lib.to.PropagationStatus;
 import org.apache.syncope.common.lib.to.GroupTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.PatchOperation;
+import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.AnyUtils;
+import org.apache.syncope.core.persistence.api.entity.group.Group;
 import org.apache.syncope.core.provisioning.api.GroupProvisioningManager;
 import org.apache.syncope.core.provisioning.api.ProvisioningManager;
 import org.apache.syncope.core.provisioning.api.WorkflowResult;
@@ -76,8 +78,8 @@ public class DefaultGroupPullResultHandler extends AbstractPullResultHandler imp
     }
 
     @Override
-    protected AnyTO getAnyTO(final String key) {
-        return groupDataBinder.getGroupTO(key);
+    protected AnyTO getAnyTO(final Any<?> any) {
+        return groupDataBinder.getGroupTO((Group) any, true);
     }
 
     @Override
@@ -95,7 +97,7 @@ public class DefaultGroupPullResultHandler extends AbstractPullResultHandler imp
                 Set.of(profile.getTask().getResource().getKey()),
                 true);
 
-        return getAnyTO(created.getKey());
+        return groupDataBinder.getGroupTO(created.getKey());
     }
 
     @Override
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultGroupPushResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultGroupPushResultHandler.java
index 39da94a..714711c 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultGroupPushResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultGroupPushResultHandler.java
@@ -41,8 +41,8 @@ public class DefaultGroupPushResultHandler extends AbstractPushResultHandler imp
     }
 
     @Override
-    protected AnyTO getAnyTO(final String key) {
-        return groupDataBinder.getGroupTO(key);
+    protected AnyTO getAnyTO(final Any<?> any) {
+        return groupDataBinder.getGroupTO((Group) any, true);
     }
 
     @Override
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 af39823..d275ea2 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
@@ -57,18 +57,13 @@ import org.quartz.JobExecutionException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Transactional;
 
-import javax.annotation.Resource;
-
 @Transactional(rollbackFor = Throwable.class)
 public class DefaultRealmPullResultHandler
         extends AbstractRealmResultHandler<PullTask, PullActions>
         implements RealmPullResultHandler {
 
-    @Resource(name = "adminUser")
-    protected String adminUser;
-
     @Autowired
-    private PullUtils pullUtils;
+    private InboundMatcher inboundMatcher;
 
     @Autowired
     private ConnObjectUtils connObjectUtils;
@@ -244,7 +239,7 @@ public class DefaultRealmPullResultHandler
             propByRes.addAll(ResourceOperation.CREATE, realm.getResourceKeys());
             if (unmatchingRule == UnmatchingRule.ASSIGN) {
                 List<PropagationTaskInfo> taskInfos = propagationManager.createTasks(realm, propByRes, null);
-                taskExecutor.execute(taskInfos, false, this.adminUser);
+                taskExecutor.execute(taskInfos, false, adminUser);
             }
 
             RealmTO actual = binder.getRealmTO(realm, true);
@@ -279,7 +274,7 @@ public class DefaultRealmPullResultHandler
         finalize(UnmatchingRule.toEventName(unmatchingRule), resultStatus, null, output, delta);
     }
 
-    private List<ProvisioningReport> update(final SyncDelta delta, final List<String> keys, final boolean inLink)
+    private List<ProvisioningReport> update(final SyncDelta delta, final List<Realm> realms, final boolean inLink)
             throws JobExecutionException {
 
         if (!profile.getTask().isPerformUpdate()) {
@@ -288,84 +283,74 @@ public class DefaultRealmPullResultHandler
             return List.of();
         }
 
-        LOG.debug("About to update {}", keys);
+        LOG.debug("About to update {}", realms);
 
         List<ProvisioningReport> results = new ArrayList<>();
 
-        for (String key : keys) {
-            LOG.debug("About to update {}", key);
+        for (Realm realm : realms) {
+            LOG.debug("About to update {}", realm);
 
             ProvisioningReport result = new ProvisioningReport();
             result.setOperation(ResourceOperation.UPDATE);
             result.setAnyType(REALM_TYPE);
             result.setStatus(ProvisioningReport.Status.SUCCESS);
-            result.setKey(key);
-
-            Realm realm = realmDAO.find(key);
-            RealmTO before = binder.getRealmTO(realm, true);
-            if (before == null) {
-                result.setStatus(ProvisioningReport.Status.FAILURE);
-                result.setMessage(String.format("Realm '%s' not found", key));
-            } else {
-                result.setName(before.getFullPath());
-            }
+            result.setKey(realm.getKey());
+            result.setName(realm.getFullPath());
 
             if (!profile.isDryRun()) {
                 Result resultStatus;
                 Object output;
 
-                if (before == null) {
-                    resultStatus = Result.FAILURE;
-                    output = null;
-                } else {
-                    try {
-                        if (!inLink) {
-                            for (PullActions action : profile.getActions()) {
-                                action.beforeUpdate(profile, delta, before, null);
-                            }
+                RealmTO before = binder.getRealmTO(realm, true);
+                try {
+                    if (!inLink) {
+                        for (PullActions action : profile.getActions()) {
+                            action.beforeUpdate(profile, delta, before, null);
                         }
+                    }
 
-                        PropagationByResource<String> propByRes = binder.update(realm, before);
-                        realm = realmDAO.save(realm);
-                        RealmTO updated = binder.getRealmTO(realm, true);
+                    PropagationByResource<String> propByRes = binder.update(realm, before);
+                    realm = realmDAO.save(realm);
+                    RealmTO updated = binder.getRealmTO(realm, true);
 
-                        List<PropagationTaskInfo> taskInfos = propagationManager.createTasks(realm, propByRes, null);
-                        taskExecutor.execute(taskInfos, false, this.adminUser);
+                    List<PropagationTaskInfo> taskInfos = propagationManager.createTasks(realm, propByRes, null);
+                    taskExecutor.execute(taskInfos, false, adminUser);
 
-                        for (PullActions action : profile.getActions()) {
-                            action.after(profile, delta, updated, result);
-                        }
+                    for (PullActions action : profile.getActions()) {
+                        action.after(profile, delta, updated, result);
+                    }
 
-                        output = updated;
-                        resultStatus = Result.SUCCESS;
-                        result.setName(updated.getFullPath());
+                    output = updated;
+                    resultStatus = Result.SUCCESS;
+                    result.setName(updated.getFullPath());
 
-                        LOG.debug("{} successfully updated", updated);
-                    } 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 Realm {}", delta.getUid().getUidValue(), e);
-                        output = e;
-                        resultStatus = Result.FAILURE;
-                    } catch (Exception e) {
-                        throwIgnoreProvisionException(delta, e);
+                    LOG.debug("{} successfully updated", updated);
+                } 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 Realm {}", 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 update Realm {}", delta.getUid().getUidValue(), e);
-                        output = e;
-                        resultStatus = Result.FAILURE;
-                    }
+                    result.setStatus(ProvisioningReport.Status.FAILURE);
+                    result.setMessage(ExceptionUtils.getRootCauseMessage(e));
+                    LOG.error("Could not update Realm {}", delta.getUid().getUidValue(), e);
+                    output = e;
+                    resultStatus = Result.FAILURE;
                 }
+
                 finalize(MatchingRule.toEventName(MatchingRule.UPDATE), resultStatus, before, output, delta);
             }
+
             results.add(result);
         }
 
         return results;
     }
 
-    private List<ProvisioningReport> deprovision(final SyncDelta delta, final List<String> keys, final boolean unlink)
+    private List<ProvisioningReport> deprovision(final SyncDelta delta, final List<Realm> realms, final boolean unlink)
             throws JobExecutionException {
 
         if (!profile.getTask().isPerformUpdate()) {
@@ -376,95 +361,84 @@ public class DefaultRealmPullResultHandler
             return List.of();
         }
 
-        LOG.debug("About to deprovision {}", keys);
+        LOG.debug("About to deprovision {}", realms);
 
         final List<ProvisioningReport> results = new ArrayList<>();
 
-        for (String key : keys) {
-            LOG.debug("About to unassign resource {}", key);
+        for (Realm realm : realms) {
+            LOG.debug("About to unassign resource {}", realm);
 
             ProvisioningReport result = new ProvisioningReport();
             result.setOperation(ResourceOperation.DELETE);
             result.setAnyType(REALM_TYPE);
             result.setStatus(ProvisioningReport.Status.SUCCESS);
-            result.setKey(key);
-
-            Realm realm = realmDAO.find(key);
-            RealmTO before = binder.getRealmTO(realm, true);
-            if (before == null) {
-                result.setStatus(ProvisioningReport.Status.FAILURE);
-                result.setMessage(String.format("Realm '%s' not found", key));
-            } else {
-                result.setName(before.getFullPath());
-            }
+            result.setKey(realm.getKey());
+            result.setName(realm.getFullPath());
 
             if (!profile.isDryRun()) {
                 Object output;
                 Result resultStatus;
 
-                if (before == null) {
-                    resultStatus = Result.FAILURE;
-                    output = null;
-                } else {
-                    try {
-                        if (unlink) {
-                            for (PullActions action : profile.getActions()) {
-                                action.beforeUnassign(profile, delta, before);
-                            }
-                        } else {
-                            for (PullActions action : profile.getActions()) {
-                                action.beforeDeprovision(profile, delta, before);
-                            }
-                        }
-
-                        PropagationByResource<String> propByRes = new PropagationByResource<>();
-                        propByRes.add(ResourceOperation.DELETE, profile.getTask().getResource().getKey());
-                        taskExecutor.execute(propagationManager.createTasks(realm, propByRes, null),
-                            false, this.adminUser);
-
-                        RealmTO realmTO;
-                        if (unlink) {
-                            realm.getResources().remove(profile.getTask().getResource());
-                            realmTO = binder.getRealmTO(realmDAO.save(realm), true);
-                        } else {
-                            realmTO = binder.getRealmTO(realm, true);
+                RealmTO before = binder.getRealmTO(realm, true);
+                try {
+                    if (unlink) {
+                        for (PullActions action : profile.getActions()) {
+                            action.beforeUnassign(profile, delta, before);
                         }
-                        output = realmTO;
-
+                    } else {
                         for (PullActions action : profile.getActions()) {
-                            action.after(profile, delta, realmTO, result);
+                            action.beforeDeprovision(profile, delta, before);
                         }
+                    }
 
-                        resultStatus = Result.SUCCESS;
+                    PropagationByResource<String> propByRes = new PropagationByResource<>();
+                    propByRes.add(ResourceOperation.DELETE, profile.getTask().getResource().getKey());
+                    taskExecutor.execute(propagationManager.createTasks(realm, propByRes, null), false, adminUser);
 
-                        LOG.debug("{} successfully updated", realm);
-                    } 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 Realm {}", delta.getUid().getUidValue(), e);
-                        output = e;
-                        resultStatus = Result.FAILURE;
-                    } catch (Exception e) {
-                        throwIgnoreProvisionException(delta, e);
+                    RealmTO realmTO;
+                    if (unlink) {
+                        realm.getResources().remove(profile.getTask().getResource());
+                        realmTO = binder.getRealmTO(realmDAO.save(realm), true);
+                    } else {
+                        realmTO = binder.getRealmTO(realm, true);
+                    }
+                    output = realmTO;
 
-                        result.setStatus(ProvisioningReport.Status.FAILURE);
-                        result.setMessage(ExceptionUtils.getRootCauseMessage(e));
-                        LOG.error("Could not update Realm {}", delta.getUid().getUidValue(), e);
-                        output = e;
-                        resultStatus = Result.FAILURE;
+                    for (PullActions action : profile.getActions()) {
+                        action.after(profile, delta, realmTO, result);
                     }
+
+                    resultStatus = Result.SUCCESS;
+
+                    LOG.debug("{} successfully updated", realm);
+                } 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 Realm {}", 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 update Realm {}", delta.getUid().getUidValue(), e);
+                    output = e;
+                    resultStatus = Result.FAILURE;
                 }
+
                 finalize(unlink
                         ? MatchingRule.toEventName(MatchingRule.UNASSIGN)
                         : MatchingRule.toEventName(MatchingRule.DEPROVISION), resultStatus, before, output, delta);
             }
+
             results.add(result);
         }
 
         return results;
     }
 
-    private List<ProvisioningReport> link(final SyncDelta delta, final List<String> keys, final boolean unlink)
+    private List<ProvisioningReport> link(final SyncDelta delta, final List<Realm> realms, final boolean unlink)
             throws JobExecutionException {
 
         if (!profile.getTask().isPerformUpdate()) {
@@ -475,72 +449,62 @@ public class DefaultRealmPullResultHandler
             return List.of();
         }
 
-        LOG.debug("About to link {}", keys);
+        LOG.debug("About to link {}", realms);
 
         final List<ProvisioningReport> results = new ArrayList<>();
 
-        for (String key : keys) {
-            LOG.debug("About to unassign resource {}", key);
+        for (Realm realm : realms) {
+            LOG.debug("About to unassign resource {}", realm);
 
             ProvisioningReport result = new ProvisioningReport();
             result.setOperation(ResourceOperation.NONE);
             result.setAnyType(REALM_TYPE);
             result.setStatus(ProvisioningReport.Status.SUCCESS);
-            result.setKey(key);
-
-            Realm realm = realmDAO.find(key);
-            RealmTO before = binder.getRealmTO(realm, true);
-            if (before == null) {
-                result.setStatus(ProvisioningReport.Status.FAILURE);
-                result.setMessage(String.format("Realm '%s' not found", key));
-            } else {
-                result.setName(before.getFullPath());
-            }
+            result.setKey(realm.getKey());
+            result.setName(realm.getFullPath());
 
-            Object output;
-            Result resultStatus;
             if (!profile.isDryRun()) {
-                if (before == null) {
-                    resultStatus = Result.FAILURE;
-                    output = null;
-                } else {
-                    try {
-                        if (unlink) {
-                            for (PullActions action : profile.getActions()) {
-                                action.beforeUnlink(profile, delta, before);
-                            }
-                        } else {
-                            for (PullActions action : profile.getActions()) {
-                                action.beforeLink(profile, delta, before);
-                            }
-                        }
+                Object output;
+                Result resultStatus;
 
-                        if (unlink) {
-                            realm.getResources().remove(profile.getTask().getResource());
-                        } else {
-                            realm.add(profile.getTask().getResource());
+                RealmTO before = binder.getRealmTO(realm, true);
+                try {
+                    if (unlink) {
+                        for (PullActions action : profile.getActions()) {
+                            action.beforeUnlink(profile, delta, before);
                         }
-                        output = update(delta, List.of(key), true);
+                    } else {
+                        for (PullActions action : profile.getActions()) {
+                            action.beforeLink(profile, delta, before);
+                        }
+                    }
 
-                        resultStatus = Result.SUCCESS;
+                    if (unlink) {
+                        realm.getResources().remove(profile.getTask().getResource());
+                    } else {
+                        realm.add(profile.getTask().getResource());
+                    }
+                    output = update(delta, List.of(realm), true);
 
-                        LOG.debug("{} successfully updated", realm);
-                    } 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 Realm {}", delta.getUid().getUidValue(), e);
-                        output = e;
-                        resultStatus = Result.FAILURE;
-                    } catch (Exception e) {
-                        throwIgnoreProvisionException(delta, e);
+                    resultStatus = Result.SUCCESS;
 
-                        result.setStatus(ProvisioningReport.Status.FAILURE);
-                        result.setMessage(ExceptionUtils.getRootCauseMessage(e));
-                        LOG.error("Could not update Realm {}", delta.getUid().getUidValue(), e);
-                        output = e;
-                        resultStatus = Result.FAILURE;
-                    }
+                    LOG.debug("{} successfully updated", realm);
+                } 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 Realm {}", 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 update Realm {}", delta.getUid().getUidValue(), e);
+                    output = e;
+                    resultStatus = Result.FAILURE;
                 }
+
                 finalize(unlink
                         ? MatchingRule.toEventName(MatchingRule.UNLINK)
                         : MatchingRule.toEventName(MatchingRule.LINK), resultStatus, before, output, delta);
@@ -551,7 +515,7 @@ public class DefaultRealmPullResultHandler
         return results;
     }
 
-    private List<ProvisioningReport> delete(final SyncDelta delta, final List<String> keys)
+    private List<ProvisioningReport> delete(final SyncDelta delta, final List<Realm> realms)
             throws JobExecutionException {
 
         if (!profile.getTask().isPerformDelete()) {
@@ -560,31 +524,24 @@ public class DefaultRealmPullResultHandler
             return List.of();
         }
 
-        LOG.debug("About to delete {}", keys);
+        LOG.debug("About to delete {}", realms);
 
         List<ProvisioningReport> results = new ArrayList<>();
 
-        for (String key : keys) {
+        realms.forEach(realm -> {
             Object output;
             Result resultStatus = Result.FAILURE;
 
             ProvisioningReport result = new ProvisioningReport();
 
+            RealmTO before = binder.getRealmTO(realm, true);
             try {
-                result.setKey(key);
+                result.setKey(realm.getKey());
+                result.setName(realm.getFullPath());
                 result.setOperation(ResourceOperation.DELETE);
                 result.setAnyType(REALM_TYPE);
                 result.setStatus(ProvisioningReport.Status.SUCCESS);
 
-                Realm realm = realmDAO.find(key);
-                RealmTO before = binder.getRealmTO(realm, true);
-                if (before == null) {
-                    result.setStatus(ProvisioningReport.Status.FAILURE);
-                    result.setMessage(String.format("Realm '%s' not found", key));
-                } else {
-                    result.setName(before.getFullPath());
-                }
-
                 if (!profile.isDryRun()) {
                     for (PullActions action : profile.getActions()) {
                         action.beforeDelete(profile, delta, before);
@@ -615,7 +572,7 @@ public class DefaultRealmPullResultHandler
                         PropagationByResource<String> propByRes = new PropagationByResource<>();
                         propByRes.addAll(ResourceOperation.DELETE, realm.getResourceKeys());
                         List<PropagationTaskInfo> taskInfos = propagationManager.createTasks(realm, propByRes, null);
-                        taskExecutor.execute(taskInfos, false, this.adminUser);
+                        taskExecutor.execute(taskInfos, false, adminUser);
 
                         realmDAO.delete(realm);
 
@@ -639,20 +596,16 @@ public class DefaultRealmPullResultHandler
 
                 results.add(result);
             } catch (DelegatedAdministrationException e) {
-                LOG.error("Not allowed to read Realm {}", key, e);
+                LOG.error("Not allowed to read Realm {}", realm, e);
             } catch (Exception e) {
-                LOG.error("Could not delete Realm {}", key, e);
+                LOG.error("Could not delete Realm {}", realm, e);
             }
-        }
+        });
 
         return results;
     }
 
-    private ProvisioningReport ignore(
-            final SyncDelta delta,
-            final boolean matching)
-            throws JobExecutionException {
-
+    private ProvisioningReport ignore(final SyncDelta delta, final boolean matching) throws JobExecutionException {
         LOG.debug("Any to ignore {}", delta.getObject().getUid().getUidValue());
 
         ProvisioningReport result = new ProvisioningReport();
@@ -684,22 +637,22 @@ public class DefaultRealmPullResultHandler
         LOG.debug("Transformed {} for {} as {}",
                 finalDelta.getDeltaType(), finalDelta.getUid().getUidValue(), finalDelta.getObject().getObjectClass());
 
-        List<String> keys = pullUtils.match(finalDelta, orgUnit);
+        List<Realm> realms = inboundMatcher.match(finalDelta, orgUnit);
         LOG.debug("Match found for {} as {}: {}",
-                finalDelta.getUid().getUidValue(), finalDelta.getObject().getObjectClass(), keys);
+                finalDelta.getUid().getUidValue(), finalDelta.getObject().getObjectClass(), realms);
 
-        if (keys.size() > 1) {
+        if (realms.size() > 1) {
             switch (profile.getConflictResolutionAction()) {
                 case IGNORE:
                     throw new IgnoreProvisionException("More than one match found for "
-                            + finalDelta.getObject().getUid().getUidValue() + ": " + keys);
+                            + finalDelta.getObject().getUid().getUidValue() + ": " + realms);
 
                 case FIRSTMATCH:
-                    keys = keys.subList(0, 1);
+                    realms = realms.subList(0, 1);
                     break;
 
                 case LASTMATCH:
-                    keys = keys.subList(keys.size() - 1, keys.size());
+                    realms = realms.subList(realms.size() - 1, realms.size());
                     break;
 
                 default:
@@ -709,7 +662,7 @@ public class DefaultRealmPullResultHandler
 
         try {
             if (SyncDeltaType.CREATE_OR_UPDATE == finalDelta.getDeltaType()) {
-                if (keys.isEmpty()) {
+                if (realms.isEmpty()) {
                     switch (profile.getTask().getUnmatchingRule()) {
                         case ASSIGN:
                             profile.getResults().addAll(assign(finalDelta, orgUnit));
@@ -729,23 +682,23 @@ public class DefaultRealmPullResultHandler
                 } else {
                     switch (profile.getTask().getMatchingRule()) {
                         case UPDATE:
-                            profile.getResults().addAll(update(finalDelta, keys, false));
+                            profile.getResults().addAll(update(finalDelta, realms, false));
                             break;
 
                         case DEPROVISION:
-                            profile.getResults().addAll(deprovision(finalDelta, keys, false));
+                            profile.getResults().addAll(deprovision(finalDelta, realms, false));
                             break;
 
                         case UNASSIGN:
-                            profile.getResults().addAll(deprovision(finalDelta, keys, true));
+                            profile.getResults().addAll(deprovision(finalDelta, realms, true));
                             break;
 
                         case LINK:
-                            profile.getResults().addAll(link(finalDelta, keys, false));
+                            profile.getResults().addAll(link(finalDelta, realms, false));
                             break;
 
                         case UNLINK:
-                            profile.getResults().addAll(link(finalDelta, keys, true));
+                            profile.getResults().addAll(link(finalDelta, realms, true));
                             break;
 
                         case IGNORE:
@@ -757,11 +710,11 @@ public class DefaultRealmPullResultHandler
                     }
                 }
             } else if (SyncDeltaType.DELETE == finalDelta.getDeltaType()) {
-                if (keys.isEmpty()) {
+                if (realms.isEmpty()) {
                     finalize(ResourceOperation.DELETE.name().toLowerCase(), Result.SUCCESS, null, null, finalDelta);
                     LOG.debug("No match found for deletion");
                 } else {
-                    profile.getResults().addAll(delete(finalDelta, keys));
+                    profile.getResults().addAll(delete(finalDelta, realms));
                 }
             }
         } catch (IllegalStateException | IllegalArgumentException e) {
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultRealmPushResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultRealmPushResultHandler.java
index 1460e6a..3576c8c 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultRealmPushResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultRealmPushResultHandler.java
@@ -20,10 +20,10 @@ package org.apache.syncope.core.provisioning.java.pushpull;
 
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.stream.Stream;
 import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.apache.syncope.common.lib.to.RealmTO;
 import org.apache.syncope.common.lib.types.AuditElements;
@@ -60,15 +60,10 @@ import org.springframework.scheduling.quartz.SchedulerFactoryBean;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 
-import javax.annotation.Resource;
-
 public class DefaultRealmPushResultHandler
         extends AbstractRealmResultHandler<PushTask, PushActions>
         implements RealmPushResultHandler {
 
-    @Resource(name = "adminUser")
-    protected String adminUser;
-
     @Autowired
     private MappingManager mappingManager;
 
@@ -115,7 +110,7 @@ public class DefaultRealmPushResultHandler
         if (!taskInfos.isEmpty()) {
             taskInfos.get(0).setBeforeObj(Optional.ofNullable(beforeObj));
             PropagationReporter reporter = new DefaultPropagationReporter();
-            taskExecutor.execute(taskInfos.get(0), reporter, this.adminUser);
+            taskExecutor.execute(taskInfos.get(0), reporter, adminUser);
             reportPropagation(result, reporter);
         }
 
@@ -133,7 +128,7 @@ public class DefaultRealmPushResultHandler
         if (!taskInfos.isEmpty()) {
             taskInfos.get(0).setBeforeObj(Optional.ofNullable(beforeObj));
             PropagationReporter reporter = new DefaultPropagationReporter();
-            taskExecutor.execute(taskInfos.get(0), reporter, this.adminUser);
+            taskExecutor.execute(taskInfos.get(0), reporter, adminUser);
             reportPropagation(result, reporter);
         }
     }
@@ -146,7 +141,7 @@ public class DefaultRealmPushResultHandler
         propByRes.add(ResourceOperation.CREATE, profile.getTask().getResource().getKey());
 
         PropagationReporter reporter = taskExecutor.execute(
-                propagationManager.createTasks(realm, propByRes, noPropResources), false, this.adminUser);
+                propagationManager.createTasks(realm, propByRes, noPropResources), false, adminUser);
         reportPropagation(result, reporter);
     }
 
@@ -180,7 +175,7 @@ public class DefaultRealmPushResultHandler
             final String connObjectKey,
             final String connObjectKeyValue,
             final boolean ignoreCaseMatch,
-            final Iterator<? extends Item> iterator) {
+            final Stream<? extends Item> mapItems) {
 
         ConnectorObject obj = null;
         try {
@@ -188,7 +183,7 @@ public class DefaultRealmPushResultHandler
                     objectClass,
                     AttributeBuilder.build(connObjectKey, connObjectKeyValue),
                     ignoreCaseMatch,
-                    MappingUtils.buildOperationOptions(iterator));
+                    MappingUtils.buildOperationOptions(mapItems));
         } catch (TimeoutException toe) {
             LOG.debug("Request timeout", toe);
             throw toe;
@@ -224,7 +219,7 @@ public class DefaultRealmPushResultHandler
                     connObjectKey.get().getExtAttrName(),
                     connObjecKeyValue.get(),
                     orgUnit.isIgnoreCaseMatch(),
-                    orgUnit.getItems().iterator());
+                    orgUnit.getItems().stream());
         } else {
             LOG.debug("OrgUnitItem {} or its value {} are null", connObjectKey, connObjecKeyValue);
         }
@@ -406,7 +401,7 @@ public class DefaultRealmPushResultHandler
                                 connObjectKey.get().getExtAttrName(),
                                 connObjecKeyValue.get(),
                                 orgUnit.isIgnoreCaseMatch(),
-                                orgUnit.getItems().iterator());
+                                orgUnit.getItems().stream());
                     }
                 }
             } catch (IgnoreProvisionException 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 20ec326..3ddd89e 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
@@ -40,11 +40,13 @@ 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.MatchType;
 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.Any;
 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;
@@ -90,8 +92,8 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
     }
 
     @Override
-    protected AnyTO getAnyTO(final String key) {
-        return userDataBinder.getUserTO(key);
+    protected AnyTO getAnyTO(final Any<?> any) {
+        return userDataBinder.getUserTO((User) any, true);
     }
 
     @Override
@@ -113,7 +115,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
                 Set.of(profile.getTask().getResource().getKey()),
                 true);
 
-        return getAnyTO(created.getKey());
+        return userDataBinder.getUserTO(created.getKey());
     }
 
     @Override
@@ -137,11 +139,10 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
     protected void handleLinkedAccounts(
             final SyncDelta delta,
             final List<PullMatch> matches,
-            final Provision provision,
-            final AnyUtils anyUtils) throws JobExecutionException {
+            final Provision provision) throws JobExecutionException {
 
         for (PullMatch match : matches) {
-            User user = userDAO.find(match.getLinkingUserKey());
+            User user = (User) match.getAny();
             if (user == null) {
                 LOG.error("Could not find linking user, cannot process match {}", match);
                 return;
@@ -201,7 +202,8 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
                         // do nothing
                     }
                 } else if (SyncDeltaType.DELETE == delta.getDeltaType()) {
-                    finalize(
+                    end(
+                            AnyTypeKind.USER,
                             ResourceOperation.DELETE.name().toLowerCase(),
                             AuditElements.Result.SUCCESS,
                             null,
@@ -220,7 +222,8 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
 
         if (!profile.getTask().isPerformUpdate()) {
             LOG.debug("PullTask not configured for update");
-            finalize(MatchingRule.toEventName(MatchingRule.UPDATE), Result.SUCCESS, null, null, delta);
+            end(AnyTypeKind.USER,
+                    MatchingRule.toEventName(MatchingRule.UPDATE), Result.SUCCESS, null, null, delta);
             return Optional.empty();
         }
 
@@ -228,7 +231,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
 
         ProvisioningReport report = new ProvisioningReport();
         report.setOperation(ResourceOperation.DELETE);
-        report.setAnyType(PullMatch.MatchTarget.LINKED_ACCOUNT.name());
+        report.setAnyType(MatchType.LINKED_ACCOUNT.name());
         report.setStatus(ProvisioningReport.Status.SUCCESS);
         report.setKey(account.getKey());
 
@@ -285,7 +288,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
                 resultStatus = Result.FAILURE;
             }
 
-            finalize(MatchingRule.toEventName(matchingRule), resultStatus, before, output, delta);
+            end(AnyTypeKind.USER, MatchingRule.toEventName(matchingRule), resultStatus, before, output, delta);
         }
 
         return Optional.of(report);
@@ -301,7 +304,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
 
         if (!profile.getTask().isPerformCreate()) {
             LOG.debug("PullTask not configured for create");
-            finalize(UnmatchingRule.toEventName(rule), Result.SUCCESS, null, null, delta);
+            end(AnyTypeKind.USER, UnmatchingRule.toEventName(rule), Result.SUCCESS, null, null, delta);
             return Optional.empty();
         }
 
@@ -310,16 +313,16 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
         ProvisioningReport report = new ProvisioningReport();
         report.setOperation(ResourceOperation.CREATE);
         report.setName(accountTO.getConnObjectKeyValue());
-        report.setAnyType(PullMatch.MatchTarget.LINKED_ACCOUNT.name());
+        report.setAnyType(MatchType.LINKED_ACCOUNT.name());
         report.setStatus(ProvisioningReport.Status.SUCCESS);
 
         if (profile.isDryRun()) {
             report.setKey(null);
-            finalize(UnmatchingRule.toEventName(rule), Result.SUCCESS, null, null, delta);
+            end(AnyTypeKind.USER, UnmatchingRule.toEventName(rule), Result.SUCCESS, null, null, delta);
         } else {
             UserTO owner = userDataBinder.getUserTO(user, false);
             UserCR connObject = connObjectUtils.getAnyCR(
-                    delta.getObject(), profile.getTask(), provision, getAnyUtils(), false);
+                    delta.getObject(), profile.getTask(), provision, false);
 
             if (connObject.getUsername().equals(owner.getUsername())) {
                 accountTO.setUsername(null);
@@ -395,7 +398,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
                 resultStatus = Result.FAILURE;
             }
 
-            finalize(UnmatchingRule.toEventName(rule), resultStatus, null, output, delta);
+            end(AnyTypeKind.USER, UnmatchingRule.toEventName(rule), resultStatus, null, output, delta);
         }
 
         return Optional.of(report);
@@ -409,7 +412,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
 
         if (!profile.getTask().isPerformUpdate()) {
             LOG.debug("PullTask not configured for update");
-            finalize(MatchingRule.toEventName(MatchingRule.UPDATE), Result.SUCCESS, null, null, delta);
+            end(AnyTypeKind.USER, MatchingRule.toEventName(MatchingRule.UPDATE), Result.SUCCESS, null, null, delta);
             return Optional.empty();
         }
 
@@ -419,7 +422,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
         report.setOperation(ResourceOperation.UPDATE);
         report.setKey(account.getKey());
         report.setName(account.getConnObjectKeyValue());
-        report.setAnyType(PullMatch.MatchTarget.LINKED_ACCOUNT.name());
+        report.setAnyType(MatchType.LINKED_ACCOUNT.name());
         report.setStatus(ProvisioningReport.Status.SUCCESS);
 
         if (!profile.isDryRun()) {
@@ -427,7 +430,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
 
             UserTO owner = userDataBinder.getUserTO(account.getOwner(), false);
             UserCR connObject = connObjectUtils.getAnyCR(
-                    delta.getObject(), profile.getTask(), provision, getAnyUtils(), false);
+                    delta.getObject(), profile.getTask(), provision, false);
 
             LinkedAccountTO update = userDataBinder.getLinkedAccountTO(account);
 
@@ -507,7 +510,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
                 resultStatus = Result.FAILURE;
             }
 
-            finalize(MatchingRule.toEventName(MatchingRule.UPDATE), resultStatus, before, output, delta);
+            end(AnyTypeKind.USER, MatchingRule.toEventName(MatchingRule.UPDATE), resultStatus, before, output, delta);
         }
 
         return Optional.of(report);
@@ -520,7 +523,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
 
         if (!profile.getTask().isPerformDelete()) {
             LOG.debug("PullTask not configured for delete");
-            finalize(ResourceOperation.DELETE.name().toLowerCase(), Result.SUCCESS, null, null, delta);
+            end(AnyTypeKind.USER, ResourceOperation.DELETE.name().toLowerCase(), Result.SUCCESS, null, null, delta);
             return Optional.empty();
         }
 
@@ -535,7 +538,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
             report.setKey(account.getKey());
             report.setName(account.getConnObjectKeyValue());
             report.setOperation(ResourceOperation.DELETE);
-            report.setAnyType(PullMatch.MatchTarget.LINKED_ACCOUNT.name());
+            report.setAnyType(MatchType.LINKED_ACCOUNT.name());
             report.setStatus(ProvisioningReport.Status.SUCCESS);
 
             if (!profile.isDryRun()) {
@@ -573,7 +576,8 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
                     output = e;
                 }
 
-                finalize(ResourceOperation.DELETE.name().toLowerCase(), resultStatus, before, output, delta);
+                end(AnyTypeKind.USER,
+                        ResourceOperation.DELETE.name().toLowerCase(), resultStatus, before, output, delta);
             }
         } catch (Exception e) {
             LOG.error("Could not delete linked account {}", account, e);
@@ -596,7 +600,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
             report.setKey(null);
             report.setName(delta.getObject().getUid().getUidValue());
             report.setOperation(ResourceOperation.NONE);
-            report.setAnyType(PullMatch.MatchTarget.LINKED_ACCOUNT.name());
+            report.setAnyType(MatchType.LINKED_ACCOUNT.name());
             report.setStatus(ProvisioningReport.Status.SUCCESS);
             if (message != null && message.length >= 1) {
                 report.setMessage(message[0]);
@@ -605,16 +609,18 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
             report.setKey(account.getKey());
             report.setName(delta.getObject().getUid().getUidValue());
             report.setOperation(ResourceOperation.NONE);
-            report.setAnyType(PullMatch.MatchTarget.LINKED_ACCOUNT.name());
+            report.setAnyType(MatchType.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);
+        end(AnyTypeKind.USER,
+                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/DefaultUserPushResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultUserPushResultHandler.java
index 535d705..d7060c3 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultUserPushResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultUserPushResultHandler.java
@@ -22,24 +22,38 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.stream.Collectors;
+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.request.AnyUR;
 import org.apache.syncope.common.lib.request.UserUR;
 import org.apache.syncope.common.lib.to.AnyTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.lib.types.MatchType;
+import org.apache.syncope.common.lib.types.MatchingRule;
 import org.apache.syncope.core.provisioning.api.PropagationByResource;
 import org.apache.syncope.common.lib.types.ResourceOperation;
+import org.apache.syncope.common.lib.types.UnmatchingRule;
 import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.AnyUtils;
 import org.apache.syncope.core.persistence.api.entity.Entity;
+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.UserWorkflowResult;
 import org.apache.syncope.core.provisioning.api.WorkflowResult;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationReporter;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
+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.PushActions;
 import org.apache.syncope.core.provisioning.api.pushpull.UserPushResultHandler;
 import org.apache.syncope.core.provisioning.java.propagation.DefaultPropagationReporter;
+import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
 import org.identityconnectors.framework.common.objects.ConnectorObject;
+import org.quartz.JobExecutionException;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
 
 public class DefaultUserPushResultHandler extends AbstractPushResultHandler implements UserPushResultHandler {
 
@@ -54,13 +68,13 @@ public class DefaultUserPushResultHandler extends AbstractPushResultHandler impl
     }
 
     @Override
-    protected AnyTO getAnyTO(final String key) {
-        return userDataBinder.getUserTO(key);
+    protected AnyTO getAnyTO(final Any<?> any) {
+        return userDataBinder.getUserTO((User) any, true);
     }
 
     @Override
     protected void provision(final Any<?> any, final Boolean enabled, final ProvisioningReport result) {
-        AnyTO before = getAnyTO(any.getKey());
+        AnyTO before = getAnyTO(any);
 
         List<String> noPropResources = new ArrayList<>(before.getResources());
         noPropResources.remove(profile.getTask().getResource().getKey());
@@ -83,7 +97,7 @@ public class DefaultUserPushResultHandler extends AbstractPushResultHandler impl
                 before.getVirAttrs(),
                 noPropResources),
                 false,
-                this.adminUser);
+                adminUser);
         reportPropagation(result, reporter);
     }
 
@@ -122,14 +136,14 @@ public class DefaultUserPushResultHandler extends AbstractPushResultHandler impl
         if (!taskInfos.isEmpty()) {
             taskInfos.get(0).setBeforeObj(Optional.of(beforeObj));
             PropagationReporter reporter = new DefaultPropagationReporter();
-            taskExecutor.execute(taskInfos.get(0), reporter, this.adminUser);
+            taskExecutor.execute(taskInfos.get(0), reporter, adminUser);
             reportPropagation(result, reporter);
         }
     }
 
     @Override
     protected void deprovision(final Any<?> any, final ConnectorObject beforeObj, final ProvisioningReport result) {
-        AnyTO before = getAnyTO(any.getKey());
+        AnyTO before = getAnyTO(any);
 
         List<String> noPropResources = new ArrayList<>(before.getResources());
         noPropResources.remove(profile.getTask().getResource().getKey());
@@ -153,7 +167,7 @@ public class DefaultUserPushResultHandler extends AbstractPushResultHandler impl
         if (!taskInfos.isEmpty()) {
             taskInfos.get(0).setBeforeObj(Optional.of(beforeObj));
             PropagationReporter reporter = new DefaultPropagationReporter();
-            taskExecutor.execute(taskInfos.get(0), reporter, this.adminUser);
+            taskExecutor.execute(taskInfos.get(0), reporter, adminUser);
             reportPropagation(result, reporter);
         }
     }
@@ -163,4 +177,226 @@ public class DefaultUserPushResultHandler extends AbstractPushResultHandler impl
         WorkflowResult<Pair<UserUR, Boolean>> update = uwfAdapter.update((UserUR) req);
         return new WorkflowResult<>(update.getResult().getLeft(), update.getPropByRes(), update.getPerformedTasks());
     }
+
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
+    @Override
+    public boolean handle(final LinkedAccount account, final Provision provision) {
+        try {
+            doHandle(account, provision);
+            return true;
+        } catch (IgnoreProvisionException e) {
+            ProvisioningReport ignoreResult = profile.getResults().stream().
+                    filter(report -> account.getKey().equalsIgnoreCase(report.getKey())).
+                    findFirst().
+                    orElse(null);
+            if (ignoreResult == null) {
+                ignoreResult = new ProvisioningReport();
+                ignoreResult.setKey(account.getKey());
+                ignoreResult.setAnyType(MatchType.LINKED_ACCOUNT.name());
+
+                profile.getResults().add(ignoreResult);
+            }
+
+            ignoreResult.setOperation(ResourceOperation.NONE);
+            ignoreResult.setStatus(ProvisioningReport.Status.IGNORE);
+            ignoreResult.setMessage(e.getMessage());
+
+            LOG.warn("Ignoring during push", e);
+            return true;
+        } catch (JobExecutionException e) {
+            LOG.error("Push failed", e);
+            return false;
+        }
+    }
+
+    protected void doHandle(final LinkedAccount account, final Provision provision) throws JobExecutionException {
+        ProvisioningReport result = new ProvisioningReport();
+        profile.getResults().add(result);
+
+        result.setKey(account.getKey());
+        result.setAnyType(MatchType.LINKED_ACCOUNT.name());
+        result.setName(account.getConnObjectKeyValue());
+
+        LOG.debug("Pushing linked account {} towards {}", account.getKey(), profile.getTask().getResource());
+
+        // Try to read remote object BEFORE any actual operation
+        Optional<ConnectorObject> connObj = MappingUtils.getConnObjectKeyItem(provision).
+                map(connObjectKeyItem -> outboundMatcher.matchByConnObjectKeyValue(
+                profile.getConnector(), connObjectKeyItem, account.getConnObjectKeyValue(), provision)).
+                orElse(Optional.empty());
+        LOG.debug("Match found for linked account {} as {}: {}", account, provision.getObjectClass(), connObj);
+
+        ConnectorObject beforeObj = connObj.isPresent() ? connObj.get() : null;
+
+        if (profile.isDryRun()) {
+            if (beforeObj == null) {
+                result.setOperation(toResourceOperation(profile.getTask().getUnmatchingRule()));
+            } else {
+                result.setOperation(toResourceOperation(profile.getTask().getMatchingRule()));
+            }
+            result.setStatus(ProvisioningReport.Status.SUCCESS);
+        } else {
+            Boolean enable = profile.getTask().isSyncStatus()
+                    ? BooleanUtils.negate(account.isSuspended())
+                    : null;
+            try {
+                if (beforeObj == null) {
+                    result.setOperation(toResourceOperation(profile.getTask().getUnmatchingRule()));
+
+                    switch (profile.getTask().getUnmatchingRule()) {
+                        case ASSIGN:
+                        case PROVISION:
+                            for (PushActions action : profile.getActions()) {
+                                if (profile.getTask().getUnmatchingRule() == UnmatchingRule.ASSIGN) {
+                                    action.beforeAssign(profile, account);
+                                } else {
+                                    action.beforeProvision(profile, account);
+                                }
+                            }
+
+                            if (!profile.getTask().isPerformCreate()) {
+                                LOG.debug("PushTask not configured for create");
+                                result.setStatus(ProvisioningReport.Status.IGNORE);
+                            } else {
+                                provision(account, enable, result);
+                            }
+                            break;
+
+                        case UNLINK:
+                            LOG.warn("{} not applicable to linked accounts, ignoring",
+                                    profile.getTask().getUnmatchingRule());
+                            break;
+
+                        case IGNORE:
+                            result.setStatus(ProvisioningReport.Status.IGNORE);
+                            break;
+
+                        default:
+                        // do nothing
+                    }
+                } else {
+                    result.setOperation(toResourceOperation(profile.getTask().getMatchingRule()));
+
+                    switch (profile.getTask().getMatchingRule()) {
+                        case UPDATE:
+                            for (PushActions action : profile.getActions()) {
+                                action.beforeUpdate(profile, account);
+                            }
+                            if (!profile.getTask().isPerformUpdate()) {
+                                LOG.debug("PushTask not configured for update");
+                                result.setStatus(ProvisioningReport.Status.IGNORE);
+                            } else {
+                                update(account, enable, beforeObj, ResourceOperation.UPDATE, result);
+                            }
+                            break;
+
+                        case UNASSIGN:
+                        case DEPROVISION:
+                            for (PushActions action : profile.getActions()) {
+                                if (profile.getTask().getMatchingRule() == MatchingRule.UNASSIGN) {
+                                    action.beforeUnassign(profile, account);
+                                } else {
+                                    action.beforeDeprovision(profile, account);
+                                }
+                            }
+
+                            if (!profile.getTask().isPerformDelete()) {
+                                LOG.debug("PushTask not configured for delete");
+                                result.setStatus(ProvisioningReport.Status.IGNORE);
+                            } else {
+                                update(account, enable, beforeObj, ResourceOperation.DELETE, result);
+                            }
+                            break;
+
+                        case LINK:
+                        case UNLINK:
+                            LOG.warn("{} not applicable to linked accounts, ignoring",
+                                    profile.getTask().getMatchingRule());
+                            break;
+
+                        case IGNORE:
+                            result.setStatus(ProvisioningReport.Status.IGNORE);
+                            break;
+
+                        default:
+                        // do nothing
+                    }
+                }
+
+                for (PushActions action : profile.getActions()) {
+                    action.after(profile, account, result);
+                }
+
+                if (result.getStatus() == null) {
+                    result.setStatus(ProvisioningReport.Status.SUCCESS);
+                }
+            } catch (IgnoreProvisionException e) {
+                throw e;
+            } catch (Exception e) {
+                result.setStatus(ProvisioningReport.Status.FAILURE);
+                result.setMessage(ExceptionUtils.getRootCauseMessage(e));
+
+                LOG.warn("Error pushing linked account {} towards {}", account, profile.getTask().getResource(), e);
+
+                for (PushActions action : profile.getActions()) {
+                    action.onError(profile, account, result, e);
+                }
+
+                throw new JobExecutionException(e);
+            }
+        }
+    }
+
+    protected void provision(
+            final LinkedAccount account,
+            final Boolean enable,
+            final ProvisioningReport result) {
+
+        PropagationByResource<Pair<String, String>> propByLinkedAccount = new PropagationByResource<>();
+        propByLinkedAccount.add(
+                ResourceOperation.CREATE,
+                Pair.of(account.getResource().getKey(), account.getConnObjectKeyValue()));
+
+        List<PropagationTaskInfo> taskInfos = propagationManager.getUserCreateTasks(
+                account.getOwner().getKey(),
+                null,
+                enable,
+                new PropagationByResource<>(),
+                propByLinkedAccount,
+                null,
+                null);
+        if (!taskInfos.isEmpty()) {
+            taskInfos.get(0).setBeforeObj(Optional.ofNullable(null));
+            PropagationReporter reporter = new DefaultPropagationReporter();
+            taskExecutor.execute(taskInfos.get(0), reporter, adminUser);
+            reportPropagation(result, reporter);
+        }
+    }
+
+    protected void update(
+            final LinkedAccount account,
+            final Boolean enable,
+            final ConnectorObject beforeObj,
+            final ResourceOperation operation,
+            final ProvisioningReport result) {
+
+        UserUR req = new UserUR();
+        req.setKey(account.getOwner().getKey());
+
+        PropagationByResource<Pair<String, String>> propByLinkedAccount = new PropagationByResource<>();
+        propByLinkedAccount.add(operation, Pair.of(account.getResource().getKey(), account.getConnObjectKeyValue()));
+
+        List<PropagationTaskInfo> taskInfos = propagationManager.getUserUpdateTasks(
+                new UserWorkflowResult<>(
+                        Pair.of(req, enable),
+                        new PropagationByResource<>(),
+                        propByLinkedAccount,
+                        ""));
+        if (!taskInfos.isEmpty()) {
+            taskInfos.get(0).setBeforeObj(Optional.ofNullable(null));
+            PropagationReporter reporter = new DefaultPropagationReporter();
+            taskExecutor.execute(taskInfos.get(0), reporter, adminUser);
+            reportPropagation(result, reporter);
+        }
+    }
 }
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/InboundMatcher.java
similarity index 65%
rename from core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PullUtils.java
rename to core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/InboundMatcher.java
index 5dab005..33a9764 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/InboundMatcher.java
@@ -20,43 +20,45 @@ package org.apache.syncope.core.provisioning.java.pushpull;
 
 import java.text.ParseException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.lib.types.MatchType;
 import org.apache.syncope.core.persistence.api.attrvalue.validation.ParsingValidationException;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.PullCorrelationRule;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
-import org.apache.syncope.core.persistence.api.dao.UserDAO;
-import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
-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.AnyType;
-import org.apache.syncope.core.persistence.api.entity.AnyUtils;
-import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
-import org.apache.syncope.core.persistence.api.entity.Entity;
-import org.apache.syncope.core.persistence.api.entity.DerSchema;
-import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue;
-import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
-import org.apache.syncope.core.persistence.api.entity.PlainSchema;
 import org.apache.syncope.core.persistence.api.entity.Realm;
-import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
-import org.apache.syncope.core.persistence.api.entity.group.Group;
 import org.apache.syncope.core.persistence.api.entity.policy.PullCorrelationRuleEntity;
 import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
 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.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.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.dao.VirSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+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.AnyUtils;
+import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
+import org.apache.syncope.core.persistence.api.entity.DerSchema;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.apache.syncope.core.persistence.api.entity.VirSchema;
+import org.apache.syncope.core.provisioning.api.VirAttrHandler;
 import org.apache.syncope.core.provisioning.java.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
 import org.apache.syncope.core.spring.ImplementationManager;
@@ -70,40 +72,30 @@ import org.identityconnectors.framework.common.objects.SyncDelta;
 import org.identityconnectors.framework.common.objects.SyncDeltaBuilder;
 import org.identityconnectors.framework.common.objects.SyncDeltaType;
 import org.identityconnectors.framework.common.objects.SyncToken;
+import org.identityconnectors.framework.common.objects.Uid;
 import org.identityconnectors.framework.spi.SearchResultsHandler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
 
 @Transactional(readOnly = true)
 @Component
-public class PullUtils {
+public class InboundMatcher {
 
-    private static final Logger LOG = LoggerFactory.getLogger(PullUtils.class);
+    private static final Logger LOG = LoggerFactory.getLogger(InboundMatcher.class);
 
-    /**
-     * Any Object DAO.
-     */
     @Autowired
-    private AnyObjectDAO anyObjectDAO;
+    private UserDAO userDAO;
 
-    /**
-     * User DAO.
-     */
     @Autowired
-    private UserDAO userDAO;
+    private AnyObjectDAO anyObjectDAO;
 
-    /**
-     * Group DAO.
-     */
     @Autowired
     private GroupDAO groupDAO;
 
-    /**
-     * Search DAO.
-     */
     @Autowired
     private AnySearchDAO searchDAO;
 
@@ -111,31 +103,39 @@ public class PullUtils {
     private RealmDAO realmDAO;
 
     @Autowired
-    private AnyUtilsFactory anyUtilsFactory;
+    private VirSchemaDAO virSchemaDAO;
+
+    @Autowired
+    private VirAttrHandler virAttrHandler;
 
     @Autowired
     private IntAttrNameParser intAttrNameParser;
 
-    public Optional<String> match(
+    @Autowired
+    private AnyUtilsFactory anyUtilsFactory;
+
+    public Optional<PullMatch> match(
             final AnyType anyType,
-            final String name,
+            final String nameValue,
             final ExternalResource resource,
-            final Connector connector,
-            final boolean ignoreCaseMatch) {
+            final Connector connector) {
 
         Optional<? extends Provision> provision = resource.getProvision(anyType);
-        if (provision.isEmpty()) {
+        if (!provision.isPresent()) {
             return Optional.empty();
         }
 
-        Optional<String> result = Optional.empty();
-
-        AnyUtils anyUtils = anyUtilsFactory.getInstance(anyType.getKind());
+        Stream<MappingItem> mapItems = Stream.concat(
+                provision.get().getMapping().getItems().stream(),
+                virSchemaDAO.findByProvision(provision.get()).stream().map(VirSchema::asLinkingMappingItem));
 
         List<ConnectorObject> found = new ArrayList<>();
-        Name nameAttr = new Name(name);
+
+        Name nameAttr = new Name(nameValue);
         connector.search(provision.get().getObjectClass(),
-                ignoreCaseMatch ? FilterBuilder.equalsIgnoreCase(nameAttr) : FilterBuilder.equalTo(nameAttr),
+                provision.get().isIgnoreCaseMatch()
+                ? FilterBuilder.equalsIgnoreCase(nameAttr)
+                : FilterBuilder.equalTo(nameAttr),
                 new SearchResultsHandler() {
 
             @Override
@@ -147,15 +147,16 @@ public class PullUtils {
             public boolean handle(final ConnectorObject connectorObject) {
                 return found.add(connectorObject);
             }
-        }, MappingUtils.buildOperationOptions(
-                        MappingUtils.getPullItems(provision.get().getMapping().getItems()).iterator()));
+        }, MappingUtils.buildOperationOptions(mapItems));
+
+        Optional<PullMatch> result = Optional.empty();
 
         if (found.isEmpty()) {
-            LOG.debug("No {} found on {} with __NAME__ {}", provision.get().getObjectClass(), resource, name);
+            LOG.debug("No {} found on {} with {} {}", provision.get().getObjectClass(), resource, Name.NAME, nameValue);
         } else {
             if (found.size() > 1) {
-                LOG.warn("More than one {} found on {} with __NAME__ {} - taking first only",
-                        provision.get().getObjectClass(), resource, name);
+                LOG.warn("More than one {} found on {} with {} {} - taking first only",
+                        provision.get().getObjectClass(), resource, Name.NAME, nameValue);
             }
 
             ConnectorObject connObj = found.iterator().next();
@@ -166,15 +167,18 @@ public class PullUtils {
                                 setDeltaType(SyncDeltaType.CREATE_OR_UPDATE).
                                 setObject(connObj).
                                 build(),
-                        provision.get(), anyUtils);
+                        provision.get());
                 if (matches.isEmpty()) {
-                    LOG.debug("No matching {} found for {}, aborting", anyUtils.anyTypeKind(), connObj);
+                    LOG.debug("No matching {} found for {}, aborting", anyType.getKind(), connObj);
                 } else {
                     if (matches.size() > 1) {
-                        LOG.warn("More than one {} found {} - taking first only", anyUtils.anyTypeKind(), matches);
+                        LOG.warn("More than one {} found {} - taking first only", anyType.getKind(), matches);
                     }
 
-                    result = Optional.ofNullable(matches.iterator().next().getMatchingKey());
+                    result = matches.stream().filter(match -> match.getAny() != null).findFirst();
+                    if (result.isPresent()) {
+                        virAttrHandler.setValues(result.get().getAny(), connObj);
+                    }
                 }
             } catch (IllegalArgumentException e) {
                 LOG.warn(e.getMessage());
@@ -184,69 +188,51 @@ public class PullUtils {
         return result;
     }
 
-    private List<PullMatch> findByConnObjectKey(
-            final SyncDelta syncDelta, final Provision provision, final AnyUtils anyUtils) {
-
-        List<PullMatch> noMatchResult = List.of(PullCorrelationRule.NO_MATCH);
-
-        String connObjectKey = null;
+    public List<PullMatch> matchByConnObjectKeyValue(
+            final MappingItem connObjectKeyItem,
+            final String connObjectKeyValue,
+            final Provision provision) {
 
-        Optional<? extends MappingItem> connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision);
-        if (connObjectKeyItem.isPresent()) {
-            Attribute connObjectKeyAttr = syncDelta.getObject().
-                    getAttributeByName(connObjectKeyItem.get().getExtAttrName());
-            if (connObjectKeyAttr != null) {
-                connObjectKey = AttributeUtil.getStringValue(connObjectKeyAttr);
-            }
-        }
-        if (connObjectKey == null) {
-            return noMatchResult;
-        }
-
-        for (ItemTransformer transformer : MappingUtils.getItemTransformers(connObjectKeyItem.get())) {
+        String finalConnObjectKeyValue = connObjectKeyValue;
+        for (ItemTransformer transformer : MappingUtils.getItemTransformers(connObjectKeyItem)) {
             List<Object> output = transformer.beforePull(
-                    connObjectKeyItem.get(),
+                    connObjectKeyItem,
                     null,
-                    List.of(connObjectKey));
-            if (output != null && !output.isEmpty()) {
-                connObjectKey = output.get(0).toString();
+                    Collections.<Object>singletonList(finalConnObjectKeyValue));
+            if (!CollectionUtils.isEmpty(output)) {
+                finalConnObjectKeyValue = output.get(0).toString();
             }
         }
 
+        List<PullMatch> noMatchResult = Collections.singletonList(PullCorrelationRule.NO_MATCH);
+
         IntAttrName intAttrName;
         try {
             intAttrName = intAttrNameParser.parse(
-                    connObjectKeyItem.get().getIntAttrName(),
-                    provision.getAnyType().getKind());
+                    connObjectKeyItem.getIntAttrName(), provision.getAnyType().getKind());
         } catch (ParseException e) {
-            LOG.error("Invalid intAttrName '{}' specified, ignoring", connObjectKeyItem.get().getIntAttrName(), e);
+            LOG.error("Invalid intAttrName '{}' specified, ignoring", connObjectKeyItem.getIntAttrName(), e);
             return noMatchResult;
         }
 
-        List<PullMatch> result = new ArrayList<>();
+        AnyUtils anyUtils = anyUtilsFactory.getInstance(provision.getAnyType().getKind());
+
+        List<Any<?>> anys = new ArrayList<>();
 
         if (intAttrName.getField() != null) {
             switch (intAttrName.getField()) {
                 case "key":
-                    Any<?> any = anyUtils.dao().find(connObjectKey);
-                    if (any != null) {
-                        result.add(new PullMatch.Builder().matchingKey(any.getKey()).build());
-                    }
+                    Optional.ofNullable(anyUtils.dao().find(connObjectKeyValue)).ifPresent(anys::add);
                     break;
 
                 case "username":
                     if (provision.getAnyType().getKind() == AnyTypeKind.USER && provision.isIgnoreCaseMatch()) {
                         AnyCond cond = new AnyCond(AttributeCond.Type.IEQ);
                         cond.setSchema("username");
-                        cond.setExpression(connObjectKey);
-                        result.addAll(searchDAO.search(SearchCond.getLeafCond(cond), AnyTypeKind.USER).stream().
-                                map(user -> new PullMatch.Builder().matchingKey(user.getKey()).build()).
-                                collect(Collectors.toList()));
+                        cond.setExpression(connObjectKeyValue);
+                        anys.addAll(searchDAO.search(SearchCond.getLeafCond(cond), AnyTypeKind.USER));
                     } else {
-                        User user = userDAO.findByUsername(connObjectKey);
-                        if (user != null) {
-                            result.add(new PullMatch.Builder().matchingKey(user.getKey()).build());
-                        }
+                        Optional.ofNullable(userDAO.findByUsername(connObjectKeyValue)).ifPresent(anys::add);
                     }
                     break;
 
@@ -254,29 +240,19 @@ public class PullUtils {
                     if (provision.getAnyType().getKind() == AnyTypeKind.GROUP && provision.isIgnoreCaseMatch()) {
                         AnyCond cond = new AnyCond(AttributeCond.Type.IEQ);
                         cond.setSchema("name");
-                        cond.setExpression(connObjectKey);
-                        result.addAll(searchDAO.search(SearchCond.getLeafCond(cond), AnyTypeKind.GROUP).stream().
-                                map(group -> new PullMatch.Builder().matchingKey(group.getKey()).build()).
-                                collect(Collectors.toList()));
+                        cond.setExpression(connObjectKeyValue);
+                        anys.addAll(searchDAO.search(SearchCond.getLeafCond(cond), AnyTypeKind.GROUP));
                     } else {
-                        Group group = groupDAO.findByName(connObjectKey);
-                        if (group != null) {
-                            result.add(new PullMatch.Builder().matchingKey(group.getKey()).build());
-                        }
+                        Optional.ofNullable(groupDAO.findByName(connObjectKeyValue)).ifPresent(anys::add);
                     }
 
                     if (provision.getAnyType().getKind() == AnyTypeKind.ANY_OBJECT && provision.isIgnoreCaseMatch()) {
                         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(anyObject -> new PullMatch.Builder().matchingKey(anyObject.getKey()).build()).
-                                collect(Collectors.toList()));
+                        cond.setExpression(connObjectKeyValue);
+                        anys.addAll(searchDAO.search(SearchCond.getLeafCond(cond), AnyTypeKind.ANY_OBJECT));
                     } else {
-                        AnyObject anyObject = anyObjectDAO.findByName(connObjectKey);
-                        if (anyObject != null) {
-                            result.add(new PullMatch.Builder().matchingKey(anyObject.getKey()).build());
-                        }
+                        Optional.ofNullable(anyObjectDAO.findByName(connObjectKeyValue)).ifPresent(anys::add);
                     }
                     break;
 
@@ -289,39 +265,43 @@ public class PullUtils {
                             ? anyUtils.newPlainAttrUniqueValue()
                             : anyUtils.newPlainAttrValue();
                     try {
-                        value.parseValue((PlainSchema) intAttrName.getSchema(), connObjectKey);
+                        value.parseValue((PlainSchema) intAttrName.getSchema(), connObjectKeyValue);
                     } catch (ParsingValidationException e) {
-                        LOG.error("While parsing provided __UID__ {}", value, e);
-                        value.setStringValue(connObjectKey);
+                        LOG.error("While parsing provided {} {}", Uid.NAME, value, e);
+                        value.setStringValue(connObjectKeyValue);
                     }
 
                     if (intAttrName.getSchema().isUniqueConstraint()) {
                         anyUtils.dao().findByPlainAttrUniqueValue((PlainSchema) intAttrName.getSchema(),
                                 (PlainAttrUniqueValue) value, provision.isIgnoreCaseMatch()).
-                                ifPresent(any -> result.add(new PullMatch.Builder().matchingKey(any.getKey()).build()));
+                                ifPresent(anys::add);
                     } else {
-                        result.addAll(anyUtils.dao().findByPlainAttrValue((PlainSchema) intAttrName.getSchema(),
-                                value, provision.isIgnoreCaseMatch()).stream().
-                                map(any -> new PullMatch.Builder().matchingKey(any.getKey()).build()).
-                                collect(Collectors.toList()));
+                        anys.addAll(anyUtils.dao().findByPlainAttrValue((PlainSchema) intAttrName.getSchema(),
+                                value, provision.isIgnoreCaseMatch()));
                     }
                     break;
 
                 case DERIVED:
-                    result.addAll(anyUtils.dao().findByDerAttrValue((DerSchema) intAttrName.getSchema(),
-                            connObjectKey, provision.isIgnoreCaseMatch()).stream().
-                            map(any -> new PullMatch.Builder().matchingKey(any.getKey()).build()).
-                            collect(Collectors.toList()));
+                    anys.addAll(anyUtils.dao().findByDerAttrValue((DerSchema) intAttrName.getSchema(),
+                            connObjectKeyValue, provision.isIgnoreCaseMatch()));
                     break;
 
                 default:
             }
         }
 
+        List<PullMatch> result = anys.stream().
+                map(any -> new PullMatch(MatchType.ANY, any)).
+                collect(Collectors.toList());
+
+        userDAO.findLinkedAccount(provision.getResource(), finalConnObjectKeyValue).
+                map(account -> new PullMatch(MatchType.LINKED_ACCOUNT, account)).
+                ifPresent(result::add);
+
         return result.isEmpty() ? noMatchResult : result;
     }
 
-    private List<PullMatch> findByCorrelationRule(
+    private List<PullMatch> matchByCorrelationRule(
             final SyncDelta syncDelta,
             final Provision provision,
             final PullCorrelationRule rule,
@@ -345,14 +325,9 @@ public class PullUtils {
      *
      * @param syncDelta change operation, including external attributes
      * @param provision mapping
-     * @param anyUtils any utils
      * @return list of matching users' / groups' / any objects' keys
      */
-    public List<PullMatch> match(
-            final SyncDelta syncDelta,
-            final Provision provision,
-            final AnyUtils anyUtils) {
-
+    public List<PullMatch> match(final SyncDelta syncDelta, final Provision provision) {
         Optional<? extends PullCorrelationRuleEntity> correlationRule = provision.getResource().getPullPolicy() == null
                 ? Optional.empty()
                 : provision.getResource().getPullPolicy().getCorrelationRule(provision.getAnyType());
@@ -366,14 +341,40 @@ public class PullUtils {
             }
         }
 
+        List<PullMatch> result = Collections.emptyList();
         try {
-            return rule.isPresent()
-                    ? findByCorrelationRule(syncDelta, provision, rule.get(), anyUtils.anyTypeKind())
-                    : findByConnObjectKey(syncDelta, provision, anyUtils);
+            if (rule.isPresent()) {
+                result = matchByCorrelationRule(syncDelta, provision, rule.get(), provision.getAnyType().getKind());
+            } else {
+                String connObjectKeyValue = null;
+
+                Optional<? extends MappingItem> connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision);
+                if (connObjectKeyItem.isPresent()) {
+                    Attribute connObjectKeyAttr = syncDelta.getObject().
+                            getAttributeByName(connObjectKeyItem.get().getExtAttrName());
+                    if (connObjectKeyAttr != null) {
+                        connObjectKeyValue = AttributeUtil.getStringValue(connObjectKeyAttr);
+                    }
+                    // fallback to __UID__
+                    if (connObjectKeyValue == null) {
+                        connObjectKeyValue = syncDelta.getUid().getUidValue();
+                    }
+                }
+                if (connObjectKeyValue == null) {
+                    result = Collections.singletonList(PullCorrelationRule.NO_MATCH);
+                } else {
+                    result = matchByConnObjectKeyValue(connObjectKeyItem.get(), connObjectKeyValue, provision);
+                }
+            }
         } catch (RuntimeException e) {
             LOG.error("Could not match {} with any existing {}", syncDelta, provision.getAnyType(), e);
-            return List.of();
         }
+
+        if (result.size() == 1 && result.get(0).getMatchTarget() == MatchType.ANY) {
+            virAttrHandler.setValues(result.get(0).getAny(), syncDelta.getObject());
+        }
+
+        return result;
     }
 
     /**
@@ -383,10 +384,8 @@ public class PullUtils {
      * @param orgUnit mapping
      * @return list of matching realms' keys.
      */
-    public List<String> match(
-            final SyncDelta syncDelta,
-            final OrgUnit orgUnit) {
-
+    @Transactional(readOnly = true)
+    public List<Realm> match(final SyncDelta syncDelta, final OrgUnit orgUnit) {
         String connObjectKey = null;
 
         Optional<? extends OrgUnitItem> connObjectKeyItem = orgUnit.getConnObjectKeyItem();
@@ -398,27 +397,27 @@ public class PullUtils {
             }
         }
         if (connObjectKey == null) {
-            return List.of();
+            return Collections.emptyList();
         }
 
         for (ItemTransformer transformer : MappingUtils.getItemTransformers(connObjectKeyItem.get())) {
             List<Object> output = transformer.beforePull(
                     connObjectKeyItem.get(),
                     null,
-                    List.of(connObjectKey));
-            if (output != null && !output.isEmpty()) {
+                    Collections.<Object>singletonList(connObjectKey));
+            if (!CollectionUtils.isEmpty(output)) {
                 connObjectKey = output.get(0).toString();
             }
         }
 
-        List<String> result = new ArrayList<>();
+        List<Realm> result = new ArrayList<>();
 
         Realm realm;
         switch (connObjectKeyItem.get().getIntAttrName()) {
             case "key":
                 realm = realmDAO.find(connObjectKey);
                 if (realm != null) {
-                    result.add(realm.getKey());
+                    result.add(realm);
                 }
                 break;
 
@@ -426,18 +425,16 @@ public class PullUtils {
                 if (orgUnit.isIgnoreCaseMatch()) {
                     final String realmName = connObjectKey;
                     result.addAll(realmDAO.findAll().stream().
-                            filter(r -> r.getName().equalsIgnoreCase(realmName)).
-                            map(Entity::getKey).collect(Collectors.toList()));
+                            filter(r -> r.getName().equalsIgnoreCase(realmName)).collect(Collectors.toList()));
                 } else {
-                    result.addAll(realmDAO.findByName(connObjectKey).stream().
-                            map(Entity::getKey).collect(Collectors.toList()));
+                    result.addAll(realmDAO.findByName(connObjectKey).stream().collect(Collectors.toList()));
                 }
                 break;
 
             case "fullpath":
                 realm = realmDAO.findByFullPath(connObjectKey);
                 if (realm != null) {
-                    result.add(realm.getKey());
+                    result.add(realm);
                 }
                 break;
 
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/LDAPMembershipPullActions.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/LDAPMembershipPullActions.java
index 62c9972..7b09175 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/LDAPMembershipPullActions.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/LDAPMembershipPullActions.java
@@ -27,12 +27,12 @@ import java.util.Set;
 import org.apache.syncope.common.lib.request.AnyUR;
 import org.apache.syncope.common.lib.to.EntityTO;
 import org.apache.syncope.common.lib.to.GroupTO;
-import org.apache.syncope.common.lib.types.ConnConfProperty;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.provisioning.api.Connector;
 import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningProfile;
 import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
+import org.apache.syncope.core.persistence.api.dao.PullMatch;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
 import org.identityconnectors.framework.common.objects.Attribute;
 import org.identityconnectors.framework.common.objects.ConnectorObject;
@@ -63,7 +63,7 @@ public class LDAPMembershipPullActions extends SchedulingPullActions {
     protected GroupDAO groupDAO;
 
     @Autowired
-    private PullUtils pullUtils;
+    private InboundMatcher inboundMatcher;
 
     protected final Map<String, Set<String>> membershipsBefore = new HashMap<>();
 
@@ -76,14 +76,11 @@ public class LDAPMembershipPullActions extends SchedulingPullActions {
      * @return the name of the attribute used to keep track of group memberships
      */
     protected String getGroupMembershipAttrName(final Connector connector) {
-        Optional<ConnConfProperty> groupMembership = connector.getConnInstance().getConf().stream().
+        return connector.getConnInstance().getConf().stream().
                 filter(property -> "groupMemberAttribute".equals(property.getSchema().getName())
-                && !property.getValues().isEmpty()).
-                findFirst();
-
-        return groupMembership.isPresent()
-                ? (String) groupMembership.get().getValues().get(0)
-                : "uniquemember";
+                && !property.getValues().isEmpty()).findFirst().
+                map(groupMembership -> (String) groupMembership.getValues().get(0)).
+                orElse("uniquemember");
     }
 
     /**
@@ -102,8 +99,11 @@ public class LDAPMembershipPullActions extends SchedulingPullActions {
         Attribute membAttr = delta.getObject().getAttributeByName(groupMemberName);
         // if not found, perform an additional read on the underlying connector for the same connector object
         if (membAttr == null) {
-            OperationOptionsBuilder oob = new OperationOptionsBuilder().setAttributesToGet(groupMemberName);
-            ConnectorObject remoteObj = connector.getObject(ObjectClass.GROUP, delta.getUid(), false, oob.build());
+            ConnectorObject remoteObj = connector.getObject(
+                    ObjectClass.GROUP,
+                    delta.getUid(),
+                    false,
+                    new OperationOptionsBuilder().setAttributesToGet(groupMemberName).build());
             if (remoteObj == null) {
                 LOG.debug("Object for '{}' not found", delta.getUid().getUidValue());
             } else {
@@ -170,17 +170,16 @@ public class LDAPMembershipPullActions extends SchedulingPullActions {
         }
 
         getMembAttrValues(delta, profile.getConnector()).forEach(membValue -> {
-            Optional<String> userKey = pullUtils.match(
+            Optional<PullMatch> match = inboundMatcher.match(
                     anyTypeDAO.findUser(),
                     membValue.toString(),
                     profile.getTask().getResource(),
-                    profile.getConnector(),
-                    false);
-            if (userKey.isPresent()) {
-                Set<String> memb = membershipsAfter.get(userKey.get());
+                    profile.getConnector());
+            if (match.isPresent()) {
+                Set<String> memb = membershipsAfter.get(match.get().getAny().getKey());
                 if (memb == null) {
                     memb = new HashSet<>();
-                    membershipsAfter.put(userKey.get(), memb);
+                    membershipsAfter.put(match.get().getAny().getKey(), memb);
                 }
                 memb.add(entity.getKey());
             } else {
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/OutboundMatcher.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/OutboundMatcher.java
new file mode 100644
index 0000000..e206ca8
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/OutboundMatcher.java
@@ -0,0 +1,229 @@
+/*
+ * 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.pushpull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.core.persistence.api.dao.PushCorrelationRule;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.dao.VirSchemaDAO;
+import org.apache.syncope.core.persistence.api.entity.Any;
+import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
+import org.apache.syncope.core.persistence.api.entity.LinkingMappingItem;
+import org.apache.syncope.core.persistence.api.entity.VirSchema;
+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.task.PropagationTask;
+import org.apache.syncope.core.provisioning.api.Connector;
+import org.apache.syncope.core.provisioning.api.MappingManager;
+import org.apache.syncope.core.provisioning.api.TimeoutException;
+import org.apache.syncope.core.provisioning.api.VirAttrHandler;
+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.SearchResult;
+import org.identityconnectors.framework.common.objects.filter.Filter;
+import org.identityconnectors.framework.spi.SearchResultsHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Component
+public class OutboundMatcher {
+
+    private static final Logger LOG = LoggerFactory.getLogger(OutboundMatcher.class);
+
+    @Autowired
+    private MappingManager mappingManager;
+
+    @Autowired
+    private UserDAO userDAO;
+
+    @Autowired
+    private AnyUtilsFactory anyUtilsFactory;
+
+    @Autowired
+    private VirSchemaDAO virSchemaDAO;
+
+    @Autowired
+    private VirAttrHandler virAttrHandler;
+
+    private Optional<PushCorrelationRule> rule(final Provision provision) {
+        Optional<? extends PushCorrelationRuleEntity> correlationRule = provision.getResource().getPushPolicy() == null
+                ? Optional.empty()
+                : provision.getResource().getPushPolicy().getCorrelationRule(provision.getAnyType());
+
+        Optional<PushCorrelationRule> rule = Optional.empty();
+        if (correlationRule.isPresent()) {
+            try {
+                rule = ImplementationManager.buildPushCorrelationRule(correlationRule.get().getImplementation());
+            } catch (Exception e) {
+                LOG.error("While building {}", correlationRule.get().getImplementation(), e);
+            }
+        }
+
+        return rule;
+    }
+
+    public List<ConnectorObject> match(
+            final PropagationTask task,
+            final Connector connector,
+            final Provision provision,
+            final String connObjectKeyValue) {
+
+        Optional<PushCorrelationRule> rule = rule(provision);
+
+        boolean isLinkedAccount = task.getAnyTypeKind() == AnyTypeKind.USER
+                && userDAO.linkedAccountExists(task.getEntityKey(), connObjectKeyValue);
+        Any<?> any = null;
+        if (!isLinkedAccount) {
+            any = anyUtilsFactory.getInstance(task.getAnyTypeKind()).dao().find(task.getEntityKey());
+        }
+
+        List<ConnectorObject> result = new ArrayList<>();
+        try {
+            if (any != null && rule.isPresent()) {
+                result.addAll(matchByCorrelationRule(connector, rule.get().getFilter(any, provision), provision));
+            } else {
+                MappingUtils.getConnObjectKeyItem(provision).ifPresent(connObjectKeyItem
+                        -> matchByConnObjectKeyValue(connector, connObjectKeyItem, connObjectKeyValue, provision).
+                                ifPresent(result::add));
+            }
+        } catch (RuntimeException e) {
+            LOG.error("Could not match {} with any existing {}", any, provision.getObjectClass(), e);
+        }
+
+        if (any != null && result.size() == 1) {
+            virAttrHandler.setValues(any, result.get(0));
+        }
+
+        return result;
+    }
+
+    @Transactional(readOnly = true)
+    public List<ConnectorObject> match(
+            final Connector connector,
+            final Any<?> any,
+            final Provision provision,
+            final LinkingMappingItem... linkingItems) {
+
+        Optional<PushCorrelationRule> rule = rule(provision);
+
+        List<ConnectorObject> result = new ArrayList<>();
+        try {
+            if (rule.isPresent()) {
+                result.addAll(matchByCorrelationRule(
+                        connector, rule.get().getFilter(any, provision), provision, linkingItems));
+            } else {
+                Optional<? extends MappingItem> connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision);
+                Optional<String> connObjectKeyValue = mappingManager.getConnObjectKeyValue(any, provision);
+
+                if (connObjectKeyItem.isPresent() && connObjectKeyValue.isPresent()) {
+                    matchByConnObjectKeyValue(
+                            connector, connObjectKeyItem.get(), connObjectKeyValue.get(), provision, linkingItems).
+                            ifPresent(result::add);
+                }
+            }
+        } catch (RuntimeException e) {
+            LOG.error("Could not match {} with any existing {}", any, provision.getObjectClass(), e);
+        }
+
+        if (result.size() == 1) {
+            virAttrHandler.setValues(any, result.get(0));
+        }
+
+        return result;
+    }
+
+    private List<ConnectorObject> matchByCorrelationRule(
+            final Connector connector,
+            final Filter filter,
+            final Provision provision,
+            final LinkingMappingItem... linkingItems) {
+
+        Stream<MappingItem> items = Stream.concat(
+                provision.getMapping().getItems().stream(),
+                ArrayUtils.isEmpty(linkingItems)
+                ? virSchemaDAO.findByProvision(provision).stream().map(VirSchema::asLinkingMappingItem)
+                : Stream.of(linkingItems));
+
+        List<ConnectorObject> objs = new ArrayList<>();
+        try {
+            connector.search(provision.getObjectClass(), filter, new SearchResultsHandler() {
+
+                @Override
+                public void handleResult(final SearchResult result) {
+                    // nothing to do
+                }
+
+                @Override
+                public boolean handle(final ConnectorObject connectorObject) {
+                    objs.add(connectorObject);
+                    return true;
+                }
+            }, MappingUtils.buildOperationOptions(items));
+        } catch (TimeoutException toe) {
+            LOG.debug("Request timeout", toe);
+            throw toe;
+        } catch (RuntimeException ignore) {
+            LOG.debug("Unexpected exception", ignore);
+        }
+
+        return objs;
+    }
+
+    @Transactional(readOnly = true)
+    public Optional<ConnectorObject> matchByConnObjectKeyValue(
+            final Connector connector,
+            final MappingItem connObjectKeyItem,
+            final String connObjectKeyValue,
+            final Provision provision,
+            final LinkingMappingItem... linkingItems) {
+
+        Stream<MappingItem> items = Stream.concat(
+                provision.getMapping().getItems().stream(),
+                ArrayUtils.isEmpty(linkingItems)
+                ? virSchemaDAO.findByProvision(provision).stream().map(VirSchema::asLinkingMappingItem)
+                : Stream.of(linkingItems));
+
+        ConnectorObject obj = null;
+        try {
+            obj = connector.getObject(
+                    provision.getObjectClass(),
+                    AttributeBuilder.build(connObjectKeyItem.getExtAttrName(), connObjectKeyValue),
+                    provision.isIgnoreCaseMatch(),
+                    MappingUtils.buildOperationOptions(items));
+        } catch (TimeoutException toe) {
+            LOG.debug("Request timeout", toe);
+            throw toe;
+        } catch (RuntimeException ignore) {
+            LOG.debug("While resolving {}", connObjectKeyValue, ignore);
+        }
+
+        return Optional.ofNullable(obj);
+    }
+}
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PullJobDelegate.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PullJobDelegate.java
index 14de90b..3f87d55 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PullJobDelegate.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PullJobDelegate.java
@@ -20,60 +20,50 @@ package org.apache.syncope.core.provisioning.java.pushpull;
 
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.tuple.MutablePair;
-import org.apache.syncope.common.lib.collections.IteratorChain;
-import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.ConflictResolutionAction;
+import org.apache.commons.lang3.tuple.MutablePair;
 import org.apache.syncope.common.lib.types.ResourceOperation;
+import org.apache.syncope.core.spring.ApplicationContextProvider;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
-import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.dao.PullMatch;
 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.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.VirSchema;
 import org.apache.syncope.core.persistence.api.entity.group.Group;
-import org.apache.syncope.core.persistence.api.entity.resource.MappingItem;
+import org.apache.syncope.core.persistence.api.entity.resource.Item;
 import org.apache.syncope.core.persistence.api.entity.resource.OrgUnit;
 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.Connector;
-import org.apache.syncope.core.provisioning.api.pushpull.AnyObjectPullResultHandler;
-import org.apache.syncope.core.provisioning.api.pushpull.GroupPullResultHandler;
 import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningProfile;
+import org.quartz.JobExecutionException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.support.AbstractBeanDefinition;
+import org.apache.syncope.core.persistence.api.entity.task.PullTask;
+import org.apache.syncope.core.persistence.api.entity.user.User;
+import org.apache.syncope.core.provisioning.api.pushpull.AnyObjectPullResultHandler;
 import org.apache.syncope.core.provisioning.api.pushpull.PullActions;
+import org.apache.syncope.core.provisioning.api.pushpull.GroupPullResultHandler;
 import org.apache.syncope.core.provisioning.api.pushpull.RealmPullResultHandler;
-import org.apache.syncope.core.provisioning.api.pushpull.ReconFilterBuilder;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopePullExecutor;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopePullResultHandler;
 import org.apache.syncope.core.provisioning.api.pushpull.UserPullResultHandler;
 import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
-import org.apache.syncope.core.spring.ApplicationContextProvider;
-import org.apache.syncope.core.spring.ImplementationManager;
 import org.identityconnectors.framework.common.objects.Name;
 import org.identityconnectors.framework.common.objects.ObjectClass;
 import org.identityconnectors.framework.common.objects.OperationOptions;
 import org.identityconnectors.framework.common.objects.SyncToken;
-import org.quartz.JobExecutionException;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.support.AbstractBeanDefinition;
+import org.apache.syncope.core.provisioning.api.pushpull.ReconFilterBuilder;
+import org.apache.syncope.core.spring.ImplementationManager;
 
 public class PullJobDelegate extends AbstractProvisioningJobDelegate<PullTask> implements SyncopePullExecutor {
 
-    protected final Map<ObjectClass, SyncToken> latestSyncTokens = new HashMap<>();
-
-    protected final Map<ObjectClass, MutablePair<Integer, String>> handled = new HashMap<>();
-
-    @Autowired
-    protected UserDAO userDAO;
-
     @Autowired
     protected GroupDAO groupDAO;
 
@@ -81,11 +71,15 @@ public class PullJobDelegate extends AbstractProvisioningJobDelegate<PullTask> i
     protected VirSchemaDAO virSchemaDAO;
 
     @Autowired
-    protected PullUtils pullUtils;
+    protected InboundMatcher inboundMatcher;
 
     @Autowired
     protected AnyUtilsFactory anyUtilsFactory;
 
+    protected final Map<ObjectClass, SyncToken> latestSyncTokens = new HashMap<>();
+
+    protected final Map<ObjectClass, MutablePair<Integer, String>> handled = new HashMap<>();
+
     protected ProvisioningProfile<PullTask, PullActions> profile;
 
     @Override
@@ -129,40 +123,35 @@ public class PullJobDelegate extends AbstractProvisioningJobDelegate<PullTask> i
         return status.get();
     }
 
-    protected void setGroupOwners(
-            final GroupPullResultHandler ghandler,
-            final boolean userIgnoreCaseMatch,
-            final boolean groupIgnoreCaseMatch) {
-
-        ghandler.getGroupOwnerMap().entrySet().stream().map(entry -> {
-            Group group = groupDAO.find(entry.getKey());
+    protected void setGroupOwners(final GroupPullResultHandler ghandler) {
+        ghandler.getGroupOwnerMap().forEach((groupKey, ownerKey) -> {
+            Group group = groupDAO.find(groupKey);
             if (group == null) {
-                throw new NotFoundException("Group " + entry.getKey());
+                throw new NotFoundException("Group " + groupKey);
             }
-            if (StringUtils.isBlank(entry.getValue())) {
+            if (StringUtils.isBlank(ownerKey)) {
                 group.setGroupOwner(null);
                 group.setUserOwner(null);
             } else {
-                Optional<String> userKey = pullUtils.match(
+                Optional<PullMatch> match = inboundMatcher.match(
                         anyTypeDAO.findUser(),
-                        entry.getValue(),
+                        ownerKey,
                         ghandler.getProfile().getTask().getResource(),
-                        ghandler.getProfile().getConnector(),
-                        userIgnoreCaseMatch);
-                if (userKey.isPresent()) {
-                    group.setUserOwner(userDAO.find(userKey.get()));
+                        ghandler.getProfile().getConnector());
+                if (match.isPresent()) {
+                    group.setUserOwner((User) match.get().getAny());
                 } else {
-                    pullUtils.match(
+                    inboundMatcher.match(
                             anyTypeDAO.findGroup(),
-                            entry.getValue(),
+                            ownerKey,
                             ghandler.getProfile().getTask().getResource(),
-                            ghandler.getProfile().getConnector(),
-                            groupIgnoreCaseMatch).
-                            ifPresent(groupKey -> group.setGroupOwner(groupDAO.find(groupKey)));
+                            ghandler.getProfile().getConnector()).
+                            ifPresent(groupMatch -> group.setGroupOwner((Group) groupMatch.getAny()));
                 }
             }
-            return group;
-        }).forEachOrdered(group -> groupDAO.save(group));
+
+            groupDAO.save(group);
+        });
     }
 
     protected static RealmPullResultHandler buildRealmHandler() {
@@ -229,7 +218,7 @@ public class PullJobDelegate extends AbstractProvisioningJobDelegate<PullTask> i
 
             OrgUnit orgUnit = pullTask.getResource().getOrgUnit();
             OperationOptions options = MappingUtils.buildOperationOptions(
-                    MappingUtils.getPullItems(orgUnit.getItems()).iterator());
+                    MappingUtils.getPullItems(orgUnit.getItems().stream()));
 
             RealmPullResultHandler handler = buildRealmHandler();
             handler.setProfile(profile);
@@ -278,16 +267,8 @@ public class PullJobDelegate extends AbstractProvisioningJobDelegate<PullTask> i
         // ...then provisions for any types
         SyncopePullResultHandler handler;
         GroupPullResultHandler ghandler = buildGroupHandler();
-        boolean userIgnoreCaseMatch = false;
-        boolean groupIgnoreCaseMatch = false;
         for (Provision provision : pullTask.getResource().getProvisions()) {
             if (provision.getMapping() != null) {
-                if (provision.getAnyType().getKind() == AnyTypeKind.USER) {
-                    userIgnoreCaseMatch = provision.isIgnoreCaseMatch();
-                } else if (provision.getAnyType().getKind() == AnyTypeKind.GROUP) {
-                    groupIgnoreCaseMatch = provision.isIgnoreCaseMatch();
-                }
-
                 status.set("Pulling " + provision.getObjectClass().getObjectClassValue());
 
                 switch (provision.getAnyType().getKind()) {
@@ -307,11 +288,9 @@ public class PullJobDelegate extends AbstractProvisioningJobDelegate<PullTask> i
                 handler.setPullExecutor(this);
 
                 try {
-                    Set<MappingItem> linkingMappingItems = virSchemaDAO.findByProvision(provision).stream().
-                            map(VirSchema::asLinkingMappingItem).collect(Collectors.toSet());
-                    Iterator<MappingItem> mapItems = new IteratorChain<>(
-                            provision.getMapping().getItems().iterator(),
-                            linkingMappingItems.iterator());
+                    Stream<? extends Item> mapItems = Stream.concat(
+                            MappingUtils.getPullItems(provision.getMapping().getItems().stream()),
+                            virSchemaDAO.findByProvision(provision).stream().map(VirSchema::asLinkingMappingItem));
                     OperationOptions options = MappingUtils.buildOperationOptions(mapItems);
 
                     switch (pullTask.getPullMode()) {
@@ -333,17 +312,17 @@ public class PullJobDelegate extends AbstractProvisioningJobDelegate<PullTask> i
                             break;
 
                         case FILTERED_RECONCILIATION:
-                            ReconFilterBuilder filterBuilder =
-                                    ImplementationManager.build(pullTask.getReconFilterBuilder());
-                            connector.filteredReconciliation(provision.getObjectClass(),
-                                    filterBuilder,
+                            connector.filteredReconciliation(
+                                    provision.getObjectClass(),
+                                    ImplementationManager.build(pullTask.getReconFilterBuilder()),
                                     handler,
                                     options);
                             break;
 
                         case FULL_RECONCILIATION:
                         default:
-                            connector.fullReconciliation(provision.getObjectClass(),
+                            connector.fullReconciliation(
+                                    provision.getObjectClass(),
                                     handler,
                                     options);
                             break;
@@ -365,7 +344,7 @@ public class PullJobDelegate extends AbstractProvisioningJobDelegate<PullTask> i
             }
         }
         try {
-            setGroupOwners(ghandler, userIgnoreCaseMatch, groupIgnoreCaseMatch);
+            setGroupOwners(ghandler);
         } catch (Exception e) {
             LOG.error("While setting group owners", e);
         }
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
deleted file mode 100644
index 6db67f1..0000000
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PushUtils.java
+++ /dev/null
@@ -1,139 +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.pushpull;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-import org.apache.syncope.core.persistence.api.dao.PushCorrelationRule;
-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.provisioning.api.Connector;
-import org.apache.syncope.core.provisioning.api.MappingManager;
-import org.apache.syncope.core.provisioning.api.TimeoutException;
-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.SearchResult;
-import org.identityconnectors.framework.spi.SearchResultsHandler;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.transaction.annotation.Transactional;
-
-@Transactional(readOnly = true)
-@Component
-public class PushUtils {
-
-    private static final Logger LOG = LoggerFactory.getLogger(PushUtils.class);
-
-    @Autowired
-    private MappingManager mappingManager;
-
-    public List<ConnectorObject> match(
-            final Connector connector,
-            final Any<?> any,
-            final Provision provision) {
-
-        Optional<? extends PushCorrelationRuleEntity> correlationRule = provision.getResource().getPushPolicy() == null
-                ? Optional.empty()
-                : provision.getResource().getPushPolicy().getCorrelationRule(provision.getAnyType());
-
-        Optional<PushCorrelationRule> rule = Optional.empty();
-        if (correlationRule.isPresent()) {
-            try {
-                rule = ImplementationManager.buildPushCorrelationRule(correlationRule.get().getImplementation());
-            } catch (Exception e) {
-                LOG.error("While building {}", correlationRule.get().getImplementation(), e);
-            }
-        }
-
-        try {
-            return rule
-                .map(pushCorrelationRule -> findByCorrelationRule(connector, any, provision, pushCorrelationRule))
-                .orElseGet(() -> findByConnObjectKey(connector, any, provision));
-        } catch (RuntimeException e) {
-            LOG.error("Could not match {} with any existing {}", any, provision.getObjectClass(), e);
-            return List.of();
-        }
-    }
-
-    private static List<ConnectorObject> findByCorrelationRule(
-        final Connector connector,
-        final Any<?> any,
-        final Provision provision,
-        final PushCorrelationRule rule) {
-
-        List<ConnectorObject> objs = new ArrayList<>();
-
-        try {
-            connector.search(provision.getObjectClass(), rule.getFilter(any, provision), new SearchResultsHandler() {
-
-                @Override
-                public void handleResult(final SearchResult result) {
-                    // nothing to do
-                }
-
-                @Override
-                public boolean handle(final ConnectorObject connectorObject) {
-                    objs.add(connectorObject);
-                    return true;
-                }
-            }, MappingUtils.buildOperationOptions(provision.getMapping().getItems().iterator()));
-        } catch (TimeoutException toe) {
-            LOG.debug("Request timeout", toe);
-            throw toe;
-        } catch (RuntimeException ignore) {
-            LOG.debug("Unexpected exception", ignore);
-        }
-
-        return objs;
-    }
-
-    public List<ConnectorObject> findByConnObjectKey(
-            final Connector connector,
-            final Any<?> any,
-            final Provision provision) {
-
-        Optional<? extends MappingItem> connObjectKey = MappingUtils.getConnObjectKeyItem(provision);
-        Optional<String> connObjectKeyValue = mappingManager.getConnObjectKeyValue(any, provision);
-
-        ConnectorObject obj = null;
-        if (connObjectKey.isPresent() && connObjectKeyValue.isPresent()) {
-            try {
-                obj = connector.getObject(
-                        provision.getObjectClass(),
-                        AttributeBuilder.build(connObjectKey.get().getExtAttrName(), connObjectKeyValue.get()),
-                        provision.isIgnoreCaseMatch(),
-                        MappingUtils.buildOperationOptions(provision.getMapping().getItems().iterator()));
-            } catch (TimeoutException toe) {
-                LOG.debug("Request timeout", toe);
-                throw toe;
-            } catch (RuntimeException ignore) {
-                LOG.debug("While resolving {}", connObjectKeyValue.get(), ignore);
-            }
-        }
-
-        return obj == null ? List.of() : List.of(obj);
-    }
-}
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/SinglePullJobDelegate.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/SinglePullJobDelegate.java
index 02919c8..154fe5f 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/SinglePullJobDelegate.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/SinglePullJobDelegate.java
@@ -19,12 +19,8 @@
 package org.apache.syncope.core.provisioning.java.pushpull;
 
 import java.util.ArrayList;
-import java.util.Iterator;
 import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import org.apache.syncope.common.lib.collections.IteratorChain;
+import java.util.stream.Stream;
 import org.apache.syncope.common.lib.to.PullTaskTO;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.lib.types.ConflictResolutionAction;
@@ -33,11 +29,11 @@ import org.apache.syncope.common.lib.types.MatchingRule;
 import org.apache.syncope.common.lib.types.PullMode;
 import org.apache.syncope.common.lib.types.UnmatchingRule;
 import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.entity.Implementation;
 import org.apache.syncope.core.persistence.api.entity.AnyType;
-import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.entity.VirSchema;
-import org.apache.syncope.core.persistence.api.entity.resource.MappingItem;
+import org.apache.syncope.core.persistence.api.entity.resource.Item;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
 import org.apache.syncope.core.persistence.api.entity.task.AnyTemplatePullTask;
 import org.apache.syncope.core.persistence.api.entity.task.PullTask;
@@ -53,7 +49,6 @@ import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
 import org.apache.syncope.core.spring.ImplementationManager;
 import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
 import org.identityconnectors.framework.common.objects.AttributeBuilder;
-import org.identityconnectors.framework.common.objects.OperationOptions;
 import org.identityconnectors.framework.common.objects.filter.Filter;
 import org.identityconnectors.framework.common.objects.filter.FilterBuilder;
 import org.quartz.JobExecutionException;
@@ -67,7 +62,7 @@ public class SinglePullJobDelegate extends PullJobDelegate implements SyncopeSin
     private ImplementationDAO implementationDAO;
 
     @Autowired
-    private TemplateUtils templateUtils;
+    private RealmDAO realmDAO;
 
     @Override
     public List<ProvisioningReport> pull(
@@ -75,7 +70,6 @@ public class SinglePullJobDelegate extends PullJobDelegate implements SyncopeSin
             final Connector connector,
             final String connObjectKey,
             final String connObjectValue,
-            final Realm realm,
             final PullTaskTO pullTaskTO) throws JobExecutionException {
 
         LOG.debug("Executing pull on {}", provision.getResource());
@@ -95,13 +89,6 @@ public class SinglePullJobDelegate extends PullJobDelegate implements SyncopeSin
         });
 
         try {
-            Set<MappingItem> linkinMappingItems = virSchemaDAO.findByProvision(provision).stream().
-                    map(VirSchema::asLinkingMappingItem).collect(Collectors.toSet());
-            Iterator<MappingItem> mapItems = new IteratorChain<>(
-                    provision.getMapping().getItems().iterator(),
-                    linkinMappingItems.iterator());
-            OperationOptions options = MappingUtils.buildOperationOptions(mapItems);
-
             PullTask pullTask = entityFactory.newEntity(PullTask.class);
             pullTask.setResource(provision.getResource());
             pullTask.setMatchingRule(pullTaskTO.getMatchingRule() == null
@@ -113,7 +100,7 @@ public class SinglePullJobDelegate extends PullJobDelegate implements SyncopeSin
             pullTask.setPerformUpdate(pullTaskTO.isPerformUpdate());
             pullTask.setPerformDelete(pullTaskTO.isPerformDelete());
             pullTask.setSyncStatus(pullTaskTO.isSyncStatus());
-            pullTask.setDestinationRealm(realm);
+            pullTask.setDestinationRealm(realmDAO.findByFullPath(pullTaskTO.getDestinationRealm()));
             pullTask.setRemediation(pullTaskTO.isRemediation());
             // validate JEXL expressions from templates and proceed if fine
             TemplateUtils.check(pullTaskTO.getTemplates(), ClientExceptionType.InvalidPullTask);
@@ -162,18 +149,17 @@ public class SinglePullJobDelegate extends PullJobDelegate implements SyncopeSin
             handler.setPullExecutor(this);
 
             // execute filtered pull
+            Stream<? extends Item> mapItems = Stream.concat(
+                    MappingUtils.getPullItems(provision.getMapping().getItems().stream()),
+                    virSchemaDAO.findByProvision(provision).stream().map(VirSchema::asLinkingMappingItem));
             connector.filteredReconciliation(
                     provision.getObjectClass(),
-                new AccountReconciliationFilterBuilder(connObjectKey, connObjectValue),
+                    new AccountReconciliationFilterBuilder(connObjectKey, connObjectValue),
                     handler,
-                    options);
+                    MappingUtils.buildOperationOptions(mapItems));
 
-            Optional<? extends Provision> userProvision = provision.getResource().getProvision(anyTypeDAO.findUser());
-            boolean userIgnoreCaseMatch = userProvision.map(Provision::isIgnoreCaseMatch).orElse(false);
-            Optional<? extends Provision> groupProvision = provision.getResource().getProvision(anyTypeDAO.findGroup());
-            boolean groupIgnoreCaseMatch = groupProvision.map(Provision::isIgnoreCaseMatch).orElse(false);
             try {
-                setGroupOwners(ghandler, userIgnoreCaseMatch, groupIgnoreCaseMatch);
+                setGroupOwners(ghandler);
             } catch (Exception e) {
                 LOG.error("While setting group owners", e);
             }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/SinglePushJobDelegate.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/SinglePushJobDelegate.java
index 8bfbc0d..4889311 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/SinglePushJobDelegate.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/SinglePushJobDelegate.java
@@ -30,12 +30,14 @@ import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.Implementation;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
 import org.apache.syncope.core.persistence.api.entity.task.PushTask;
+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.pushpull.ProvisioningProfile;
 import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
 import org.apache.syncope.core.provisioning.api.pushpull.PushActions;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopePushResultHandler;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopeSinglePushExecutor;
+import org.apache.syncope.core.provisioning.api.pushpull.UserPushResultHandler;
 import org.apache.syncope.core.spring.ImplementationManager;
 import org.quartz.JobExecutionException;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -47,11 +49,9 @@ public class SinglePushJobDelegate extends PushJobDelegate implements SyncopeSin
     @Autowired
     private ImplementationDAO implementationDAO;
 
-    @Override
-    public List<ProvisioningReport> push(
+    private List<PushActions> before(
             final Provision provision,
             final Connector connector,
-            final Any<?> any,
             final PushTaskTO pushTaskTO) throws JobExecutionException {
 
         LOG.debug("Executing push on {}", provision.getResource());
@@ -70,25 +70,37 @@ public class SinglePushJobDelegate extends PushJobDelegate implements SyncopeSin
             }
         });
 
-        try {
-            PushTask pushTask = entityFactory.newEntity(PushTask.class);
-            pushTask.setResource(provision.getResource());
-            pushTask.setMatchingRule(pushTaskTO.getMatchingRule() == null
-                    ? MatchingRule.LINK : pushTaskTO.getMatchingRule());
-            pushTask.setUnmatchingRule(pushTaskTO.getUnmatchingRule() == null
-                    ? UnmatchingRule.ASSIGN : pushTaskTO.getUnmatchingRule());
-            pushTask.setPerformCreate(pushTaskTO.isPerformCreate());
-            pushTask.setPerformUpdate(pushTaskTO.isPerformUpdate());
-            pushTask.setPerformDelete(pushTaskTO.isPerformDelete());
-            pushTask.setSyncStatus(pushTaskTO.isSyncStatus());
-
-            profile = new ProvisioningProfile<>(connector, pushTask);
-            profile.getActions().addAll(actions);
-            profile.setConflictResolutionAction(ConflictResolutionAction.FIRSTMATCH);
+        PushTask pushTask = entityFactory.newEntity(PushTask.class);
+        pushTask.setResource(provision.getResource());
+        pushTask.setMatchingRule(pushTaskTO.getMatchingRule() == null
+                ? MatchingRule.LINK : pushTaskTO.getMatchingRule());
+        pushTask.setUnmatchingRule(pushTaskTO.getUnmatchingRule() == null
+                ? UnmatchingRule.ASSIGN : pushTaskTO.getUnmatchingRule());
+        pushTask.setPerformCreate(pushTaskTO.isPerformCreate());
+        pushTask.setPerformUpdate(pushTaskTO.isPerformUpdate());
+        pushTask.setPerformDelete(pushTaskTO.isPerformDelete());
+        pushTask.setSyncStatus(pushTaskTO.isSyncStatus());
 
-            for (PushActions action : actions) {
-                action.beforeAll(profile);
-            }
+        profile = new ProvisioningProfile<>(connector, pushTask);
+        profile.getActions().addAll(actions);
+        profile.setConflictResolutionAction(ConflictResolutionAction.FIRSTMATCH);
+
+        for (PushActions action : actions) {
+            action.beforeAll(profile);
+        }
+
+        return actions;
+    }
+
+    @Override
+    public List<ProvisioningReport> push(
+            final Provision provision,
+            final Connector connector,
+            final Any<?> any,
+            final PushTaskTO pushTaskTO) throws JobExecutionException {
+
+        try {
+            List<PushActions> actions = before(provision, connector, pushTaskTO);
 
             SyncopePushResultHandler handler;
             switch (provision.getAnyType().getKind()) {
@@ -106,7 +118,7 @@ public class SinglePushJobDelegate extends PushJobDelegate implements SyncopeSin
             }
             handler.setProfile(profile);
 
-            doHandle(List.of(any), handler, pushTask.getResource());
+            doHandle(List.of(any), handler, provision.getResource());
 
             for (PushActions action : actions) {
                 action.afterAll(profile);
@@ -119,4 +131,31 @@ public class SinglePushJobDelegate extends PushJobDelegate implements SyncopeSin
                     : new JobExecutionException("While pushing to connector", e);
         }
     }
+
+    @Override
+    public ProvisioningReport push(
+            final Provision provision,
+            final Connector connector,
+            final LinkedAccount account,
+            final PushTaskTO pushTaskTO) throws JobExecutionException {
+
+        try {
+            List<PushActions> actions = before(provision, connector, pushTaskTO);
+
+            UserPushResultHandler handler = buildUserHandler();
+            handler.setProfile(profile);
+
+            handler.handle(account, provision);
+
+            for (PushActions action : actions) {
+                action.afterAll(profile);
+            }
+
+            return profile.getResults().get(0);
+        } catch (Exception e) {
+            throw e instanceof JobExecutionException
+                    ? (JobExecutionException) e
+                    : new JobExecutionException("While pushing to connector", e);
+        }
+    }
 }
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 fda08a0..3162765 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
@@ -41,9 +41,9 @@ import org.apache.syncope.common.lib.to.RealmTO;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
-import org.apache.syncope.core.persistence.api.entity.AnyUtils;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
+import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.entity.policy.PasswordPolicy;
 import org.apache.syncope.core.persistence.api.entity.resource.OrgUnit;
@@ -90,6 +90,9 @@ public class ConnObjectUtils {
     @Autowired
     private MappingManager mappingManager;
 
+    @Autowired
+    private AnyUtilsFactory anyUtilsFactory;
+
     /**
      * Extract password value from passed value (if instance of GuardedString or GuardedByteArray).
      *
@@ -113,6 +116,7 @@ public class ConnObjectUtils {
     }
 
     /**
+     * <<<<<<< HEAD
      * Builds {@link ConnObjectTO} out of {@link ConnectorObject}.
      *
      * @param connObject connector object.
@@ -124,6 +128,9 @@ public class ConnObjectUtils {
     }
 
     /**
+     * =======
+     * >>>>>>> 1b918568e... [SYNCOPE-1500] Reconciliation now supports single pull / push + [SYNCOPE-957] Reconciliation
+     * now supports Linked Accounts + [SYNCOPE-1499] Use Push correlation rule wherever it makes sense
      * Builds {@link ConnObjectTO} out of a collection of {@link Attribute} instances.
      *
      * @param attrs attributes
@@ -160,7 +167,6 @@ public class ConnObjectUtils {
      * @param obj connector object
      * @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 <C> create request type
@@ -171,11 +177,10 @@ public class ConnObjectUtils {
             final ConnectorObject obj,
             final PullTask pullTask,
             final Provision provision,
-            final AnyUtils anyUtils,
             final boolean generatePasswordIfPossible) {
 
-        AnyTO anyTO = getAnyTOFromConnObject(obj, pullTask, provision, anyUtils);
-        C anyCR = anyUtils.newAnyCR();
+        AnyTO anyTO = getAnyTOFromConnObject(obj, pullTask, provision);
+        C anyCR = anyUtilsFactory.getInstance(provision.getAnyType().getKind()).newAnyCR();
         EntityTOUtils.toAnyCR(anyTO, anyCR);
 
         // (for users) if password was not set above, generate if resource is configured for that
@@ -212,6 +217,15 @@ public class ConnObjectUtils {
         return anyCR;
     }
 
+    public RealmTO getRealmTO(final ConnectorObject obj, final PullTask task, final OrgUnit orgUnit) {
+        RealmTO realmTO = new RealmTO();
+
+        MappingUtils.getPullItems(orgUnit.getItems().stream()).forEach(item
+                -> mappingManager.setIntValues(item, obj.getAttributeByName(item.getExtAttrName()), realmTO));
+
+        return realmTO;
+    }
+
     /**
      * Build {@link AnyUR} out of connector object attributes and schema mapping.
      *
@@ -220,7 +234,6 @@ public class ConnObjectUtils {
      * @param original any object to get diff from
      * @param pullTask pull task
      * @param provision provision information
-     * @param anyUtils utils
      * @param <U> any object
      * @return modifications for the any object to be updated
      */
@@ -231,14 +244,13 @@ public class ConnObjectUtils {
             final ConnectorObject obj,
             final AnyTO original,
             final PullTask pullTask,
-            final Provision provision,
-            final AnyUtils anyUtils) {
+            final Provision provision) {
 
-        AnyTO updated = getAnyTOFromConnObject(obj, pullTask, provision, anyUtils);
+        AnyTO updated = getAnyTOFromConnObject(obj, pullTask, provision);
         updated.setKey(key);
 
         U anyUR = null;
-        switch (anyUtils.anyTypeKind()) {
+        switch (provision.getAnyType().getKind()) {
             case USER:
                 UserTO originalUser = (UserTO) original;
                 UserTO updatedUser = (UserTO) updated;
@@ -303,17 +315,14 @@ public class ConnObjectUtils {
     }
 
     private <T extends AnyTO> T getAnyTOFromConnObject(
-            final ConnectorObject obj,
-            final PullTask pullTask,
-            final Provision provision,
-            final AnyUtils anyUtils) {
+            final ConnectorObject obj, final PullTask pullTask, final Provision provision) {
 
-        T anyTO = anyUtils.newAnyTO();
+        T anyTO = anyUtilsFactory.getInstance(provision.getAnyType().getKind()).newAnyTO();
         anyTO.setType(provision.getAnyType().getKey());
 
         // 1. fill with data from connector object
         anyTO.setRealm(pullTask.getDestinationRealm().getFullPath());
-        MappingUtils.getPullItems(provision.getMapping().getItems()).forEach(
+        MappingUtils.getPullItems(provision.getMapping().getItems().stream()).forEach(
                 item -> mappingManager.setIntValues(item, obj.getAttributeByName(item.getExtAttrName()), anyTO));
 
         // 2. add data from defined template (if any)
@@ -321,13 +330,4 @@ public class ConnObjectUtils {
 
         return anyTO;
     }
-
-    public RealmTO getRealmTO(final ConnectorObject obj, final PullTask task, final OrgUnit orgUnit) {
-        RealmTO realmTO = new RealmTO();
-
-        MappingUtils.getPullItems(orgUnit.getItems()).forEach(
-                item -> mappingManager.setIntValues(item, obj.getAttributeByName(item.getExtAttrName()), realmTO));
-
-        return realmTO;
-    }
 }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/MappingUtils.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/MappingUtils.java
index 0c3f7d8..c425d6e 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/MappingUtils.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/MappingUtils.java
@@ -20,11 +20,10 @@ package org.apache.syncope.core.provisioning.java.utils;
 
 import java.util.ArrayList;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.apache.commons.jexl3.JexlContext;
 import org.apache.commons.jexl3.MapContext;
 import org.apache.commons.lang3.StringUtils;
@@ -66,16 +65,14 @@ public final class MappingUtils {
                 : mapping.getConnObjectKeyItem();
     }
 
-    public static List<? extends Item> getPropagationItems(final List<? extends Item> items) {
-        return items.stream().
-                filter(item -> item.getPurpose() == MappingPurpose.PROPAGATION
-                || item.getPurpose() == MappingPurpose.BOTH).collect(Collectors.toList());
+    public static Stream<? extends Item> getPropagationItems(final Stream<? extends Item> items) {
+        return items.filter(
+                item -> item.getPurpose() == MappingPurpose.PROPAGATION || item.getPurpose() == MappingPurpose.BOTH);
     }
 
-    public static List<? extends Item> getPullItems(final List<? extends Item> items) {
-        return items.stream().
-                filter(item -> item.getPurpose() == MappingPurpose.PULL
-                || item.getPurpose() == MappingPurpose.BOTH).collect(Collectors.toList());
+    public static Stream<? extends Item> getPullItems(final Stream<? extends Item> items) {
+        return items.filter(
+                item -> item.getPurpose() == MappingPurpose.PULL || item.getPurpose() == MappingPurpose.BOTH);
     }
 
     private static Name getName(final String evalConnObjectLink, final String connObjectKey) {
@@ -84,14 +81,14 @@ public final class MappingUtils {
         Name name;
         if (StringUtils.isBlank(evalConnObjectLink)) {
             // add connObjectKey as __NAME__ attribute ...
-            LOG.debug("Add connObjectKey [{}] as __NAME__", connObjectKey);
+            LOG.debug("Add connObjectKey [{}] as {}", connObjectKey, Name.NAME);
             name = new Name(connObjectKey);
         } else {
-            LOG.debug("Add connObjectLink [{}] as __NAME__", evalConnObjectLink);
+            LOG.debug("Add connObjectLink [{}] as {}", evalConnObjectLink, Name.NAME);
             name = new Name(evalConnObjectLink);
 
             // connObjectKey not propagated: it will be used to set the value for __UID__ attribute
-            LOG.debug("connObjectKey will be used just as __UID__ attribute");
+            LOG.debug("connObjectKey will be used just as {} attribute", Uid.NAME);
         }
 
         return name;
@@ -187,11 +184,11 @@ public final class MappingUtils {
     /**
      * Build options for requesting all mapped connector attributes.
      *
-     * @param iterator items
+     * @param items items
      * @return options for requesting all mapped connector attributes
      * @see OperationOptions
      */
-    public static OperationOptions buildOperationOptions(final Iterator<? extends Item> iterator) {
+    public static OperationOptions buildOperationOptions(final Stream<? extends Item> items) {
         OperationOptionsBuilder builder = new OperationOptionsBuilder();
 
         Set<String> attrsToGet = new HashSet<>();
@@ -199,12 +196,8 @@ public final class MappingUtils {
         attrsToGet.add(Uid.NAME);
         attrsToGet.add(OperationalAttributes.ENABLE_NAME);
 
-        while (iterator.hasNext()) {
-            Item item = iterator.next();
-            if (item.getPurpose() != MappingPurpose.NONE) {
-                attrsToGet.add(item.getExtAttrName());
-            }
-        }
+        items.filter(item -> item.getPurpose() != MappingPurpose.NONE).
+                forEach(item -> attrsToGet.add(item.getExtAttrName()));
 
         builder.setAttributesToGet(attrsToGet);
         // -------------------------------------
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
index fed103d..0e628e9 100644
--- 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
@@ -19,15 +19,19 @@
 package org.apache.syncope.fit.core.reference;
 
 import java.util.Optional;
+import org.apache.syncope.common.lib.types.MatchType;
 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.UserDAO;
 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.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 
 @PullCorrelationRuleConfClass(LinkedAccountSamplePullCorrelationRuleConf.class)
@@ -35,6 +39,9 @@ public class LinkedAccountSamplePullCorrelationRule implements PullCorrelationRu
 
     public static final String VIVALDI_KEY = "b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee";
 
+    @Autowired
+    private UserDAO userDAO;
+
     @Override
     public SearchCond getSearchCond(final SyncDelta syncDelta, final Provision provision) {
         AttributeCond cond = new AttributeCond();
@@ -51,6 +58,7 @@ public class LinkedAccountSamplePullCorrelationRule implements PullCorrelationRu
         return SearchCond.getLeafCond(cond);
     }
 
+    @Transactional(readOnly = true)
     @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
@@ -60,19 +68,16 @@ public class LinkedAccountSamplePullCorrelationRule implements PullCorrelationRu
                 && 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 new PullMatch(MatchType.LINKED_ACCOUNT, any);
         }
 
         return PullCorrelationRule.super.matching(any, syncDelta, provision);
     }
 
+    @Transactional(readOnly = true)
     @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());
+        return Optional.of(new PullMatch(MatchType.LINKED_ACCOUNT, userDAO.find(VIVALDI_KEY)));
     }
 }
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 9d3e9bd..be5f0cb 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
@@ -198,6 +198,8 @@ public abstract class AbstractITCase {
 
     protected static final String RESOURCE_LDAP_ADMIN_PWD = "secret";
 
+    protected static final String PRINTER = "PRINTER";
+
     protected static String ANONYMOUS_UNAME;
 
     protected static String ANONYMOUS_KEY;
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AnyObjectITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AnyObjectITCase.java
index 68427f3..7eb3d5d 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AnyObjectITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AnyObjectITCase.java
@@ -51,7 +51,7 @@ import org.junit.jupiter.api.Test;
 public class AnyObjectITCase extends AbstractITCase {
 
     public static AnyObjectCR getSample(final String location) {
-        return new AnyObjectCR.Builder(SyncopeConstants.ROOT_REALM, "PRINTER", location + getUUIDString()).
+        return new AnyObjectCR.Builder(SyncopeConstants.ROOT_REALM, PRINTER, location + getUUIDString()).
                 plainAttr(attr("location", location + getUUIDString())).
                 resource(RESOURCE_NAME_DBSCRIPTED).
                 build();
@@ -122,7 +122,7 @@ public class AnyObjectITCase extends AbstractITCase {
     public void list() {
         PagedResult<AnyObjectTO> anyObjectTOs = anyObjectService.search(
                 new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).
-                        fiql(SyncopeClient.getAnyObjectSearchConditionBuilder("PRINTER").query()).
+                        fiql(SyncopeClient.getAnyObjectSearchConditionBuilder(PRINTER).query()).
                         build());
         assertNotNull(anyObjectTOs);
         assertTrue(anyObjectTOs.getResult().size() >= 2);
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AnyTypeITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AnyTypeITCase.java
index e5eb88d..33162df 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AnyTypeITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AnyTypeITCase.java
@@ -52,10 +52,10 @@ public class AnyTypeITCase extends AbstractITCase {
         assertEquals(AnyTypeKind.GROUP.name(), groupType.getKey());
         assertFalse(groupType.getClasses().isEmpty());
 
-        AnyTypeTO otherType = anyTypeService.read("PRINTER");
+        AnyTypeTO otherType = anyTypeService.read(PRINTER);
         assertNotNull(otherType);
         assertEquals(AnyTypeKind.ANY_OBJECT, otherType.getKind());
-        assertEquals("PRINTER", otherType.getKey());
+        assertEquals(PRINTER, otherType.getKey());
     }
 
     @Test
@@ -137,7 +137,7 @@ public class AnyTypeITCase extends AbstractITCase {
         newClass = getObject(response.getLocation(), AnyTypeClassService.class, AnyTypeClassTO.class);
         assertNotNull(newClass);
 
-        AnyTypeTO other = anyTypeService.read("PRINTER");
+        AnyTypeTO other = anyTypeService.read(PRINTER);
         assertNotNull(other);
 
         other.getClasses().add(newClass.getKey());
@@ -182,7 +182,7 @@ public class AnyTypeITCase extends AbstractITCase {
     @Test
     public void issueSYNCOPE1472() {
         // 1. add any type class csv twice to PRINTER any type
-        AnyTypeTO anyTypeTO = anyTypeService.read("PRINTER");
+        AnyTypeTO anyTypeTO = anyTypeService.read(PRINTER);
         anyTypeTO.getClasses().clear();
         anyTypeTO.getClasses().add("minimal printer");
         anyTypeTO.getClasses().add("csv");
@@ -190,11 +190,11 @@ public class AnyTypeITCase extends AbstractITCase {
         anyTypeService.update(anyTypeTO);
 
         // 2. read again and remove any type class
-        anyTypeTO = anyTypeService.read("PRINTER");
+        anyTypeTO = anyTypeService.read(PRINTER);
         anyTypeTO.getClasses().remove("csv");
         anyTypeService.update(anyTypeTO);
 
-        assertFalse(anyTypeService.read("PRINTER").getClasses().contains("csv"), 
+        assertFalse(anyTypeService.read(PRINTER).getClasses().contains("csv"), 
                 "Should not contain removed any type classes");
     }
 }
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GroupITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GroupITCase.java
index 0e60efe..fbebb21 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GroupITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GroupITCase.java
@@ -249,7 +249,7 @@ public class GroupITCase extends AbstractITCase {
         GroupCR createReq = getBasicSample("patch");
         createReq.setUDynMembershipCond("(($groups==3;$resources!=ws-target-resource-1);aLong==1)");
         createReq.getADynMembershipConds().put(
-                "PRINTER",
+                PRINTER,
                 "(($groups==7;cool==ss);$resources==ws-target-resource-2);$type==PRINTER");
 
         GroupTO created = createGroup(createReq).getEntity();
@@ -649,17 +649,17 @@ public class GroupITCase extends AbstractITCase {
 
     @Test
     public void aDynMembership() {
-        String fiql = SyncopeClient.getAnyObjectSearchConditionBuilder("PRINTER").is("location").notNullValue().query();
+        String fiql = SyncopeClient.getAnyObjectSearchConditionBuilder(PRINTER).is("location").notNullValue().query();
 
         // 1. create group with a given aDynMembership condition
         GroupCR groupCR = getBasicSample("aDynMembership");
-        groupCR.getADynMembershipConds().put("PRINTER", fiql);
+        groupCR.getADynMembershipConds().put(PRINTER, fiql);
         GroupTO group = createGroup(groupCR).getEntity();
-        assertEquals(fiql, group.getADynMembershipConds().get("PRINTER"));
+        assertEquals(fiql, group.getADynMembershipConds().get(PRINTER));
 
         group = groupService.read(group.getKey());
         final String groupKey = group.getKey();
-        assertEquals(fiql, group.getADynMembershipConds().get("PRINTER"));
+        assertEquals(fiql, group.getADynMembershipConds().get(PRINTER));
 
         // verify that the condition is dynamically applied
         AnyObjectCR newAnyCR = AnyObjectITCase.getSample("aDynMembership");
@@ -678,17 +678,17 @@ public class GroupITCase extends AbstractITCase {
         assertTrue(memberships.stream().anyMatch(m -> m.getGroupKey().equals(groupKey)));
 
         // 2. update group and change aDynMembership condition
-        fiql = SyncopeClient.getAnyObjectSearchConditionBuilder("PRINTER").is("location").nullValue().query();
+        fiql = SyncopeClient.getAnyObjectSearchConditionBuilder(PRINTER).is("location").nullValue().query();
 
         GroupUR groupUR = new GroupUR();
         groupUR.setKey(group.getKey());
-        groupUR.getADynMembershipConds().put("PRINTER", fiql);
+        groupUR.getADynMembershipConds().put(PRINTER, fiql);
 
         group = updateGroup(groupUR).getEntity();
-        assertEquals(fiql, group.getADynMembershipConds().get("PRINTER"));
+        assertEquals(fiql, group.getADynMembershipConds().get(PRINTER));
 
         group = groupService.read(group.getKey());
-        assertEquals(fiql, group.getADynMembershipConds().get("PRINTER"));
+        assertEquals(fiql, group.getADynMembershipConds().get(PRINTER));
 
         // verify that the condition is dynamically applied
         AnyObjectUR anyObjectUR = new AnyObjectUR();
@@ -713,14 +713,14 @@ public class GroupITCase extends AbstractITCase {
     public void aDynMembershipCount() {
         // Create a new printer as a dynamic member of a new group
         GroupCR groupCR = getBasicSample("aDynamicMembership");
-        String fiql = SyncopeClient.getAnyObjectSearchConditionBuilder("PRINTER").is("location").equalTo("home").query();
-        groupCR.getADynMembershipConds().put("PRINTER", fiql);
+        String fiql = SyncopeClient.getAnyObjectSearchConditionBuilder(PRINTER).is("location").equalTo("home").query();
+        groupCR.getADynMembershipConds().put(PRINTER, fiql);
         GroupTO group = createGroup(groupCR).getEntity();
 
         AnyObjectCR printerCR = new AnyObjectCR();
         printerCR.setRealm(SyncopeConstants.ROOT_REALM);
         printerCR.setName("Printer_" + getUUIDString());
-        printerCR.setType("PRINTER");
+        printerCR.setType(PRINTER);
         printerCR.getPlainAttrs().add(new Attr.Builder("location").value("home").build());
         AnyObjectTO printer = createAnyObject(printerCR).getEntity();
 
@@ -741,7 +741,7 @@ public class GroupITCase extends AbstractITCase {
         AnyObjectCR printerCR = new AnyObjectCR();
         printerCR.setRealm(SyncopeConstants.ROOT_REALM);
         printerCR.setName("Printer_" + getUUIDString());
-        printerCR.setType("PRINTER");
+        printerCR.setType(PRINTER);
         printerCR.getMemberships().add(new MembershipTO.Builder(group.getKey()).build());
         AnyObjectTO printer = createAnyObject(printerCR).getEntity();
 
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LoggerITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LoggerITCase.java
index 36613bd..4d18370 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LoggerITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LoggerITCase.java
@@ -57,6 +57,7 @@ import org.apache.syncope.common.lib.types.MatchingRule;
 import org.apache.syncope.common.lib.types.ResourceOperation;
 import org.apache.syncope.common.lib.types.UnmatchingRule;
 import org.apache.syncope.common.rest.api.LoggerWrapper;
+import org.apache.syncope.common.rest.api.beans.ReconQuery;
 import org.apache.syncope.core.logic.ConnectorLogic;
 import org.apache.syncope.core.logic.ReportLogic;
 import org.apache.syncope.core.logic.ResourceLogic;
@@ -421,8 +422,8 @@ public class LoggerITCase extends AbstractITCase {
             pushTask.setPerformUpdate(true);
             pushTask.setUnmatchingRule(UnmatchingRule.PROVISION);
             pushTask.setMatchingRule(MatchingRule.UPDATE);
-            reconciliationService.push(
-                    AnyTypeKind.ANY_OBJECT, "fc6dbc3a-6c07-4965-8781-921e7401a4a5", RESOURCE_NAME_DBSCRIPTED, pushTask);
+            reconciliationService.push(new ReconQuery.Builder(PRINTER, RESOURCE_NAME_DBSCRIPTED).
+                    anyKey("fc6dbc3a-6c07-4965-8781-921e7401a4a5").build(), pushTask);
         } catch (Exception e) {
             LOG.error("Unexpected exception", e);
             fail(e::getMessage);
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java
index 3a8566b..022247d 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java
@@ -58,6 +58,7 @@ import org.apache.syncope.common.lib.types.PullMode;
 import org.apache.syncope.common.lib.types.SchemaType;
 import org.apache.syncope.common.lib.types.TaskType;
 import org.apache.syncope.common.rest.api.beans.AnyQuery;
+import org.apache.syncope.common.rest.api.beans.ReconQuery;
 import org.apache.syncope.common.rest.api.beans.SchemaQuery;
 import org.apache.syncope.common.rest.api.beans.TaskQuery;
 import org.apache.syncope.common.rest.api.service.ConnectorService;
@@ -240,7 +241,8 @@ public class MultitenancyITCase extends AbstractITCase {
             pushTask.setPerformUpdate(true);
             pushTask.setMatchingRule(MatchingRule.UPDATE);
             adminClient.getService(ReconciliationService.class).
-                    push(AnyTypeKind.USER, pullFromLDAPKey, resource.getKey(), pushTask);
+                    push(new ReconQuery.Builder(AnyTypeKind.USER.name(), resource.getKey()).
+                            anyKey(pullFromLDAPKey).build(), pushTask);
 
             assertEquals(1, adminClient.getService(TaskService.class).
                     search(new TaskQuery.Builder(TaskType.PROPAGATION).
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PropagationTaskITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PropagationTaskITCase.java
index 0dd9dfb..95475fd 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PropagationTaskITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PropagationTaskITCase.java
@@ -187,7 +187,7 @@ public class PropagationTaskITCase extends AbstractTaskITCase {
         // 0. Set propagation JEXL MappingItemTransformer
         ResourceTO resource = resourceService.read(RESOURCE_NAME_DBSCRIPTED);
         ResourceTO originalResource = SerializationUtils.clone(resource);
-        ProvisionTO provision = resource.getProvision("PRINTER").get();
+        ProvisionTO provision = resource.getProvision(PRINTER).get();
         assertNotNull(provision);
 
         Optional<ItemTO> mappingItem = provision.getMapping().getItems().stream().
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 85bd8f3..bfa5636 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
@@ -479,7 +479,7 @@ public class PullTaskITCase extends AbstractTaskITCase {
         // 0. reset sync token and set MappingItemTransformer
         ResourceTO resource = resourceService.read(RESOURCE_NAME_DBSCRIPTED);
         ResourceTO originalResource = SerializationUtils.clone(resource);
-        ProvisionTO provision = resource.getProvision("PRINTER").get();
+        ProvisionTO provision = resource.getProvision(PRINTER).get();
         assertNotNull(provision);
 
         ImplementationTO transformer = null;
@@ -537,7 +537,7 @@ public class PullTaskITCase extends AbstractTaskITCase {
             // 3. unlink any existing printer and delete from Syncope (printer is now only on external resource)
             PagedResult<AnyObjectTO> matchingPrinters = anyObjectService.search(
                     new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).
-                            fiql(SyncopeClient.getAnyObjectSearchConditionBuilder("PRINTER").
+                            fiql(SyncopeClient.getAnyObjectSearchConditionBuilder(PRINTER).
                                     is("location").equalTo("pull*").query()).build());
             assertTrue(matchingPrinters.getSize() > 0);
             for (AnyObjectTO printer : matchingPrinters.getResult()) {
@@ -557,7 +557,7 @@ public class PullTaskITCase extends AbstractTaskITCase {
             // 5. verify that printer was re-created in Syncope (implies that location does not start with given prefix,
             // hence PrefixItemTransformer was applied during pull)
             matchingPrinters = anyObjectService.search(new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).
-                    fiql(SyncopeClient.getAnyObjectSearchConditionBuilder("PRINTER").
+                    fiql(SyncopeClient.getAnyObjectSearchConditionBuilder(PRINTER).
                             is("location").equalTo("pull*").query()).build());
             assertTrue(matchingPrinters.getSize() > 0);
 
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PushTaskITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PushTaskITCase.java
index 5c1f83e..96347d5 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PushTaskITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PushTaskITCase.java
@@ -56,6 +56,7 @@ import org.apache.syncope.common.lib.types.SchemaType;
 import org.apache.syncope.common.lib.types.TaskType;
 import org.apache.syncope.common.lib.types.TraceLevel;
 import org.apache.syncope.common.lib.types.UnmatchingRule;
+import org.apache.syncope.common.rest.api.beans.ReconQuery;
 import org.apache.syncope.common.rest.api.beans.TaskQuery;
 import org.apache.syncope.common.rest.api.service.NotificationService;
 import org.apache.syncope.common.rest.api.service.ResourceService;
@@ -271,46 +272,54 @@ public class PushTaskITCase extends AbstractTaskITCase {
         ResourceTO ldap = resourceService.read(RESOURCE_NAME_LDAP);
         assertNull(ldap.getPushPolicy());
 
-        ldap.setPushPolicy("fb6530e5-892d-4f47-a46b-180c5b6c5c83");
-        resourceService.update(ldap);
-
-        // 2. create push task with sole scope as the user 'vivaldi'
-        PushTaskTO sendVivaldi = new PushTaskTO();
-        sendVivaldi.setName("Send Vivaldi");
-        sendVivaldi.setResource(RESOURCE_NAME_LDAP);
-        sendVivaldi.setUnmatchingRule(UnmatchingRule.PROVISION);
-        sendVivaldi.setMatchingRule(MatchingRule.UPDATE);
-        sendVivaldi.setSourceRealm(SyncopeConstants.ROOT_REALM);
-        sendVivaldi.getFilters().put(AnyTypeKind.GROUP.name(), "name==$null");
-        sendVivaldi.getFilters().put(AnyTypeKind.USER.name(), "username==vivaldi");
-        sendVivaldi.setPerformCreate(true);
-        sendVivaldi.setPerformUpdate(true);
-
-        Response response = taskService.create(TaskType.PUSH, sendVivaldi);
-        sendVivaldi = getObject(response.getLocation(), TaskService.class, PushTaskTO.class);
-        assertNotNull(sendVivaldi);
-
-        // 3. execute push: vivaldi is found on ldap
-        execProvisioningTask(taskService, TaskType.PUSH, sendVivaldi.getKey(), 50, false);
-
-        ReconStatus status = reconciliationService.status(AnyTypeKind.USER, "vivaldi", RESOURCE_NAME_LDAP);
-        assertNotNull(status.getOnResource());
-
-        // 4. update vivaldi on ldap: reconciliation status does not find it anymore, as remote key was changed
-        Map<String, String> attrs = new HashMap<>();
-        attrs.put("cn", "vivaldiZZ");
-        attrs.put("mail", "vivaldi@syncope.org");
-        updateLdapRemoteObject(RESOURCE_LDAP_ADMIN_DN, RESOURCE_LDAP_ADMIN_PWD, "uid=vivaldi,ou=People,o=isp", attrs);
-
-        status = reconciliationService.status(AnyTypeKind.USER, "vivaldi", RESOURCE_NAME_LDAP);
-        assertNull(status.getOnResource());
-
-        // 5. execute push again: the push policy will find anyway vivaldi because of the email attribute
-        execProvisioningTask(taskService, TaskType.PUSH, sendVivaldi.getKey(), 50, false);
-
-        // 6. now the reconciliation status is fine again, as the push above did overwrite the entry on ldap
-        status = reconciliationService.status(AnyTypeKind.USER, "vivaldi", RESOURCE_NAME_LDAP);
-        assertNotNull(status.getOnResource());
+        try {
+            ldap.setPushPolicy("fb6530e5-892d-4f47-a46b-180c5b6c5c83");
+            resourceService.update(ldap);
+
+            // 2. create push task with sole scope as the user 'vivaldi'
+            PushTaskTO sendVivaldi = new PushTaskTO();
+            sendVivaldi.setName("Send Vivaldi");
+            sendVivaldi.setResource(RESOURCE_NAME_LDAP);
+            sendVivaldi.setUnmatchingRule(UnmatchingRule.PROVISION);
+            sendVivaldi.setMatchingRule(MatchingRule.UPDATE);
+            sendVivaldi.setSourceRealm(SyncopeConstants.ROOT_REALM);
+            sendVivaldi.getFilters().put(AnyTypeKind.GROUP.name(), "name==$null");
+            sendVivaldi.getFilters().put(AnyTypeKind.USER.name(), "username==vivaldi");
+            sendVivaldi.setPerformCreate(true);
+            sendVivaldi.setPerformUpdate(true);
+
+            Response response = taskService.create(TaskType.PUSH, sendVivaldi);
+            sendVivaldi = getObject(response.getLocation(), TaskService.class, PushTaskTO.class);
+            assertNotNull(sendVivaldi);
+
+            // 3. execute push: vivaldi is found on ldap
+            execProvisioningTask(taskService, TaskType.PUSH, sendVivaldi.getKey(), 50, false);
+
+            ReconStatus status = reconciliationService.status(
+                    new ReconQuery.Builder(AnyTypeKind.USER.name(), RESOURCE_NAME_LDAP).anyKey("vivaldi").build());
+            assertNotNull(status.getOnResource());
+
+            // 4. update vivaldi on ldap: reconciliation status does not find it anymore, as remote key was changed
+            Map<String, String> attrs = new HashMap<>();
+            attrs.put("sn", "VivaldiZ");
+            updateLdapRemoteObject(
+                    RESOURCE_LDAP_ADMIN_DN, RESOURCE_LDAP_ADMIN_PWD, "uid=vivaldi,ou=People,o=isp", attrs);
+
+            status = reconciliationService.status(
+                    new ReconQuery.Builder(AnyTypeKind.USER.name(), RESOURCE_NAME_LDAP).anyKey("vivaldi").build());
+            assertNull(status.getOnResource());
+
+            // 5. execute push again: propagation task for CREATE will be generated, but that will fail
+            // as task executor is not able any more to identify the entry to UPDATE
+            execProvisioningTask(taskService, TaskType.PUSH, sendVivaldi.getKey(), 50, false);
+
+            status = reconciliationService.status(
+                    new ReconQuery.Builder(AnyTypeKind.USER.name(), RESOURCE_NAME_LDAP).anyKey("vivaldi").build());
+            assertNull(status.getOnResource());
+        } finally {
+            ldap.setPushPolicy(null);
+            resourceService.update(ldap);
+        }
     }
 
     @Test
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ReconciliationITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ReconciliationITCase.java
index 720255e..1635f02 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ReconciliationITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ReconciliationITCase.java
@@ -26,15 +26,20 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.Date;
 import org.apache.syncope.common.lib.request.AnyObjectCR;
+import java.util.UUID;
+import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.to.AnyObjectTO;
 import org.apache.syncope.common.lib.Attr;
 import org.apache.syncope.common.lib.to.PullTaskTO;
 import org.apache.syncope.common.lib.to.PushTaskTO;
 import org.apache.syncope.common.lib.to.ReconStatus;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.lib.types.MatchType;
 import org.apache.syncope.common.lib.types.UnmatchingRule;
+import org.apache.syncope.common.rest.api.beans.ReconQuery;
 import org.apache.syncope.fit.AbstractITCase;
 import org.identityconnectors.framework.common.objects.OperationalAttributes;
+import org.identityconnectors.framework.common.objects.Uid;
 import org.junit.jupiter.api.Test;
 import org.springframework.jdbc.core.JdbcTemplate;
 
@@ -54,9 +59,12 @@ public class ReconciliationITCase extends AbstractITCase {
                 "SELECT id FROM testPRINTER WHERE printername=?", printer.getName()).size());
 
         // 3. verify reconciliation status
-        ReconStatus status =
-                reconciliationService.status(AnyTypeKind.ANY_OBJECT, printer.getName(), "resource-db-scripted");
+        ReconStatus status = reconciliationService.status(
+                new ReconQuery.Builder(PRINTER, RESOURCE_NAME_DBSCRIPTED).anyKey(printer.getName()).build());
         assertNotNull(status);
+        assertEquals(AnyTypeKind.ANY_OBJECT, status.getAnyTypeKind());
+        assertEquals(printer.getKey(), status.getAnyKey());
+        assertEquals(MatchType.ANY, status.getMatchType());
         assertNotNull(status.getOnSyncope());
         assertNull(status.getOnResource());
 
@@ -64,7 +72,8 @@ public class ReconciliationITCase extends AbstractITCase {
         PushTaskTO pushTask = new PushTaskTO();
         pushTask.setPerformCreate(true);
         pushTask.setUnmatchingRule(UnmatchingRule.PROVISION);
-        reconciliationService.push(AnyTypeKind.ANY_OBJECT, printer.getKey(), "resource-db-scripted", pushTask);
+        reconciliationService.push(new ReconQuery.Builder(PRINTER, RESOURCE_NAME_DBSCRIPTED).
+                anyKey(printer.getKey()).build(), pushTask);
 
         // 5. verify that printer is now propagated
         assertEquals(1, jdbcTemplate.queryForList(
@@ -75,7 +84,8 @@ public class ReconciliationITCase extends AbstractITCase {
         assertTrue(printer.getResources().isEmpty());
 
         // 7. verify reconciliation status
-        status = reconciliationService.status(AnyTypeKind.ANY_OBJECT, printer.getName(), "resource-db-scripted");
+        status = reconciliationService.status(
+                new ReconQuery.Builder(PRINTER, RESOURCE_NAME_DBSCRIPTED).anyKey(printer.getName()).build());
         assertNotNull(status);
         assertNotNull(status.getOnSyncope());
         assertNotNull(status.getOnResource());
@@ -97,15 +107,15 @@ public class ReconciliationITCase extends AbstractITCase {
         assertNotNull(printer.getKey());
         assertNotEquals("Nowhere", printer.getPlainAttr("location").get().getValues().get(0));
 
-        // 2. create table into the external resource's db, with same name
+        // 2. add row into the external resource's table, with same name
         JdbcTemplate jdbcTemplate = new JdbcTemplate(testDataSource);
         jdbcTemplate.update(
                 "INSERT INTO TESTPRINTER (id, printername, location, deleted, lastmodification) VALUES (?,?,?,?,?)",
                 printer.getKey(), printer.getName(), "Nowhere", false, new Date());
 
         // 3. verify reconciliation status
-        ReconStatus status =
-                reconciliationService.status(AnyTypeKind.ANY_OBJECT, printer.getName(), "resource-db-scripted");
+        ReconStatus status = reconciliationService.status(
+                new ReconQuery.Builder(PRINTER, RESOURCE_NAME_DBSCRIPTED).anyKey(printer.getName()).build());
         assertNotNull(status);
         assertNotNull(status.getOnSyncope());
         assertNotNull(status.getOnResource());
@@ -113,12 +123,49 @@ public class ReconciliationITCase extends AbstractITCase {
 
         // 4. pull
         PullTaskTO pullTask = new PullTaskTO();
+        pullTask.setDestinationRealm(SyncopeConstants.ROOT_REALM);
         pullTask.setPerformUpdate(true);
-        reconciliationService.pull(AnyTypeKind.ANY_OBJECT, printer.getName(), "resource-db-scripted", pullTask);
+        reconciliationService.pull(new ReconQuery.Builder(PRINTER, RESOURCE_NAME_DBSCRIPTED).
+                anyKey(printer.getName()).build(), pullTask);
 
         // 5. verify reconciliation result (and resource is still not assigned)
         printer = anyObjectService.read(printer.getKey());
         assertEquals("Nowhere", printer.getPlainAttr("location").get().getValues().get(0));
         assertTrue(printer.getResources().isEmpty());
     }
+
+    @Test
+    public void importSingle() {
+        // 1. add row into the external resource's table
+        String externalKey = UUID.randomUUID().toString();
+        String externalName = "printer" + getUUIDString();
+
+        JdbcTemplate jdbcTemplate = new JdbcTemplate(testDataSource);
+        jdbcTemplate.update(
+                "INSERT INTO TESTPRINTER (id, printername, location, deleted, lastmodification) VALUES (?,?,?,?,?)",
+                externalKey, externalName, "Nowhere", false, new Date());
+
+        // 2. verify reconciliation status
+        ReconStatus status = reconciliationService.status(
+                new ReconQuery.Builder(PRINTER, RESOURCE_NAME_DBSCRIPTED).connObjectKeyValue(externalKey).build());
+        assertNotNull(status);
+        assertNull(status.getAnyTypeKind());
+        assertNull(status.getAnyKey());
+        assertNull(status.getMatchType());
+        assertNull(status.getOnSyncope());
+        assertNotNull(status.getOnResource());
+        assertEquals(externalKey, status.getOnResource().getAttr(Uid.NAME).get().getValues().get(0));
+        assertEquals(externalName, status.getOnResource().getAttr("PRINTERNAME").get().getValues().get(0));
+
+        // 3. pull
+        PullTaskTO pullTask = new PullTaskTO();
+        pullTask.setDestinationRealm(SyncopeConstants.ROOT_REALM);
+        pullTask.setPerformCreate(true);
+        reconciliationService.pull(new ReconQuery.Builder(PRINTER, RESOURCE_NAME_DBSCRIPTED).
+                connObjectKeyValue(externalKey).build(), pullTask);
+
+        // 4. verify reconciliation result
+        AnyObjectTO printer = anyObjectService.read(externalName);
+        assertNotNull(printer);
+    }
 }
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ResourceITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ResourceITCase.java
index d1d8826..7cdc784 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ResourceITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ResourceITCase.java
@@ -408,22 +408,22 @@ public class ResourceITCase extends AbstractITCase {
         try {
             // create a new resource
             resource = createResource(resource);
-            assertNull(resource.getProvision("PRINTER").get().getSyncToken());
+            assertNull(resource.getProvision(PRINTER).get().getSyncToken());
 
             // create some object on the new resource
             anyObject = createAnyObject(anyObjectCR).getEntity();
 
             // update sync token
-            resourceService.setLatestSyncToken(resource.getKey(), "PRINTER");
+            resourceService.setLatestSyncToken(resource.getKey(), PRINTER);
 
             resource = resourceService.read(resource.getKey());
-            assertNotNull(resource.getProvision("PRINTER").get().getSyncToken());
+            assertNotNull(resource.getProvision(PRINTER).get().getSyncToken());
 
             // remove sync token
-            resourceService.removeSyncToken(resource.getKey(), "PRINTER");
+            resourceService.removeSyncToken(resource.getKey(), PRINTER);
 
             resource = resourceService.read(resource.getKey());
-            assertNull(resource.getProvision("PRINTER").get().getSyncToken());
+            assertNull(resource.getProvision(PRINTER).get().getSyncToken());
         } finally {
             if (anyObject != null) {
                 anyObjectService.delete(anyObject.getKey());
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
index 347e0b7..2a5d64f 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
@@ -337,7 +337,7 @@ public class SearchITCase extends AbstractITCase {
     public void searchByType() {
         PagedResult<AnyObjectTO> matching = anyObjectService.search(new AnyQuery.Builder().realm(
                 SyncopeConstants.ROOT_REALM).
-                fiql(SyncopeClient.getAnyObjectSearchConditionBuilder("PRINTER").query()).build());
+                fiql(SyncopeClient.getAnyObjectSearchConditionBuilder(PRINTER).query()).build());
         assertNotNull(matching);
 
         assertFalse(matching.getResult().isEmpty());
@@ -356,7 +356,7 @@ public class SearchITCase extends AbstractITCase {
     public void searchByRelationship() {
         PagedResult<AnyObjectTO> anyObjects = anyObjectService.search(new AnyQuery.Builder().realm(
                 SyncopeConstants.ROOT_REALM).
-                fiql(SyncopeClient.getAnyObjectSearchConditionBuilder("PRINTER").
+                fiql(SyncopeClient.getAnyObjectSearchConditionBuilder(PRINTER).
                         inRelationships("Canon MF 8030cn").query()).
                 build());
         assertNotNull(anyObjects);
@@ -375,7 +375,7 @@ public class SearchITCase extends AbstractITCase {
     public void searchByRelationshipType() {
         PagedResult<AnyObjectTO> anyObjects = anyObjectService.search(new AnyQuery.Builder().realm(
                 SyncopeConstants.ROOT_REALM).
-                fiql(SyncopeClient.getAnyObjectSearchConditionBuilder("PRINTER").
+                fiql(SyncopeClient.getAnyObjectSearchConditionBuilder(PRINTER).
                         inRelationshipTypes("neighborhood").query()).
                 build());
         assertNotNull(anyObjects);
@@ -423,7 +423,7 @@ public class SearchITCase extends AbstractITCase {
                 anyMatch(group -> "e7ff94e8-19c9-4f0a-b8b7-28327edbf6ed".equals(group.getKey())));
 
         PagedResult<AnyObjectTO> anyObjects = anyObjectService.search(new AnyQuery.Builder().realm("/odd").
-                fiql(SyncopeClient.getAnyObjectSearchConditionBuilder("PRINTER").isAssignable().
+                fiql(SyncopeClient.getAnyObjectSearchConditionBuilder(PRINTER).isAssignable().
                         and("name").equalTo("*").query()).
                 build());
         assertNotNull(anyObjects);