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 2021/01/25 16:56:42 UTC

[syncope] branch master updated: [SYNCOPE-1610] Refactoring ReconQuery to work with FIQL (#234)

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 dd1c82a  [SYNCOPE-1610] Refactoring ReconQuery to work with FIQL (#234)
dd1c82a is described below

commit dd1c82adc6cfd340f198c6123971bff27a826562
Author: Francesco Chicchiriccò <il...@users.noreply.github.com>
AuthorDate: Mon Jan 25 17:12:17 2021 +0100

    [SYNCOPE-1610] Refactoring ReconQuery to work with FIQL (#234)
---
 .../console/panels/ConnObjectListViewPanel.java    |  60 ++--
 .../syncope/client/console/panels/ConnObjects.java |   6 +-
 .../console/panels/LinkedAccountModalPanel.java    |   5 +-
 .../client/console/status/ReconStatusUtils.java    |   5 +-
 .../client/console/status/ReconTaskPanel.java      |   7 +-
 .../any/MergeLinkedAccountsWizardBuilder.java      |  78 +++--
 .../client/console/SyncopeConsoleSession.java      |  70 +++-
 .../console/SyncopeConsoleApplicationTest.java     |   8 +-
 .../syncope/common/rest/api/beans/ReconQuery.java  |  16 +-
 .../rest/api/service/ReconciliationService.java    |   6 +-
 .../apache/syncope/common/lib/to/ConnObjectTO.java |  12 +
 .../syncope/core/logic/ReconciliationLogic.java    | 352 +++++++++++++--------
 .../apache/syncope/core/logic/ResourceLogic.java   |  25 +-
 .../cxf/service/ReconciliationServiceImpl.java     |  97 ++++--
 .../persistence/api/dao/PushCorrelationRule.java   |  26 +-
 .../jpa/dao/DefaultPushCorrelationRule.java        |  61 +++-
 .../api/propagation/PropagationReporter.java       |   2 +
 ...ecutor.java => ConstantReconFilterBuilder.java} |  26 +-
 ...ecutor.java => KeyValueReconFilterBuilder.java} |  31 +-
 .../api/pushpull/SyncopeSinglePullExecutor.java    |   3 +-
 .../java/data/AbstractAnyDataBinder.java           |  44 ++-
 .../AbstractPropagationTaskExecutor.java           |   9 +
 .../propagation/DefaultPropagationReporter.java    |   5 +-
 .../PriorityPropagationTaskExecutor.java           |   2 +-
 .../java/pushpull/OutboundMatcher.java             |   6 +
 .../java/pushpull/SinglePullJobDelegate.java       |  25 +-
 .../provisioning/java/utils/ConnObjectUtils.java   |  19 +-
 .../syncope/fit/core/PropagationTaskITCase.java    |   9 +-
 .../syncope/fit/core/ReconciliationITCase.java     |  10 +-
 29 files changed, 680 insertions(+), 345 deletions(-)

diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ConnObjectListViewPanel.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ConnObjectListViewPanel.java
index 3a39acb..afe3278 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ConnObjectListViewPanel.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ConnObjectListViewPanel.java
@@ -27,6 +27,8 @@ import java.util.List;
 import java.util.Optional;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.client.console.SyncopeConsoleSession;
+import org.apache.syncope.client.console.pages.BasePage;
 import org.apache.syncope.client.console.panels.ListViewPanel.ListViewReload;
 import org.apache.syncope.client.console.panels.search.AbstractSearchPanel;
 import org.apache.syncope.client.console.panels.search.ConnObjectSearchPanel;
@@ -70,11 +72,15 @@ import org.apache.wicket.markup.html.panel.Panel;
 import org.apache.wicket.model.Model;
 import org.apache.wicket.model.StringResourceModel;
 import org.apache.wicket.model.util.ListModel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public abstract class ConnObjectListViewPanel extends Panel {
 
     private static final long serialVersionUID = 4986172040062752781L;
 
+    private static final Logger LOG = LoggerFactory.getLogger(ConnObjectListViewPanel.class);
+
     private static final int SIZE = 10;
 
     private static final String STATUS = "Status";
@@ -139,9 +145,9 @@ public abstract class ConnObjectListViewPanel extends Panel {
         accordion.setOutputMarkupId(true);
         add(accordion.setEnabled(true).setVisible(true));
 
-        final List<ConnObjectTO> listOfItems = reloadItems(resource.getKey(), anyType, null, null);
+        List<ConnObjectTO> listOfItems = reloadItems(resource.getKey(), anyType, null, null);
 
-        final ListViewPanel.Builder<ConnObjectTO> builder = new ListViewPanel.Builder<ConnObjectTO>(
+        ListViewPanel.Builder<ConnObjectTO> builder = new ListViewPanel.Builder<ConnObjectTO>(
                 ConnObjectTO.class, pageRef) {
 
             private static final long serialVersionUID = -8251750413385566738L;
@@ -149,15 +155,21 @@ public abstract class ConnObjectListViewPanel extends Panel {
             @Override
             protected Component getValueComponent(final String key, final ConnObjectTO bean) {
                 if (StringUtils.equals(key, STATUS)) {
-                    ReconStatus status = ReconciliationRestClient.status(
-                            new ReconQuery.Builder(anyType, resource.getKey()).
-                                    connObjectKeyValue(bean.getAttr(ConnIdSpecialName.UID).get().getValues().get(0)).
-                                    build());
+                    ReconStatus status;
+                    try {
+                        status = ReconciliationRestClient.status(
+                                new ReconQuery.Builder(anyType, resource.getKey()).fiql(bean.getFiql()).build());
+                    } catch (Exception e) {
+                        LOG.error("While requesting for reconciliation status of {} {} with FIQL '{}'",
+                                anyType, resource.getKey(), bean.getFiql(), e);
+
+                        status = new ReconStatus();
+                    }
 
                     return status.getOnSyncope() == null
                             ? StatusUtils.getLabel("field", "notfound icon", "Not found", Constants.NOT_FOUND_ICON)
                             : new Label("field", Model.of()).add(new PopoverBehavior(
-                                    Model.<String>of(),
+                                    Model.of(),
                                     Model.of(status.getAnyKey()),
                                     new PopoverConfig().
                                             withTitle(status.getMatchType() == MatchType.LINKED_ACCOUNT
@@ -214,19 +226,25 @@ public abstract class ConnObjectListViewPanel extends Panel {
 
                 @Override
                 public void onClick(final AjaxRequestTarget target, final ConnObjectTO modelObject) {
-                    String connObjectKeyValue = modelObject.getAttr(ConnIdSpecialName.UID).get().getValues().get(0);
-                    ReconStatus status = ReconciliationRestClient.status(
-                            new ReconQuery.Builder(anyType, resource.getKey()).
-                                    connObjectKeyValue(connObjectKeyValue).build());
-
-                    pullConnObject(
-                            connObjectKeyValue,
-                            target,
-                            resource.getKey(),
-                            anyType,
-                            status.getRealm(),
-                            StringUtils.isNotBlank(status.getAnyKey()),
-                            pageRef);
+                    try {
+                        ReconStatus status = ReconciliationRestClient.status(
+                                new ReconQuery.Builder(anyType, resource.getKey()).fiql(modelObject.getFiql()).build());
+
+                        pullConnObject(
+                                modelObject.getFiql(),
+                                target,
+                                resource.getKey(),
+                                anyType,
+                                status.getRealm(),
+                                StringUtils.isNotBlank(status.getAnyKey()),
+                                pageRef);
+                    } catch (Exception e) {
+                        LOG.error("While puling single object {} {} with FIQL '{}'",
+                                anyType, resource.getKey(), modelObject.getFiql(), e);
+
+                        SyncopeConsoleSession.get().onException(e);
+                        ((BasePage) pageRef.getPage()).getNotificationPanel().refresh(target);
+                    }
                 }
             }, ActionLink.ActionType.RECONCILIATION_PULL, IdRepoEntitlement.TASK_EXECUTE);
 
@@ -272,7 +290,7 @@ public abstract class ConnObjectListViewPanel extends Panel {
     protected abstract void viewConnObject(ConnObjectTO connObjectTO, AjaxRequestTarget target);
 
     protected abstract void pullConnObject(
-            String connObjectTO,
+            String fiql,
             AjaxRequestTarget target,
             String resource,
             String anyType,
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ConnObjects.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ConnObjects.java
index 47350fb..4af0ea9 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ConnObjects.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ConnObjects.java
@@ -128,13 +128,14 @@ public class ConnObjects extends Panel implements ModalPanel {
 
         @Override
         protected void pullConnObject(
-                final String connObjectKeyValue,
+                final String fiql,
                 final AjaxRequestTarget target,
                 final String resource,
                 final String anyType,
                 final String realm,
                 final boolean isOnSyncope,
                 final PageReference pageRef) {
+
             anyTypes.setEnabled(false);
             target.add(anyTypes);
 
@@ -146,13 +147,12 @@ public class ConnObjects extends Panel implements ModalPanel {
                             pullTaskTO,
                             anyType,
                             null,
-                            connObjectKeyValue,
+                            fiql,
                             isOnSyncope,
                             connObjects,
                             pageRef),
                     target
             );
         }
-
     }
 }
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/LinkedAccountModalPanel.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/LinkedAccountModalPanel.java
index 83f2db2..3e1b62b 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/LinkedAccountModalPanel.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/LinkedAccountModalPanel.java
@@ -39,6 +39,7 @@ import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
 import org.apache.syncope.client.console.wicket.markup.html.form.ActionLinksTogglePanel;
 import org.apache.syncope.client.console.wizards.WizardMgtPanel;
 import org.apache.syncope.client.console.wizards.any.LinkedAccountWizardBuilder;
+import org.apache.syncope.client.ui.commons.ConnIdSpecialName;
 import org.apache.syncope.client.ui.commons.Constants;
 import org.apache.syncope.client.ui.commons.panels.ModalPanel;
 import org.apache.syncope.client.ui.commons.wizards.AjaxWizard;
@@ -272,7 +273,7 @@ public class LinkedAccountModalPanel extends Panel implements ModalPanel {
                                 new PushTaskTO(),
                                 model.getObject().getType(),
                                 null,
-                                linkedAccountTO.getConnObjectKeyValue(),
+                                ConnIdSpecialName.UID + "==" + linkedAccountTO.getConnObjectKeyValue(),
                                 true,
                                 mlp,
                                 pageRef),
@@ -298,7 +299,7 @@ public class LinkedAccountModalPanel extends Panel implements ModalPanel {
                                 new PullTaskTO(),
                                 model.getObject().getType(),
                                 null,
-                                linkedAccountTO.getConnObjectKeyValue(),
+                                ConnIdSpecialName.UID + "==" + linkedAccountTO.getConnObjectKeyValue(),
                                 true,
                                 mlp,
                                 pageRef),
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 9fba8d5..81d7879 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,6 +26,7 @@ import java.util.Optional;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.client.console.rest.ReconciliationRestClient;
+import org.apache.syncope.client.ui.commons.ConnIdSpecialName;
 import org.apache.syncope.common.lib.to.ReconStatus;
 import org.apache.syncope.common.rest.api.beans.ReconQuery;
 import org.slf4j.Logger;
@@ -42,8 +43,8 @@ public final class ReconStatusUtils implements Serializable {
 
         ReconStatus result = null;
         try {
-            result = ReconciliationRestClient.status(
-                    new ReconQuery.Builder(anyTypeKey, resource).connObjectKeyValue(connObjectKeyValue).build());
+            result = ReconciliationRestClient.status(new ReconQuery.Builder(anyTypeKey, resource).
+                    fiql(ConnIdSpecialName.UID + "==" + connObjectKeyValue).build());
         } catch (Exception e) {
             LOG.warn("Unexpected error for {} {} on {}", anyTypeKey, connObjectKeyValue, resource, e);
         }
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 14e6cf4..f6bce2f 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
@@ -95,6 +95,7 @@ public class ReconTaskPanel extends MultilevelPanel.SecondLevel {
             final boolean isOnSyncope,
             final MultilevelPanel multiLevelPanelRef,
             final PageReference pageRef) {
+
         this(resource, taskTO, anyType, anyKey, null, isOnSyncope, multiLevelPanelRef, pageRef);
     }
 
@@ -103,7 +104,7 @@ public class ReconTaskPanel extends MultilevelPanel.SecondLevel {
             final ProvisioningTaskTO taskTO,
             final String anyType,
             final String anyKey,
-            final String connObjectKeyValue,
+            final String fiql,
             final boolean isOnSyncope,
             final MultilevelPanel multiLevelPanelRef,
             final PageReference pageRef) {
@@ -194,9 +195,7 @@ public class ReconTaskPanel extends MultilevelPanel.SecondLevel {
 
             @Override
             protected void onSubmit(final AjaxRequestTarget target) {
-                ReconQuery reconQuery = new ReconQuery.Builder(anyType, resource).
-                        anyKey(anyKey).
-                        connObjectKeyValue(connObjectKeyValue).build();
+                ReconQuery reconQuery = new ReconQuery.Builder(anyType, resource).anyKey(anyKey).fiql(fiql).build();
                 try {
                     if (taskTO instanceof PushTaskTO) {
                         ReconciliationRestClient.push(reconQuery, (PushTaskTO) form.getModelObject());
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/any/MergeLinkedAccountsWizardBuilder.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/any/MergeLinkedAccountsWizardBuilder.java
index 7b2771c..7f1fa02 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/any/MergeLinkedAccountsWizardBuilder.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/any/MergeLinkedAccountsWizardBuilder.java
@@ -19,8 +19,8 @@
 package org.apache.syncope.client.console.wizards.any;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import java.util.HashMap;
 import java.util.List;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
 import javax.ws.rs.HttpMethod;
@@ -43,6 +43,7 @@ import org.apache.syncope.common.lib.request.LinkedAccountUR;
 import org.apache.syncope.common.lib.request.UserUR;
 import org.apache.syncope.common.lib.to.LinkedAccountTO;
 import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.PatchOperation;
 import org.apache.syncope.common.rest.api.Preference;
 import org.apache.syncope.common.rest.api.RESTHeaders;
 import org.apache.syncope.common.rest.api.batch.BatchRequestItem;
@@ -78,7 +79,7 @@ public class MergeLinkedAccountsWizardBuilder extends BaseAjaxWizardBuilder<User
 
     @Override
     protected WizardModel buildModelSteps(final UserTO modelObject, final WizardModel wizardModel) {
-        this.model = new MergeLinkedAccountsWizardModel(modelObject);
+        model = new MergeLinkedAccountsWizardModel(modelObject);
         wizardModel.add(new MergeLinkedAccountsSearchPanel(model, getPageReference()));
         wizardModel.add(new MergeLinkedAccountsResourcesPanel(model, getPageReference()));
         wizardModel.add(new MergeLinkedAccountsReviewPanel(model, getPageReference()));
@@ -91,69 +92,76 @@ public class MergeLinkedAccountsWizardBuilder extends BaseAjaxWizardBuilder<User
             ((AjaxWizard.NewItemCancelEvent<?>) event.getPayload()).getTarget().ifPresent(modal::close);
         }
         if (event.getPayload() instanceof AjaxWizard.NewItemFinishEvent) {
-            Optional<AjaxRequestTarget> targetResult =
+            Optional<AjaxRequestTarget> target =
                     ((AjaxWizard.NewItemFinishEvent<?>) event.getPayload()).getTarget();
             try {
                 mergeAccounts();
-                this.parentPanel.info(this.parentPanel.getString(Constants.OPERATION_SUCCEEDED));
-                targetResult.ifPresent(target -> {
-                    ((BasePage) this.parentPanel.getPage()).getNotificationPanel().refresh(target);
-                    parentPanel.updateResultTable(target);
-                    modal.close(target);
+
+                parentPanel.info(parentPanel.getString(Constants.OPERATION_SUCCEEDED));
+                target.ifPresent(t -> {
+                    ((BasePage) parentPanel.getPage()).getNotificationPanel().refresh(t);
+                    parentPanel.updateResultTable(t);
+                    modal.close(t);
                 });
             } catch (Exception e) {
-                this.parentPanel.error(this.parentPanel.getString(Constants.ERROR) + ": " + e.getMessage());
-                targetResult.ifPresent(target -> ((BasePage) pageRef.getPage()).getNotificationPanel().refresh(target));
+                parentPanel.error(parentPanel.getString(Constants.ERROR) + ": " + e.getMessage());
+                target.ifPresent(t -> ((BasePage) pageRef.getPage()).getNotificationPanel().refresh(t));
             }
         }
     }
 
     private void mergeAccounts() throws Exception {
-        ResourceRestClient resourceRestClient = new ResourceRestClient();
-
-        UserTO mergingUserTO = this.model.getMergingUser();
+        UserTO mergingUserTO = model.getMergingUser();
 
         UserUR userUR = new UserUR();
-        userUR.setKey(this.model.getBaseUser().getUsername());
+        userUR.setKey(model.getBaseUser().getUsername());
 
         // Move linked accounts into the target/base user as linked accounts
         mergingUserTO.getLinkedAccounts().forEach(acct -> {
             LinkedAccountTO linkedAccount =
-                    new LinkedAccountTO.Builder(acct.getResource(), acct.getConnObjectKeyValue())
-                            .password(acct.getPassword())
-                            .suspended(acct.isSuspended())
-                            .username(acct.getUsername())
-                            .build();
+                    new LinkedAccountTO.Builder(acct.getResource(), acct.getConnObjectKeyValue()).
+                            password(acct.getPassword()).
+                            suspended(acct.isSuspended()).
+                            username(acct.getUsername()).
+                            build();
             linkedAccount.getPlainAttrs().addAll(acct.getPlainAttrs());
             linkedAccount.getPrivileges().addAll(acct.getPrivileges());
-            userUR.getLinkedAccounts().add(new LinkedAccountUR.Builder().linkedAccountTO(linkedAccount).build());
+            LinkedAccountUR patch = new LinkedAccountUR.Builder().
+                    linkedAccountTO(linkedAccount).
+                    operation(PatchOperation.ADD_REPLACE).
+                    build();
+            userUR.getLinkedAccounts().add(patch);
         });
 
         // Move merging user's resources into the target/base user as a linked account
         mergingUserTO.getResources().forEach(resource -> {
-            String connObjectKeyValue = resourceRestClient.getConnObjectKeyValue(resource,
+            String connObjectKeyValue = ResourceRestClient.getConnObjectKeyValue(resource,
                     mergingUserTO.getType(), mergingUserTO.getKey());
-            LinkedAccountTO linkedAccount =
-                    new LinkedAccountTO.Builder(resource, connObjectKeyValue)
-                            .build();
+            LinkedAccountTO linkedAccount = new LinkedAccountTO.Builder(resource, connObjectKeyValue).build();
             linkedAccount.getPlainAttrs().addAll(mergingUserTO.getPlainAttrs());
             linkedAccount.getPrivileges().addAll(mergingUserTO.getPrivileges());
-            userUR.getLinkedAccounts().add(new LinkedAccountUR.Builder().linkedAccountTO(linkedAccount).build());
+            LinkedAccountUR patch = new LinkedAccountUR.Builder().
+                    linkedAccountTO(linkedAccount).
+                    operation(PatchOperation.ADD_REPLACE).
+                    build();
+            userUR.getLinkedAccounts().add(patch);
         });
 
         // Move merging user into target/base user as a linked account
-        String connObjectKeyValue = resourceRestClient.getConnObjectKeyValue(
-                this.model.getResource().getKey(),
+        String connObjectKeyValue = ResourceRestClient.getConnObjectKeyValue(
+                model.getResource().getKey(),
                 mergingUserTO.getType(), mergingUserTO.getKey());
-        LinkedAccountTO linkedAccount =
-                new LinkedAccountTO.Builder(this.model.getResource().getKey(), connObjectKeyValue)
-                        .password(mergingUserTO.getPassword())
-                        .suspended(mergingUserTO.isSuspended())
-                        .username(mergingUserTO.getUsername())
-                        .build();
+        LinkedAccountTO linkedAccount = new LinkedAccountTO.Builder(model.getResource().getKey(), connObjectKeyValue).
+                password(mergingUserTO.getPassword()).
+                suspended(mergingUserTO.isSuspended()).
+                username(mergingUserTO.getUsername()).
+                build();
         linkedAccount.getPlainAttrs().addAll(mergingUserTO.getPlainAttrs());
         linkedAccount.getPrivileges().addAll(mergingUserTO.getPrivileges());
-        userUR.getLinkedAccounts().add(new LinkedAccountUR.Builder().linkedAccountTO(linkedAccount).build());
+        LinkedAccountUR patch = new LinkedAccountUR.Builder().linkedAccountTO(linkedAccount).
+                operation(PatchOperation.ADD_REPLACE).
+                build();
+        userUR.getLinkedAccounts().add(patch);
 
         BatchRequest batchRequest = SyncopeConsoleSession.get().batch();
 
@@ -168,7 +176,7 @@ public class MergeLinkedAccountsWizardBuilder extends BaseAjaxWizardBuilder<User
         String updateUserPayload = MAPPER.writeValueAsString(userUR);
         BatchRequestItem updateUser = new BatchRequestItem();
         updateUser.setMethod(HttpMethod.PATCH);
-        updateUser.setRequestURI("/users/" + this.model.getBaseUser().getUsername());
+        updateUser.setRequestURI("/users/" + model.getBaseUser().getUsername());
         updateUser.setHeaders(new HashMap<>());
         updateUser.getHeaders().put(RESTHeaders.PREFER, List.of(Preference.RETURN_NO_CONTENT.toString()));
         updateUser.getHeaders().put(HttpHeaders.ACCEPT, List.of(MediaType.APPLICATION_JSON));
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleSession.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleSession.java
index 34dff66..99d2c9a 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleSession.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeConsoleSession.java
@@ -67,29 +67,52 @@ public class SyncopeConsoleSession extends AuthenticatedWebSession implements Ba
 
     private static final long serialVersionUID = 747562246415852166L;
 
-    private static final Logger LOG = LoggerFactory.getLogger(SyncopeConsoleSession.class);
+    public enum Error {
+        SESSION_EXPIRED("error.session.expired", "Session expired: please login again"),
+        AUTHORIZATION("error.authorization", "Insufficient access rights when performing the requested operation"),
+        REST("error.rest", "There was an error while contacting the Core server");
 
-    private final SyncopeClientFactoryBean clientFactory;
+        private final String key;
 
-    private final SyncopeClient anonymousClient;
+        private final String fallback;
 
-    private final PlatformInfo platformInfo;
+        Error(final String key, final String fallback) {
+            this.key = key;
+            this.fallback = fallback;
+        }
+
+        public String key() {
+            return key;
+        }
+
+        public String fallback() {
+            return fallback;
+        }
+    }
+
+    protected static final Logger LOG = LoggerFactory.getLogger(SyncopeConsoleSession.class);
 
-    private final SystemInfo systemInfo;
+    protected final SyncopeClientFactoryBean clientFactory;
 
-    private final Map<Class<?>, Object> services = Collections.synchronizedMap(new HashMap<>());
+    protected final SyncopeClient anonymousClient;
 
-    private final ThreadPoolTaskExecutor executor;
+    protected final PlatformInfo platformInfo;
 
-    private String domain;
+    protected final SystemInfo systemInfo;
 
-    private SyncopeClient client;
+    protected final Map<Class<?>, Object> services = Collections.synchronizedMap(new HashMap<>());
 
-    private UserTO selfTO;
+    protected final ThreadPoolTaskExecutor executor;
 
-    private Map<String, Set<String>> auth;
+    protected String domain;
 
-    private Roles roles;
+    protected SyncopeClient client;
+
+    protected UserTO selfTO;
+
+    protected Map<String, Set<String>> auth;
+
+    protected Roles roles;
 
     public static SyncopeConsoleSession get() {
         return (SyncopeConsoleSession) Session.get();
@@ -114,6 +137,18 @@ public class SyncopeConsoleSession extends AuthenticatedWebSession implements Ba
         executor.initialize();
     }
 
+    protected String message(final SyncopeClientException sce) {
+        return sce.getType().name() + ": " + sce.getElements().stream().collect(Collectors.joining(", "));
+    }
+
+    /**
+     * Extract and localize (if translation available) the actual message from the given exception; then, report it
+     * via {@link Session#error(java.io.Serializable)}.
+     *
+     * @see org.apache.syncope.client.lib.RestClientExceptionMapper
+     *
+     * @param e raised exception
+     */
     @Override
     public void onException(final Exception e) {
         Throwable root = ExceptionUtils.getRootCause(e);
@@ -121,9 +156,10 @@ public class SyncopeConsoleSession extends AuthenticatedWebSession implements Ba
 
         if (root instanceof SyncopeClientException) {
             SyncopeClientException sce = (SyncopeClientException) root;
-            if (!sce.isComposite()) {
-                message = sce.getElements().stream().collect(Collectors.joining(", "));
-            }
+            message = sce.isComposite()
+                    ? sce.asComposite().getExceptions().stream().
+                            map(c -> message(c)).collect(Collectors.joining("; "))
+                    : message(sce);
         } else if (root instanceof AccessControlException || root instanceof ForbiddenException) {
             Error error = StringUtils.containsIgnoreCase(message, "expired")
                     ? Error.SESSION_EXPIRED
@@ -256,7 +292,7 @@ public class SyncopeConsoleSession extends AuthenticatedWebSession implements Ba
     public List<String> getSearchableRealms() {
         Set<String> roots = auth.get(IdRepoEntitlement.REALM_LIST);
         return roots.isEmpty()
-                ? Collections.emptyList()
+                ? List.of()
                 : roots.stream().sorted().collect(Collectors.toList());
     }
 
@@ -328,7 +364,7 @@ public class SyncopeConsoleSession extends AuthenticatedWebSession implements Ba
     }
 
     @SuppressWarnings("unchecked")
-    private <T> T getCachedService(final Class<T> serviceClass) {
+    protected <T> T getCachedService(final Class<T> serviceClass) {
         T service;
         if (services.containsKey(serviceClass)) {
             service = (T) services.get(serviceClass);
diff --git a/client/idrepo/console/src/test/java/org/apache/syncope/client/console/SyncopeConsoleApplicationTest.java b/client/idrepo/console/src/test/java/org/apache/syncope/client/console/SyncopeConsoleApplicationTest.java
index a0eccc2..d4c4bdb 100644
--- a/client/idrepo/console/src/test/java/org/apache/syncope/client/console/SyncopeConsoleApplicationTest.java
+++ b/client/idrepo/console/src/test/java/org/apache/syncope/client/console/SyncopeConsoleApplicationTest.java
@@ -110,7 +110,7 @@ public class SyncopeConsoleApplicationTest extends AbstractTest {
         message = session.getFeedbackMessages().first();
         assertNotNull(message);
         assertTrue(message.isError());
-        assertEquals("Error 1", message.getMessage());
+        assertEquals(ClientExceptionType.InvalidUser.name() + ": Error 1", message.getMessage());
         session.getFeedbackMessages().clear();
 
         sce = SyncopeClientException.build(ClientExceptionType.InvalidUser);
@@ -120,7 +120,7 @@ public class SyncopeConsoleApplicationTest extends AbstractTest {
         message = session.getFeedbackMessages().first();
         assertNotNull(message);
         assertTrue(message.isError());
-        assertEquals("Error 1, Error 2", message.getMessage());
+        assertEquals(ClientExceptionType.InvalidUser.name() + ": Error 1, Error 2", message.getMessage());
         session.getFeedbackMessages().clear();
 
         SyncopeClientCompositeException scce = SyncopeClientException.buildComposite();
@@ -130,7 +130,9 @@ public class SyncopeConsoleApplicationTest extends AbstractTest {
         message = session.getFeedbackMessages().first();
         assertNotNull(message);
         assertTrue(message.isError());
-        assertEquals(scce.getMessage(), message.getMessage());
+        assertTrue(StringUtils.contains((CharSequence) message.getMessage(),
+                ClientExceptionType.InvalidExternalResource.name()));
+        assertTrue(StringUtils.contains((CharSequence) message.getMessage(), ClientExceptionType.InvalidUser.name()));
         session.getFeedbackMessages().clear();
     }
 }
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
index 152993a..4aaf620 100644
--- 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
@@ -48,8 +48,8 @@ public class ReconQuery implements Serializable {
             return this;
         }
 
-        public Builder connObjectKeyValue(final String connObjectKeyValue) {
-            instance.setConnObjectKeyValue(connObjectKeyValue);
+        public Builder fiql(final String fiql) {
+            instance.setFiql(fiql);
             return this;
         }
 
@@ -82,7 +82,7 @@ public class ReconQuery implements Serializable {
 
     private String resourceKey;
 
-    private String connObjectKeyValue;
+    private String fiql;
 
     private Set<String> moreAttrsToGet;
 
@@ -115,13 +115,13 @@ public class ReconQuery implements Serializable {
         this.resourceKey = resourceKey;
     }
 
-    public String getConnObjectKeyValue() {
-        return connObjectKeyValue;
+    public String getFiql() {
+        return fiql;
     }
 
-    @QueryParam("connObjectKeyValue")
-    public void setConnObjectKeyValue(final String connObjectKeyValue) {
-        this.connObjectKeyValue = connObjectKeyValue;
+    @QueryParam(JAXRSService.PARAM_FIQL)
+    public void setFiql(final String fiql) {
+        this.fiql = fiql;
     }
 
     public Set<String> getMoreAttrsToGet() {
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 a2c8193..6e1e480 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
@@ -69,6 +69,7 @@ public interface ReconciliationService extends JAXRSService {
      *
      * @param query query conditions
      * @param pushTask push specification
+     * @return push report
      */
     @ApiResponses(
             @ApiResponse(responseCode = "204", description = "Operation was successful"))
@@ -76,13 +77,14 @@ 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(@BeanParam ReconQuery query, @NotNull PushTaskTO pushTask);
+    List<ProvisioningReport> push(@BeanParam ReconQuery query, @NotNull PushTaskTO pushTask);
 
     /**
      * Pulls the matching user, group, any object or linked account from the External Resource into Syncope.
      *
      * @param query query conditions
      * @param pullTask pull specification
+     * @return pull report
      */
     @ApiResponses(
             @ApiResponse(responseCode = "204", description = "Operation was successful"))
@@ -90,7 +92,7 @@ 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(@BeanParam ReconQuery query, @NotNull PullTaskTO pullTask);
+    List<ProvisioningReport> pull(@BeanParam ReconQuery query, @NotNull PullTaskTO pullTask);
 
     /**
      * Export a list of any objects matching the given query as CSV according to the provided specification.
diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/ConnObjectTO.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/ConnObjectTO.java
index fc1158f..9b72672 100644
--- a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/ConnObjectTO.java
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/ConnObjectTO.java
@@ -33,8 +33,18 @@ public class ConnObjectTO implements BaseBean {
 
     private static final long serialVersionUID = 5139554911265442497L;
 
+    private String fiql;
+
     private final Set<Attr> attrs = new LinkedHashSet<>();
 
+    public String getFiql() {
+        return fiql;
+    }
+
+    public void setFiql(final String fiql) {
+        this.fiql = fiql;
+    }
+
     @JacksonXmlElementWrapper(localName = "attrs")
     @JacksonXmlProperty(localName = "attr")
     public Set<Attr> getAttrs() {
@@ -49,6 +59,7 @@ public class ConnObjectTO implements BaseBean {
     @Override
     public int hashCode() {
         return new HashCodeBuilder().
+                append(fiql).
                 append(attrs).
                 build();
     }
@@ -66,6 +77,7 @@ public class ConnObjectTO implements BaseBean {
         }
         final ConnObjectTO other = (ConnObjectTO) obj;
         return new EqualsBuilder().
+                append(fiql, other.fiql).
                 append(attrs, other.attrs).
                 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 3ef81ac..5b4381c 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
@@ -26,6 +26,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
+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;
@@ -40,7 +41,6 @@ import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.lib.types.MatchType;
 import org.apache.syncope.common.rest.api.beans.CSVPullSpec;
-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;
@@ -69,6 +69,10 @@ import org.apache.syncope.core.persistence.api.dao.VirSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
 import org.apache.syncope.core.persistence.api.entity.AnyUtils;
+import org.apache.syncope.core.persistence.api.entity.VirSchema;
+import org.apache.syncope.core.provisioning.api.pushpull.ConstantReconFilterBuilder;
+import org.apache.syncope.core.provisioning.api.pushpull.KeyValueReconFilterBuilder;
+import org.apache.syncope.core.provisioning.api.pushpull.ReconFilterBuilder;
 import org.apache.syncope.core.provisioning.java.pushpull.stream.CSVStreamConnector;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopeSinglePullExecutor;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopeSinglePushExecutor;
@@ -82,12 +86,18 @@ import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.identityconnectors.framework.common.objects.Attribute;
 import org.identityconnectors.framework.common.objects.ConnectorObject;
+import org.identityconnectors.framework.common.objects.OperationOptions;
+import org.identityconnectors.framework.common.objects.SearchResult;
+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.common.objects.filter.Filter;
+import org.identityconnectors.framework.spi.SearchResultsHandler;
 import org.quartz.JobExecutionException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Component;
-import org.springframework.util.CollectionUtils;
 
 @Component
 public class ReconciliationLogic extends AbstractTransactionalLogic<EntityTO> {
@@ -168,7 +178,7 @@ public class ReconciliationLogic extends AbstractTransactionalLogic<EntityTO> {
             final String connObjectKeyValue,
             final Set<Attribute> attrs) {
 
-        ConnObjectTO connObjectTO = ConnObjectUtils.getConnObjectTO(attrs);
+        ConnObjectTO connObjectTO = ConnObjectUtils.getConnObjectTO(null, attrs);
         connObjectTO.getAttrs().add(new Attr.Builder(connObjectKeyItem.getExtAttrName()).
                 value(connObjectKeyValue).build());
         connObjectTO.getAttrs().add(new Attr.Builder(Uid.NAME).
@@ -209,71 +219,111 @@ public class ReconciliationLogic extends AbstractTransactionalLogic<EntityTO> {
     }
 
     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_GET_CONNOBJECT + "')")
-    public ReconStatus status(final ReconQuery query) {
-        Provision provision = getProvision(query.getAnyTypeKey(), query.getResourceKey());
+    public ReconStatus status(final String anyTypeKey, final String resourceKey, final String anyKey) {
+        Provision provision = getProvision(anyTypeKey, resourceKey);
 
         MappingItem connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision).
                 orElseThrow(() -> new NotFoundException(
                 "ConnObjectKey for " + provision.getAnyType().getKey()
                 + " on resource '" + provision.getResource().getKey() + "'"));
 
+        Any<?> any = getAny(provision, anyKey);
+
         ReconStatus status = new ReconStatus();
+        status.setMatchType(MatchType.ANY);
+        status.setAnyTypeKind(any.getType().getKind());
+        status.setAnyKey(any.getKey());
+        status.setRealm(any.getRealm().getFullPath());
+        status.setOnSyncope(getOnSyncope(any, connObjectKeyItem, provision));
+
+        List<ConnectorObject> connObjs = outboundMatcher.match(
+                connFactory.getConnector(provision.getResource()), any, provision, Optional.empty());
+        if (!connObjs.isEmpty()) {
+            status.setOnResource(ConnObjectUtils.getConnObjectTO(
+                    outboundMatcher.getFIQL(connObjs.get(0), provision), connObjs.get(0).getAttributes()));
+
+            if (connObjs.size() > 1) {
+                LOG.warn("Expected single match, found {}", connObjs);
+            } else {
+                virAttrHandler.setValues(any, connObjs.get(0));
+            }
+        }
 
-        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.setRealm(match.getAny().getRealm().getFullPath());
-                            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.setRealm(match.getLinkedAccount().getOwner().getRealm().getFullPath());
-                            status.setOnSyncope(getOnSyncope(match.getLinkedAccount(), connObjectKeyItem, provision));
-                        }
-                    });
+        return status;
+    }
 
-            Optional<String[]> moreAttrsToGet = CollectionUtils.isEmpty(query.getMoreAttrsToGet())
-                    ? Optional.empty()
-                    : Optional.of(query.getMoreAttrsToGet().toArray(new String[0]));
+    private SyncDeltaBuilder syncDeltaBuilder(
+            final Provision provision,
+            final Filter filter,
+            final Set<String> moreAttrsToGet) {
+
+        Stream<MappingItem> mapItems = Stream.concat(
+                provision.getMapping().getItems().stream(),
+                virSchemaDAO.findByProvision(provision).stream().map(VirSchema::asLinkingMappingItem));
+        OperationOptions options = MappingUtils.buildOperationOptions(mapItems, moreAttrsToGet.toArray(new String[0]));
+
+        SyncDeltaBuilder syncDeltaBuilder = new SyncDeltaBuilder().
+                setToken(new SyncToken("")).
+                setDeltaType(SyncDeltaType.CREATE_OR_UPDATE).
+                setObjectClass(provision.getObjectClass());
+        connFactory.getConnector(provision.getResource()).
+                search(provision.getObjectClass(), filter, new SearchResultsHandler() {
+
+                    @Override
+                    public boolean handle(final ConnectorObject connObj) {
+                        syncDeltaBuilder.setObject(connObj);
+                        return false;
+                    }
+
+                    @Override
+                    public void handleResult(final SearchResult sr) {
+                        // do nothing
+                    }
+                }, 1, null, List.of(), options);
+
+        return syncDeltaBuilder;
+    }
 
-            outboundMatcher.matchByConnObjectKeyValue(
-                    connFactory.getConnector(provision.getResource()),
-                    connObjectKeyItem,
-                    query.getConnObjectKeyValue(),
-                    provision,
-                    moreAttrsToGet,
-                    Optional.empty()).
-                    ifPresent(connObj -> {
-                        status.setOnResource(ConnObjectUtils.getConnObjectTO(connObj.getAttributes()));
+    @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_GET_CONNOBJECT + "')")
+    public ReconStatus status(
+            final String anyTypeKey,
+            final String resourceKey,
+            final Filter filter,
+            final Set<String> moreAttrsToGet) {
 
-                        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.setRealm(any.getRealm().getFullPath());
-            status.setOnSyncope(getOnSyncope(any, connObjectKeyItem, provision));
-
-            List<ConnectorObject> connObjs = outboundMatcher.match(
-                    connFactory.getConnector(provision.getResource()), any, provision, Optional.empty());
-            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));
+        Provision provision = getProvision(anyTypeKey, resourceKey);
+
+        SyncDeltaBuilder syncDeltaBuilder = syncDeltaBuilder(provision, filter, moreAttrsToGet);
+
+        ReconStatus status = new ReconStatus();
+        if (syncDeltaBuilder.getObject() != null) {
+            MappingItem connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision).
+                    orElseThrow(() -> new NotFoundException(
+                    "ConnObjectKey for " + provision.getAnyType().getKey()
+                    + " on resource '" + provision.getResource().getKey() + "'"));
+
+            inboundMatcher.match(syncDeltaBuilder.build(), 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.setRealm(match.getAny().getRealm().getFullPath());
+                    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.setRealm(match.getLinkedAccount().getOwner().getRealm().getFullPath());
+                    status.setOnSyncope(getOnSyncope(match.getLinkedAccount(), connObjectKeyItem, provision));
                 }
+            });
+
+            status.setOnResource(ConnObjectUtils.getConnObjectTO(
+                    outboundMatcher.getFIQL(syncDeltaBuilder.getObject(), provision),
+                    syncDeltaBuilder.getObject().getAttributes()));
+
+            if (status.getMatchType() == MatchType.ANY && StringUtils.isNotBlank(status.getAnyKey())) {
+                virAttrHandler.setValues(getAny(provision, status.getAnyKey()), syncDeltaBuilder.getObject());
             }
         }
 
@@ -281,101 +331,105 @@ public class ReconciliationLogic extends AbstractTransactionalLogic<EntityTO> {
     }
 
     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_EXECUTE + "')")
-    public void push(final ReconQuery query, final PushTaskTO pushTask) {
-        Provision provision = getProvision(query.getAnyTypeKey(), query.getResourceKey());
+    public List<ProvisioningReport> push(
+            final String anyTypeKey,
+            final String resourceKey,
+            final String anyKey,
+            final PushTaskTO pushTask) {
 
-        MappingItem connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision).
-                orElseThrow(() -> new NotFoundException(
-                "ConnObjectKey for " + provision.getAnyType().getKey()
-                + " on resource '" + provision.getResource().getKey() + "'"));
+        Provision provision = getProvision(anyTypeKey, resourceKey);
 
         SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Reconciliation);
         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(result.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());
+        try {
+            results.addAll(singlePushExecutor.push(
+                    provision,
+                    connFactory.getConnector(provision.getResource()),
+                    getAny(provision, anyKey),
+                    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());
         }
 
         if (!sce.isEmpty()) {
             throw sce;
         }
+
+        return results;
     }
 
     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_EXECUTE + "')")
-    public void pull(final ReconQuery query, final PullTaskTO pullTask) {
-        Provision provision = getProvision(query.getAnyTypeKey(), query.getResourceKey());
+    public List<ProvisioningReport> push(
+            final String anyTypeKey,
+            final String resourceKey,
+            final Filter filter,
+            final Set<String> moreAttrsToGet,
+            final PushTaskTO pushTask) {
+
+        Provision provision = getProvision(anyTypeKey, resourceKey);
 
-        Optional<String> connObjectKeyValue = Optional.ofNullable(query.getConnObjectKeyValue());
-        if (query.getAnyKey() != null) {
-            Any<?> any = getAny(provision, query.getAnyKey());
-            connObjectKeyValue = mappingManager.getConnObjectKeyValue(any, provision);
+        SyncDeltaBuilder syncDeltaBuilder = syncDeltaBuilder(provision, filter, moreAttrsToGet);
+
+        SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Reconciliation);
+        List<ProvisioningReport> results = new ArrayList<>();
+
+        if (syncDeltaBuilder.getObject() != null) {
+            inboundMatcher.match(syncDeltaBuilder.build(), 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(result.getMessage());
+                        } else {
+                            results.add(result);
+                        }
+                    }
+                } catch (JobExecutionException e) {
+                    sce.getElements().add(e.getMessage());
+                }
+            });
         }
-        if (connObjectKeyValue.isEmpty()) {
-            throw new NotFoundException(
-                    "ConnObjectKey for " + provision.getAnyType().getKey()
-                    + " on resource '" + provision.getResource().getKey() + "'");
+
+        if (!sce.isEmpty()) {
+            throw sce;
         }
 
+        return results;
+    }
+
+    private List<ProvisioningReport> pull(
+            final Provision provision,
+            final ReconFilterBuilder reconFilterBuilder,
+            final PullTaskTO pullTask) {
+
         if (pullTask.getDestinationRealm() == null || realmDAO.findByFullPath(pullTask.getDestinationRealm()) == null) {
             throw new NotFoundException("Realm " + pullTask.getDestinationRealm());
         }
-        if (provision.getMapping().getConnObjectKeyItem().isEmpty()) {
-            throw new NotFoundException(
-                    "ConnObjectKey cannot be determined for mapping " + provision.getMapping().getKey());
-        }
 
         SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Reconciliation);
+        List<ProvisioningReport> results = new ArrayList<>();
         try {
-            List<ProvisioningReport> results = singlePullExecutor.pull(
+            results.addAll(singlePullExecutor.pull(
                     provision,
                     connFactory.getConnector(provision.getResource()),
-                    provision.getMapping().getConnObjectKeyItem().get().getExtAttrName(),
-                    connObjectKeyValue.get(),
-                    pullTask);
+                    reconFilterBuilder,
+                    pullTask));
             if (!results.isEmpty() && results.get(0).getStatus() == ProvisioningReport.Status.FAILURE) {
                 sce.getElements().add(results.get(0).getMessage());
             }
@@ -386,6 +440,52 @@ public class ReconciliationLogic extends AbstractTransactionalLogic<EntityTO> {
         if (!sce.isEmpty()) {
             throw sce;
         }
+
+        return results;
+    }
+
+    @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_EXECUTE + "')")
+    public List<ProvisioningReport> pull(
+            final String anyTypeKey,
+            final String resourceKey,
+            final String anyKey,
+            final PullTaskTO pullTask) {
+
+        Provision provision = getProvision(anyTypeKey, resourceKey);
+
+        Any<?> any = getAny(provision, anyKey);
+
+        if (!provision.getMapping().getConnObjectKeyItem().isPresent()) {
+            throw new NotFoundException(
+                    "ConnObjectKey cannot be determined for mapping " + provision.getMapping().getKey());
+        }
+
+        String connObjectKeyValue = mappingManager.getConnObjectKeyValue(any, provision).
+                orElseThrow(() -> new NotFoundException(
+                "ConnObjectKey for " + provision.getAnyType().getKey()
+                + " on resource '" + provision.getResource().getKey() + "'"));
+
+        return pull(
+                provision,
+                new KeyValueReconFilterBuilder(
+                        provision.getMapping().getConnObjectKeyItem().get().getExtAttrName(), connObjectKeyValue),
+                pullTask);
+    }
+
+    @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_EXECUTE + "')")
+    public List<ProvisioningReport> pull(
+            final String anyTypeKey,
+            final String resourceKey,
+            final Filter filter,
+            final Set<String> moreAttrsToGet,
+            final PullTaskTO pullTask) {
+
+        Provision provision = getProvision(anyTypeKey, resourceKey);
+
+        return pull(
+                provision,
+                new ConstantReconFilterBuilder(filter),
+                pullTask);
     }
 
     private CsvSchema.Builder csvSchema(final AbstractCSVSpec spec) {
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 1eb41fd..9a0ca55 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
@@ -336,7 +336,8 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
             virAttrHandler.setValues(any, connObjs.get(0));
         }
 
-        return ConnObjectUtils.getConnObjectTO(connObjs.get(0).getAttributes());
+        return ConnObjectUtils.getConnObjectTO(
+                outboundMatcher.getFIQL(connObjs.get(0), provision), connObjs.get(0).getAttributes());
     }
 
     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_GET_CONNOBJECT + "')")
@@ -353,20 +354,18 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
                 "ConnObjectKey mapping for " + provision.getAnyType().getKey()
                 + " on resource '" + provision.getResource().getKey() + "'"));
 
-        Optional<ConnectorObject> connObj = outboundMatcher.matchByConnObjectKeyValue(
+        return outboundMatcher.matchByConnObjectKeyValue(
                 connFactory.getConnector(provision.getResource()),
                 connObjectKeyItem,
                 connObjectKeyValue,
                 provision,
                 Optional.empty(),
-                Optional.empty());
-        if (connObj.isPresent()) {
-            return ConnObjectUtils.getConnObjectTO(connObj.get().getAttributes());
-        }
-
-        throw new NotFoundException(
+                Optional.empty()).
+                map(connectorObject -> ConnObjectUtils.getConnObjectTO(
+                outboundMatcher.getFIQL(connectorObject, provision), connectorObject.getAttributes())).
+                orElseThrow(() -> new NotFoundException(
                 "Object " + connObjectKeyValue + " with class " + provision.getObjectClass()
-                + " not found on resource " + provision.getResource().getKey());
+                + " not found on resource " + provision.getResource().getKey()));
     }
 
     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_LIST_CONNOBJECT + "')")
@@ -381,6 +380,7 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
             final List<OrderByClause> orderBy) {
 
         ExternalResource resource;
+        Provision provision;
         ObjectClass objectClass;
         OperationOptions options;
         if (SyncopeConstants.REALM_ANYTYPE.equals(anyTypeKey)) {
@@ -392,11 +392,12 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
                 throw new NotFoundException("Realm provisioning for resource '" + key + '\'');
             }
 
+            provision = null;
             objectClass = resource.getOrgUnit().getObjectClass();
             options = MappingUtils.buildOperationOptions(
                     resource.getOrgUnit().getItems().stream(), moreAttrsToGet.toArray(new String[0]));
         } else {
-            Provision provision = getProvision(key, anyTypeKey);
+            provision = getProvision(key, anyTypeKey);
             resource = provision.getResource();
             objectClass = provision.getObjectClass();
 
@@ -414,7 +415,9 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
 
                     @Override
                     public boolean handle(final ConnectorObject connectorObject) {
-                        connObjects.add(ConnObjectUtils.getConnObjectTO(connectorObject.getAttributes()));
+                        connObjects.add(ConnObjectUtils.getConnObjectTO(
+                                provision == null ? null : outboundMatcher.getFIQL(connectorObject, provision),
+                                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 6f3b1a1..881544b 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
@@ -19,17 +19,25 @@
 package org.apache.syncope.core.rest.cxf.service;
 
 import java.io.InputStream;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import javax.validation.ValidationException;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.StreamingOutput;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.cxf.jaxrs.ext.search.SearchBean;
+import org.apache.cxf.jaxrs.ext.search.SearchCondition;
+import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.to.ProvisioningReport;
 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.ClientExceptionType;
 import org.apache.syncope.common.rest.api.RESTHeaders;
 import org.apache.syncope.common.rest.api.beans.AnyQuery;
 import org.apache.syncope.common.rest.api.beans.CSVPullSpec;
@@ -38,7 +46,9 @@ 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.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.search.FilterVisitor;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.identityconnectors.framework.common.objects.filter.Filter;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -49,44 +59,91 @@ public class ReconciliationServiceImpl extends AbstractServiceImpl implements Re
     private ReconciliationLogic logic;
 
     private void validate(final ReconQuery reconQuery) {
-        if ((reconQuery.getAnyKey() == null && reconQuery.getConnObjectKeyValue() == null)
-                || (reconQuery.getAnyKey() != null && reconQuery.getConnObjectKeyValue() != null)) {
+        if ((reconQuery.getAnyKey() == null && reconQuery.getFiql() == null)
+                || (reconQuery.getAnyKey() != null && reconQuery.getFiql() != null)) {
 
-            throw new ValidationException("Either provide anyKey or connObjectKeyValue, not both");
+            throw new ValidationException("Either provide anyKey or fiql, not both");
         }
     }
 
+    private Pair<Filter, Set<String>> buildFromFIQL(final ReconQuery reconQuery) {
+        Filter filter = null;
+        Set<String> moreAttrsToGet = new HashSet<>();
+        if (reconQuery.getMoreAttrsToGet() != null) {
+            moreAttrsToGet.addAll(reconQuery.getMoreAttrsToGet());
+        }
+        if (StringUtils.isNotBlank(reconQuery.getFiql())) {
+            try {
+                FilterVisitor visitor = new FilterVisitor();
+                SearchCondition<SearchBean> sc = searchContext.getCondition(reconQuery.getFiql(), SearchBean.class);
+                sc.accept(visitor);
+
+                filter = visitor.getQuery();
+                moreAttrsToGet.addAll(visitor.getAttrs());
+            } catch (Exception e) {
+                LOG.error("Invalid FIQL expression: {}", reconQuery.getFiql(), e);
+
+                SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidSearchExpression);
+                sce.getElements().add(reconQuery.getFiql());
+                sce.getElements().add(ExceptionUtils.getRootCauseMessage(e));
+                throw sce;
+            }
+        }
+
+        return Pair.of(filter, moreAttrsToGet);
+    }
+
     @Override
-    public ReconStatus status(final ReconQuery reconQuery) {
-        validate(reconQuery);
-        return logic.status(reconQuery);
+    public ReconStatus status(final ReconQuery query) {
+        validate(query);
+
+        if (query.getAnyKey() != null) {
+            return logic.status(query.getAnyTypeKey(), query.getResourceKey(), query.getAnyKey());
+        }
+
+        Pair<Filter, Set<String>> fromFIQL = buildFromFIQL(query);
+        return logic.status(query.getAnyTypeKey(), query.getResourceKey(), fromFIQL.getLeft(), fromFIQL.getRight());
     }
 
     @Override
-    public void push(final ReconQuery reconQuery, final PushTaskTO pushTask) {
-        validate(reconQuery);
-        logic.push(reconQuery, pushTask);
+    public List<ProvisioningReport> push(final ReconQuery query, final PushTaskTO pushTask) {
+        validate(query);
+
+        if (query.getAnyKey() != null) {
+            return logic.push(query.getAnyTypeKey(), query.getResourceKey(), query.getAnyKey(), pushTask);
+        }
+
+        Pair<Filter, Set<String>> fromFIQL = buildFromFIQL(query);
+        return logic.push(
+                query.getAnyTypeKey(), query.getResourceKey(), fromFIQL.getLeft(), fromFIQL.getRight(), pushTask);
     }
 
     @Override
-    public void pull(final ReconQuery reconQuery, final PullTaskTO pullTask) {
-        validate(reconQuery);
-        logic.pull(reconQuery, pullTask);
+    public List<ProvisioningReport> pull(final ReconQuery query, final PullTaskTO pullTask) {
+        validate(query);
+
+        if (query.getAnyKey() != null) {
+            return logic.pull(query.getAnyTypeKey(), query.getResourceKey(), query.getAnyKey(), pullTask);
+        }
+
+        Pair<Filter, Set<String>> fromFIQL = buildFromFIQL(query);
+        return logic.pull(
+                query.getAnyTypeKey(), query.getResourceKey(), fromFIQL.getLeft(), fromFIQL.getRight(), pullTask);
     }
 
     @Override
-    public Response push(final AnyQuery anyQuery, final CSVPushSpec spec) {
-        String realm = StringUtils.prependIfMissing(anyQuery.getRealm(), SyncopeConstants.ROOT_REALM);
+    public Response push(final AnyQuery query, final CSVPushSpec spec) {
+        String realm = StringUtils.prependIfMissing(query.getRealm(), SyncopeConstants.ROOT_REALM);
 
-        SearchCond searchCond = StringUtils.isBlank(anyQuery.getFiql())
+        SearchCond searchCond = StringUtils.isBlank(query.getFiql())
                 ? null
-                : getSearchCond(anyQuery.getFiql(), realm);
+                : getSearchCond(query.getFiql(), realm);
 
-        StreamingOutput sout = (os) -> logic.push(
+        StreamingOutput sout = os -> logic.push(
                 searchCond,
-                anyQuery.getPage(),
-                anyQuery.getSize(),
-                getOrderByClauses(anyQuery.getOrderBy()),
+                query.getPage(),
+                query.getSize(),
+                getOrderByClauses(query.getOrderBy()),
                 realm,
                 spec,
                 os);
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/PushCorrelationRule.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/PushCorrelationRule.java
index 050cdce..256b799 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/PushCorrelationRule.java
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/PushCorrelationRule.java
@@ -18,9 +18,12 @@
  */
 package org.apache.syncope.core.persistence.api.dao;
 
+import java.util.function.BiFunction;
 import org.apache.syncope.common.lib.policy.PushCorrelationRuleConf;
 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.ConnectorObject;
+import org.identityconnectors.framework.common.objects.Uid;
 import org.identityconnectors.framework.common.objects.filter.Filter;
 
 /**
@@ -29,15 +32,34 @@ import org.identityconnectors.framework.common.objects.filter.Filter;
 @FunctionalInterface
 public interface PushCorrelationRule {
 
+    /**
+     * Default FIQL builder using __UID__.
+     */
+    BiFunction<ConnectorObject, Provision, String> DEFAULT_FIQL_BUILDER =
+            (connectorObject, provision) -> Uid.NAME + "==" + connectorObject.getUid().getUidValue();
+
     default void setConf(PushCorrelationRuleConf conf) {
     }
 
     /**
-     * Return a search condition.
+     * Returns a filter to match the given any with a connector object on the external resource identified by
+     * the given provision.
      *
      * @param any user, group or any object
      * @param provision resource provision
-     * @return search condition.
+     * @return filter.
      */
     Filter getFilter(Any<?> any, Provision provision);
+
+    /**
+     * Returns a FIQL string to match the given connector object when searching into the external resource identified by
+     * the given provision.
+     *
+     * @param connectorObject connector object
+     * @param provision resource provision
+     * @return fiql
+     */
+    default String getFiql(ConnectorObject connectorObject, Provision provision) {
+        return DEFAULT_FIQL_BUILDER.apply(connectorObject, provision);
+    }
 }
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/DefaultPushCorrelationRule.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/DefaultPushCorrelationRule.java
index 03d1e3d..19ac31c 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/DefaultPushCorrelationRule.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/DefaultPushCorrelationRule.java
@@ -19,10 +19,15 @@
 package org.apache.syncope.core.persistence.jpa.dao;
 
 import java.util.ArrayList;
+import java.util.Base64;
 import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
 import org.apache.commons.lang3.tuple.Pair;
+import org.apache.cxf.jaxrs.ext.search.client.CompleteCondition;
 import org.apache.syncope.common.lib.policy.DefaultPushCorrelationRuleConf;
 import org.apache.syncope.common.lib.policy.PushCorrelationRuleConf;
+import org.apache.syncope.common.lib.search.ConnObjectTOFiqlSearchConditionBuilder;
 import org.apache.syncope.common.lib.types.MappingPurpose;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
 import org.identityconnectors.framework.common.objects.Attribute;
@@ -32,18 +37,26 @@ import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.provisioning.api.AccountGetter;
 import org.apache.syncope.core.provisioning.api.MappingManager;
 import org.apache.syncope.core.provisioning.api.PlainAttrGetter;
+import org.identityconnectors.common.security.GuardedByteArray;
+import org.identityconnectors.common.security.GuardedString;
+import org.identityconnectors.common.security.SecurityUtil;
 import org.identityconnectors.framework.common.objects.AttributeBuilder;
+import org.identityconnectors.framework.common.objects.ConnectorObject;
 import org.identityconnectors.framework.common.objects.filter.Filter;
 import org.identityconnectors.framework.common.objects.filter.FilterBuilder;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.CollectionUtils;
 
 @PushCorrelationRuleConfClass(DefaultPushCorrelationRuleConf.class)
 public class DefaultPushCorrelationRule implements PushCorrelationRule {
 
+    protected static final ConnObjectTOFiqlSearchConditionBuilder FIQL_BUILDER =
+            new ConnObjectTOFiqlSearchConditionBuilder();
+
     @Autowired
-    private MappingManager mappingManager;
+    protected MappingManager mappingManager;
 
-    private DefaultPushCorrelationRuleConf conf;
+    protected DefaultPushCorrelationRuleConf conf;
 
     @Override
     public void setConf(final PushCorrelationRuleConf conf) {
@@ -60,8 +73,7 @@ public class DefaultPushCorrelationRule implements PushCorrelationRule {
         List<Filter> filters = new ArrayList<>();
 
         provision.getMapping().getItems().stream().filter(
-                item -> conf.getSchemas().contains(item.getIntAttrName())
-                && (item.getPurpose() == MappingPurpose.PROPAGATION || item.getPurpose() == MappingPurpose.BOTH)).
+                item -> conf.getSchemas().contains(item.getIntAttrName()) && item.getPurpose() != MappingPurpose.NONE).
                 forEach(item -> {
                     Pair<String, Attribute> attr = mappingManager.prepareAttr(
                             provision,
@@ -90,4 +102,45 @@ public class DefaultPushCorrelationRule implements PushCorrelationRule {
                 ? FilterBuilder.or(filters)
                 : FilterBuilder.and(filters);
     }
+
+    @Override
+    public String getFiql(final ConnectorObject connectorObject, final Provision provision) {
+        List<CompleteCondition> conditions = new ArrayList<>();
+
+        provision.getMapping().getItems().stream().filter(
+                item -> conf.getSchemas().contains(item.getIntAttrName()) && item.getPurpose() != MappingPurpose.NONE).
+                forEach(item -> Optional.ofNullable(connectorObject.getAttributeByName(item.getExtAttrName())).
+                ifPresent(attr -> {
+                    if (CollectionUtils.isEmpty(attr.getValue())) {
+                        conditions.add(FIQL_BUILDER.isNull(attr.getName()));
+                    } else {
+                        List<CompleteCondition> valueConditions = new ArrayList<>();
+
+                        attr.getValue().stream().filter(Objects::nonNull).forEach(value -> {
+                            if (value instanceof GuardedString) {
+                                valueConditions.add(FIQL_BUILDER.is(attr.getName()).
+                                        equalTo(SecurityUtil.decrypt((GuardedString) value)));
+                            } else if (value instanceof GuardedByteArray) {
+                                valueConditions.add(FIQL_BUILDER.is(attr.getName()).
+                                        equalTo(new String(SecurityUtil.decrypt((GuardedByteArray) value))));
+                            } else if (value instanceof byte[]) {
+                                valueConditions.add(FIQL_BUILDER.is(attr.getName()).
+                                        equalTo(Base64.getEncoder().encodeToString((byte[]) value)));
+                            } else {
+                                valueConditions.add(FIQL_BUILDER.is(attr.getName()).equalTo(value.toString()));
+                            }
+                        });
+
+                        if (!valueConditions.isEmpty()) {
+                            conditions.add(valueConditions.size() == 1
+                                    ? valueConditions.get(0)
+                                    : FIQL_BUILDER.and(valueConditions));
+                        }
+                    }
+                }));
+
+        return conf.isOrSchemas()
+                ? FIQL_BUILDER.or(conditions).query()
+                : FIQL_BUILDER.and(conditions).query();
+    }
 }
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/propagation/PropagationReporter.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/propagation/PropagationReporter.java
index 814f75b..fd7ccbe 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/propagation/PropagationReporter.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/propagation/PropagationReporter.java
@@ -44,6 +44,7 @@ public interface PropagationReporter {
      * @param taskInfo propagation task
      * @param execStatus propagation execution status
      * @param failureReason propagation execution failure message
+     * @param fiql FIQL string to match the connector objects into the external resource
      * @param beforeObj retrieved connector object before operation execution
      * @param afterObj retrieved connector object after operation execution
      */
@@ -51,6 +52,7 @@ public interface PropagationReporter {
             PropagationTaskInfo taskInfo,
             ExecStatus execStatus,
             String failureReason,
+            String fiql,
             ConnectorObject beforeObj,
             ConnectorObject afterObj);
 
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/ConstantReconFilterBuilder.java
similarity index 58%
copy from core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePullExecutor.java
copy to core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/ConstantReconFilterBuilder.java
index 7614291..d980602 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/ConstantReconFilterBuilder.java
@@ -18,20 +18,18 @@
  */
 package org.apache.syncope.core.provisioning.api.pushpull;
 
-import org.apache.syncope.common.lib.to.ProvisioningReport;
-import java.util.List;
-import org.apache.syncope.common.lib.to.PullTaskTO;
-import org.apache.syncope.core.persistence.api.entity.resource.Provision;
-import org.apache.syncope.core.provisioning.api.Connector;
-import org.quartz.JobExecutionException;
+import org.identityconnectors.framework.common.objects.filter.Filter;
 
-@FunctionalInterface
-public interface SyncopeSinglePullExecutor {
+public class ConstantReconFilterBuilder implements ReconFilterBuilder {
 
-    List<ProvisioningReport> pull(
-            Provision provision,
-            Connector connector,
-            String connObjectKey,
-            String connObjectValue,
-            PullTaskTO pullTaskTO) throws JobExecutionException;
+    private final Filter filter;
+
+    public ConstantReconFilterBuilder(final Filter filter) {
+        this.filter = filter;
+    }
+
+    @Override
+    public Filter build() {
+        return filter;
+    }
 }
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/KeyValueReconFilterBuilder.java
similarity index 58%
copy from core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePullExecutor.java
copy to core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/KeyValueReconFilterBuilder.java
index 7614291..1b5d349 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/KeyValueReconFilterBuilder.java
@@ -18,20 +18,23 @@
  */
 package org.apache.syncope.core.provisioning.api.pushpull;
 
-import org.apache.syncope.common.lib.to.ProvisioningReport;
-import java.util.List;
-import org.apache.syncope.common.lib.to.PullTaskTO;
-import org.apache.syncope.core.persistence.api.entity.resource.Provision;
-import org.apache.syncope.core.provisioning.api.Connector;
-import org.quartz.JobExecutionException;
+import org.identityconnectors.framework.common.objects.AttributeBuilder;
+import org.identityconnectors.framework.common.objects.filter.Filter;
+import org.identityconnectors.framework.common.objects.filter.FilterBuilder;
 
-@FunctionalInterface
-public interface SyncopeSinglePullExecutor {
+public class KeyValueReconFilterBuilder implements ReconFilterBuilder {
 
-    List<ProvisioningReport> pull(
-            Provision provision,
-            Connector connector,
-            String connObjectKey,
-            String connObjectValue,
-            PullTaskTO pullTaskTO) throws JobExecutionException;
+    private final String key;
+
+    private final String value;
+
+    public KeyValueReconFilterBuilder(final String key, final String value) {
+        this.key = key;
+        this.value = value;
+    }
+
+    @Override
+    public Filter build() {
+        return FilterBuilder.equalTo(AttributeBuilder.build(key, value));
+    }
 }
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 7614291..4ace3c8 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
@@ -31,7 +31,6 @@ public interface SyncopeSinglePullExecutor {
     List<ProvisioningReport> pull(
             Provision provision,
             Connector connector,
-            String connObjectKey,
-            String connObjectValue,
+            ReconFilterBuilder reconFilterBuilder,
             PullTaskTO pullTaskTO) throws JobExecutionException;
 }
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 ed17e68..29a4f26 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
@@ -18,6 +18,15 @@
  */
 package org.apache.syncope.core.provisioning.java.data;
 
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+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.Attr;
@@ -74,24 +83,18 @@ import org.apache.syncope.core.provisioning.api.VirAttrHandler;
 import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.api.PropagationByResource;
 import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+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.ConnectorObject;
+import org.identityconnectors.framework.common.objects.ConnectorObjectBuilder;
 import org.identityconnectors.framework.common.objects.Uid;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-
 abstract class AbstractAnyDataBinder {
 
     protected static final Logger LOG = LoggerFactory.getLogger(AbstractAnyDataBinder.class);
@@ -144,6 +147,9 @@ abstract class AbstractAnyDataBinder {
     @Autowired
     protected IntAttrNameParser intAttrNameParser;
 
+    @Autowired
+    protected OutboundMatcher outboundMatcher;
+
     protected void setRealm(final Any<?> any, final AnyUR anyUR) {
         if (anyUR.getRealm() != null && StringUtils.isNotBlank(anyUR.getRealm().getValue())) {
             Realm newRealm = realmDAO.findByFullPath(anyUR.getRealm().getValue());
@@ -170,10 +176,20 @@ abstract class AbstractAnyDataBinder {
             Pair<String, Set<Attribute>> prepared = mappingManager.prepareAttrsFromAny(
                     any, password, changePwd, true, provision);
 
-            ConnObjectTO connObjectTO = ConnObjectUtils.getConnObjectTO(prepared.getRight());
-            connObjectTO.getAttrs().add(
-                    new Attr.Builder(connObjectKeyItem.getExtAttrName()).value(prepared.getLeft()).build());
-            connObjectTO.getAttrs().add(new Attr.Builder(Uid.NAME).value(prepared.getLeft()).build());
+            ConnObjectTO connObjectTO;
+            if (StringUtils.isBlank(prepared.getLeft())) {
+                connObjectTO = ConnObjectUtils.getConnObjectTO(null, prepared.getRight());
+            } else {
+                ConnectorObject connectorObject = new ConnectorObjectBuilder().
+                        addAttributes(prepared.getRight()).
+                        addAttribute(new Uid(prepared.getLeft())).
+                        addAttribute(AttributeBuilder.build(connObjectKeyItem.getExtAttrName(), prepared.getLeft())).
+                        build();
+
+                connObjectTO = ConnObjectUtils.getConnObjectTO(
+                        outboundMatcher.getFIQL(connectorObject, provision),
+                        connectorObject.getAttributes());
+            }
 
             onResources.put(provision.getResource().getKey(), connObjectTO);
         }));
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 3e3fe7d..3dc37d3 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
@@ -449,9 +449,17 @@ public abstract class AbstractPropagationTaskExecutor implements PropagationTask
                 taskDAO.save(task);
             }
 
+            String fiql = provision == null
+                    ? null
+                    : afterObj != null
+                            ? outboundMatcher.getFIQL(afterObj, provision)
+                            : beforeObj != null
+                                    ? outboundMatcher.getFIQL(beforeObj, provision)
+                                    : null;
             reporter.onSuccessOrNonPriorityResourceFailures(taskInfo,
                     ExecStatus.valueOf(execution.getStatus()),
                     failureReason,
+                    fiql,
                     beforeObj,
                     afterObj);
         }
@@ -532,6 +540,7 @@ public abstract class AbstractPropagationTaskExecutor implements PropagationTask
                 ExecStatus.valueOf(execution.getStatus()),
                 rejectReason,
                 null,
+                null,
                 null);
 
         return execution;
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 5fbb4b0..010daab 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
@@ -50,6 +50,7 @@ public class DefaultPropagationReporter implements PropagationReporter {
             final PropagationTaskInfo taskInfo,
             final ExecStatus executionStatus,
             final String failureReason,
+            final String fiql,
             final ConnectorObject beforeObj,
             final ConnectorObject afterObj) {
 
@@ -59,11 +60,11 @@ public class DefaultPropagationReporter implements PropagationReporter {
         status.setFailureReason(failureReason);
 
         if (beforeObj != null) {
-            status.setBeforeObj(ConnObjectUtils.getConnObjectTO(beforeObj.getAttributes()));
+            status.setBeforeObj(ConnObjectUtils.getConnObjectTO(fiql, beforeObj.getAttributes()));
         }
 
         if (afterObj != null) {
-            status.setAfterObj(ConnObjectUtils.getConnObjectTO(afterObj.getAttributes()));
+            status.setAfterObj(ConnObjectUtils.getConnObjectTO(fiql, afterObj.getAttributes()));
         }
 
         add(status);
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PriorityPropagationTaskExecutor.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PriorityPropagationTaskExecutor.java
index 5af16d1..69e9902 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PriorityPropagationTaskExecutor.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PriorityPropagationTaskExecutor.java
@@ -122,7 +122,7 @@ public class PriorityPropagationTaskExecutor extends AbstractPropagationTaskExec
 
                     if (nullPriorityAsync) {
                         reporter.onSuccessOrNonPriorityResourceFailures(
-                                taskInfo, ExecStatus.CREATED, null, null, null);
+                                taskInfo, ExecStatus.CREATED, null, null, null, null);
                     }
                 } catch (Exception e) {
                     LOG.error("While submitting task for async execution: {}", taskInfo, e);
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
index 4536fca..c776a0e 100644
--- 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
@@ -93,6 +93,12 @@ public class OutboundMatcher {
         return rule;
     }
 
+    public String getFIQL(final ConnectorObject connectorObject, final Provision provision) {
+        return rule(provision).
+                map(rule -> rule.getFiql(connectorObject, provision)).
+                orElseGet(() -> PushCorrelationRule.DEFAULT_FIQL_BUILDER.apply(connectorObject, provision));
+    }
+
     public List<ConnectorObject> match(
             final PropagationTask task,
             final Connector connector,
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 a31d08e..97c67ea 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
@@ -50,9 +50,6 @@ import org.apache.syncope.core.provisioning.api.pushpull.SyncopeSinglePullExecut
 import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
 import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
 import org.apache.syncope.core.spring.ImplementationManager;
-import org.identityconnectors.framework.common.objects.AttributeBuilder;
-import org.identityconnectors.framework.common.objects.filter.Filter;
-import org.identityconnectors.framework.common.objects.filter.FilterBuilder;
 import org.quartz.JobExecutionException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
@@ -70,8 +67,7 @@ public class SinglePullJobDelegate extends PullJobDelegate implements SyncopeSin
     public List<ProvisioningReport> pull(
             final Provision provision,
             final Connector connector,
-            final String connObjectKey,
-            final String connObjectValue,
+            final ReconFilterBuilder reconFilterBuilder,
             final PullTaskTO pullTaskTO) throws JobExecutionException {
 
         LOG.debug("Executing pull on {}", provision.getResource());
@@ -160,7 +156,7 @@ public class SinglePullJobDelegate extends PullJobDelegate implements SyncopeSin
 
             connector.filteredReconciliation(
                     provision.getObjectClass(),
-                    new SingleReconciliationFilterBuilder(connObjectKey, connObjectValue),
+                    reconFilterBuilder,
                     handler,
                     MappingUtils.buildOperationOptions(mapItems, moreAttrsToGet.toArray(new String[0])));
 
@@ -181,21 +177,4 @@ public class SinglePullJobDelegate extends PullJobDelegate implements SyncopeSin
                     : new JobExecutionException("While pulling from connector", e);
         }
     }
-
-    static class SingleReconciliationFilterBuilder implements ReconFilterBuilder {
-
-        private final String key;
-
-        private final String value;
-
-        SingleReconciliationFilterBuilder(final String key, final String value) {
-            this.key = key;
-            this.value = value;
-        }
-
-        @Override
-        public Filter build() {
-            return FilterBuilder.equalTo(AttributeBuilder.build(key, value));
-        }
-    }
 }
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 aad7460..4ccbf09 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
@@ -64,6 +64,7 @@ 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;
 
 @Component
 public class ConnObjectUtils {
@@ -100,7 +101,7 @@ public class ConnObjectUtils {
      * @return password value
      */
     public static String getPassword(final Object pwd) {
-        final StringBuilder result = new StringBuilder();
+        StringBuilder result = new StringBuilder();
 
         if (pwd instanceof GuardedString) {
             result.append(SecurityUtil.decrypt((GuardedString) pwd));
@@ -118,27 +119,31 @@ public class ConnObjectUtils {
     /**
      * Builds {@link ConnObjectTO} out of a collection of {@link Attribute} instances.
      *
+     * @param fiql FIQL expression to uniquely identify the given Connector Object
      * @param attrs attributes
      * @return transfer object
      */
-    public static ConnObjectTO getConnObjectTO(final Set<Attribute> attrs) {
-        final ConnObjectTO connObjectTO = new ConnObjectTO();
+    public static ConnObjectTO getConnObjectTO(final String fiql, final Set<Attribute> attrs) {
+        ConnObjectTO connObjectTO = new ConnObjectTO();
+        connObjectTO.setFiql(fiql);
 
-        if (attrs != null) {
+        if (!CollectionUtils.isEmpty(attrs)) {
             connObjectTO.getAttrs().addAll(attrs.stream().map(attr -> {
                 Attr attrTO = new Attr();
                 attrTO.setSchema(attr.getName());
-                if (attr.getValue() != null) {
-                    attr.getValue().stream().filter(Objects::nonNull).forEachOrdered(value -> {
+
+                if (!CollectionUtils.isEmpty(attr.getValue())) {
+                    attr.getValue().stream().filter(Objects::nonNull).forEach(value -> {
                         if (value instanceof GuardedString || value instanceof GuardedByteArray) {
                             attrTO.getValues().add(getPassword(value));
                         } else if (value instanceof byte[]) {
                             attrTO.getValues().add(Base64.getEncoder().encodeToString((byte[]) value));
-                        } else if (value != null) {
+                        } else {
                             attrTO.getValues().add(value.toString());
                         }
                     });
                 }
+
                 return attrTO;
             }).collect(Collectors.toList()));
         }
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 ac13163..1637720 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
@@ -669,7 +669,7 @@ public class PropagationTaskITCase extends AbstractTaskITCase {
 
             GroupCR groupCR = new GroupCR();
             groupCR.setName("SYNCOPEGROUP1605-" + getUUIDString());
-            groupCR.setRealm("/");
+            groupCR.setRealm(SyncopeConstants.ROOT_REALM);
             groupCR.getResources().add(ldap.getKey());
             groupCR.getPlainAttrs().add(new Attr.Builder("originalName").value(originalName).build());
 
@@ -678,8 +678,7 @@ public class PropagationTaskITCase extends AbstractTaskITCase {
 
             // 3. check attributes prepared for propagation
             PagedResult<PropagationTaskTO> tasks = taskService.search(new TaskQuery.Builder(TaskType.PROPAGATION).
-                    resource(groupTO.getResources().iterator().next()).
-                    anyTypeKind(AnyTypeKind.GROUP).entityKey(groupTO.getKey()).build());
+                    resource(ldap.getKey()).anyTypeKind(AnyTypeKind.GROUP).entityKey(groupTO.getKey()).build());
             assertEquals(1, tasks.getSize());
             assertEquals(ResourceOperation.CREATE, tasks.getResult().get(0).getOperation());
             assertEquals(ExecStatus.SUCCESS.name(), tasks.getResult().get(0).getLatestExecStatus());
@@ -694,8 +693,8 @@ public class PropagationTaskITCase extends AbstractTaskITCase {
             groupTO = updateGroup(groupUR).getEntity();
 
             tasks = taskService.search(new TaskQuery.Builder(TaskType.PROPAGATION).
-                    resource(groupTO.getResources().iterator().next()).
-                    anyTypeKind(AnyTypeKind.GROUP).entityKey(groupTO.getKey()).orderBy("start DESC").build());
+                    resource(ldap.getKey()).anyTypeKind(AnyTypeKind.GROUP).entityKey(groupTO.getKey()).
+                    orderBy("start DESC").build());
             assertEquals(2, tasks.getSize());
             assertEquals(ResourceOperation.UPDATE, tasks.getResult().get(0).getOperation());
             assertEquals(ExecStatus.SUCCESS.name(), tasks.getResult().get(0).getLatestExecStatus());
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 529e09b..4642b67 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
@@ -117,6 +117,10 @@ public class ReconciliationITCase extends AbstractITCase {
         if (enable != null) {
             status.getOnSyncope().getAttrs().remove(enable);
         }
+        // FIQL is always null for Syncope
+        assertNull(status.getOnSyncope().getFiql());
+        assertNotNull(status.getOnResource().getFiql());
+        status.getOnResource().setFiql(null);
         assertEquals(status.getOnSyncope(), status.getOnResource());
     }
 
@@ -169,7 +173,7 @@ public class ReconciliationITCase extends AbstractITCase {
 
         // 2. verify reconciliation status
         ReconStatus status = reconciliationService.status(
-                new ReconQuery.Builder(PRINTER, RESOURCE_NAME_DBSCRIPTED).connObjectKeyValue(externalKey).build());
+                new ReconQuery.Builder(PRINTER, RESOURCE_NAME_DBSCRIPTED).fiql("ID==" + externalKey).build());
         assertNotNull(status);
         assertNull(status.getAnyTypeKind());
         assertNull(status.getAnyKey());
@@ -183,8 +187,8 @@ public class ReconciliationITCase extends AbstractITCase {
         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);
+        reconciliationService.pull(
+                new ReconQuery.Builder(PRINTER, RESOURCE_NAME_DBSCRIPTED).fiql("ID==" + externalKey).build(), pullTask);
 
         // 4. verify reconciliation result
         AnyObjectTO printer = anyObjectService.read(externalName);