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

[syncope] branch 2_1_X updated: [SYNCOPE-1501] Connector Objects can now be filtered via FIQL

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

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


The following commit(s) were added to refs/heads/2_1_X by this push:
     new d5513ae  [SYNCOPE-1501] Connector Objects can now be filtered via FIQL
d5513ae is described below

commit d5513ae2d128653a9160ae35aa4ba596cbeba7d1
Author: Francesco Chicchiriccò <il...@apache.org>
AuthorDate: Wed Oct 9 14:33:54 2019 +0200

    [SYNCOPE-1501] Connector Objects can now be filtered via FIQL
---
 .../client/console/rest/ResourceRestClient.java    |   6 +-
 .../enduser/resources/DynamicTemplateResource.java |   2 -
 .../search/AbstractFiqlSearchConditionBuilder.java |   1 -
 .../AnyObjectFiqlSearchConditionBuilder.java       |   1 -
 ...=> ConnObjectTOFiqlSearchConditionBuilder.java} |  50 ++--
 .../search/GroupFiqlSearchConditionBuilder.java    |   1 -
 ...jectTOListQuery.java => ConnObjectTOQuery.java} |  23 +-
 .../common/rest/api/service/ResourceService.java   |  15 +-
 .../apache/syncope/core/logic/ResourceLogic.java   | 129 +++++----
 .../persistence/api/search/FilterConverter.java    |  64 +++++
 .../core/persistence/api/search/FilterVisitor.java | 172 ++++++++++++
 .../persistence/api/search/SearchCondVisitor.java  |  27 +-
 .../api/search/FilterConverterTest.java            | 289 +++++++++++++++++++++
 .../core/rest/cxf/service/ResourceServiceImpl.java |  49 +++-
 .../enduser/resources/UserRequestsResource.java    |   5 +-
 .../resources/UserRequestsStartResource.java       |   2 -
 .../syncope/fit/core/LinkedAccountITCase.java      |  34 +--
 .../apache/syncope/fit/core/ResourceITCase.java    |  99 -------
 .../org/apache/syncope/fit/core/SearchITCase.java  | 112 ++++++++
 19 files changed, 826 insertions(+), 255 deletions(-)

diff --git a/client/console/src/main/java/org/apache/syncope/client/console/rest/ResourceRestClient.java b/client/console/src/main/java/org/apache/syncope/client/console/rest/ResourceRestClient.java
index c8e135b..93d0166 100644
--- a/client/console/src/main/java/org/apache/syncope/client/console/rest/ResourceRestClient.java
+++ b/client/console/src/main/java/org/apache/syncope/client/console/rest/ResourceRestClient.java
@@ -27,7 +27,7 @@ import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.to.ConnObjectTO;
 import org.apache.syncope.common.lib.to.PagedConnObjectTOResult;
 import org.apache.syncope.common.lib.to.ResourceTO;
-import org.apache.syncope.common.rest.api.beans.ConnObjectTOListQuery;
+import org.apache.syncope.common.rest.api.beans.ConnObjectTOQuery;
 import org.apache.syncope.common.rest.api.service.ResourceService;
 import org.apache.wicket.extensions.markup.html.repeater.util.SortParam;
 
@@ -63,7 +63,7 @@ public class ResourceRestClient extends BaseRestClient {
             final String pagedResultCookie,
             final SortParam<String> sort) {
 
-        ConnObjectTOListQuery.Builder builder = new ConnObjectTOListQuery.Builder().
+        ConnObjectTOQuery.Builder builder = new ConnObjectTOQuery.Builder().
                 pagedResultsCookie(pagedResultCookie).
                 size(size).
                 orderBy(toOrderBy(sort));
@@ -73,7 +73,7 @@ public class ResourceRestClient extends BaseRestClient {
 
         PagedConnObjectTOResult list;
         try {
-            list = getService(ResourceService.class).listConnObjects(resource, anyTypeKey, builder.build());
+            list = getService(ResourceService.class).searchConnObjects(resource, anyTypeKey, builder.build());
             result.addAll(list.getResult());
             nextPageResultCookie = list.getPagedResultsCookie();
         } catch (Exception e) {
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/DynamicTemplateResource.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/DynamicTemplateResource.java
index 86b19bd..8e4268f 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/DynamicTemplateResource.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/DynamicTemplateResource.java
@@ -18,8 +18,6 @@
  */
 package org.apache.syncope.client.enduser.resources;
 
-import static org.apache.syncope.client.enduser.resources.BaseResource.MAPPER;
-
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import javax.ws.rs.core.MediaType;
diff --git a/common/lib/src/main/java/org/apache/syncope/common/lib/search/AbstractFiqlSearchConditionBuilder.java b/common/lib/src/main/java/org/apache/syncope/common/lib/search/AbstractFiqlSearchConditionBuilder.java
index 18fee59..5f8aa43 100644
--- a/common/lib/src/main/java/org/apache/syncope/common/lib/search/AbstractFiqlSearchConditionBuilder.java
+++ b/common/lib/src/main/java/org/apache/syncope/common/lib/search/AbstractFiqlSearchConditionBuilder.java
@@ -146,6 +146,5 @@ public abstract class AbstractFiqlSearchConditionBuilder extends FiqlSearchCondi
             this.result = SpecialAttr.DYNREALMS.toString();
             return condition(FiqlParser.NEQ, dynRealm, (Object[]) moreDynRealms);
         }
-
     }
 }
diff --git a/common/lib/src/main/java/org/apache/syncope/common/lib/search/AnyObjectFiqlSearchConditionBuilder.java b/common/lib/src/main/java/org/apache/syncope/common/lib/search/AnyObjectFiqlSearchConditionBuilder.java
index 9276015..b497c7b 100644
--- a/common/lib/src/main/java/org/apache/syncope/common/lib/search/AnyObjectFiqlSearchConditionBuilder.java
+++ b/common/lib/src/main/java/org/apache/syncope/common/lib/search/AnyObjectFiqlSearchConditionBuilder.java
@@ -162,6 +162,5 @@ public class AnyObjectFiqlSearchConditionBuilder extends AbstractFiqlSearchCondi
             this.result = SpecialAttr.ASSIGNABLE.toString();
             return condition(FiqlParser.EQ, SpecialAttr.NULL);
         }
-
     }
 }
diff --git a/common/lib/src/main/java/org/apache/syncope/common/lib/search/GroupFiqlSearchConditionBuilder.java b/common/lib/src/main/java/org/apache/syncope/common/lib/search/ConnObjectTOFiqlSearchConditionBuilder.java
similarity index 50%
copy from common/lib/src/main/java/org/apache/syncope/common/lib/search/GroupFiqlSearchConditionBuilder.java
copy to common/lib/src/main/java/org/apache/syncope/common/lib/search/ConnObjectTOFiqlSearchConditionBuilder.java
index 188b5e0..d028a71 100644
--- a/common/lib/src/main/java/org/apache/syncope/common/lib/search/GroupFiqlSearchConditionBuilder.java
+++ b/common/lib/src/main/java/org/apache/syncope/common/lib/search/ConnObjectTOFiqlSearchConditionBuilder.java
@@ -20,15 +20,14 @@ package org.apache.syncope.common.lib.search;
 
 import java.util.Map;
 import org.apache.cxf.jaxrs.ext.search.client.CompleteCondition;
-import org.apache.cxf.jaxrs.ext.search.fiql.FiqlParser;
 
 /**
  * Extends {@link AbstractFiqlSearchConditionBuilder} by providing some additional facilities for searching
- * groups in Syncope.
+ * connector objects.
  */
-public class GroupFiqlSearchConditionBuilder extends AbstractFiqlSearchConditionBuilder {
+public class ConnObjectTOFiqlSearchConditionBuilder extends AbstractFiqlSearchConditionBuilder {
 
-    private static final long serialVersionUID = 6275686371606165706L;
+    private static final long serialVersionUID = 4983742159694010935L;
 
     @Override
     protected Builder newBuilderInstance() {
@@ -36,30 +35,12 @@ public class GroupFiqlSearchConditionBuilder extends AbstractFiqlSearchCondition
     }
 
     @Override
-    public GroupProperty is(final String property) {
+    public SyncopeProperty is(final String property) {
         return newBuilderInstance().is(property);
     }
 
-    public CompleteCondition isAssignable() {
-        return newBuilderInstance().
-                is(SpecialAttr.ASSIGNABLE.toString()).
-                isAssignable();
-    }
-
-    public CompleteCondition withMembers(final String member, final String... moreMembers) {
-        return newBuilderInstance().
-                is(SpecialAttr.MEMBER.toString()).
-                withMembers(member, moreMembers);
-    }
-
-    public CompleteCondition withoutMembers(final String member, final String... moreMembers) {
-        return newBuilderInstance().
-                is(SpecialAttr.MEMBER.toString()).
-                withoutMembers(member, moreMembers);
-    }
-
     protected class Builder extends AbstractFiqlSearchConditionBuilder.Builder
-            implements GroupProperty, CompleteCondition {
+            implements SyncopeProperty, CompleteCondition {
 
         public Builder(final Map<String, String> properties) {
             super(properties);
@@ -70,29 +51,30 @@ public class GroupFiqlSearchConditionBuilder extends AbstractFiqlSearchCondition
         }
 
         @Override
-        public GroupProperty is(final String property) {
+        public SyncopeProperty is(final String property) {
             Builder b = new Builder(this);
             b.result = property;
             return b;
         }
 
         @Override
-        public CompleteCondition isAssignable() {
-            this.result = SpecialAttr.ASSIGNABLE.toString();
-            return condition(FiqlParser.EQ, SpecialAttr.NULL);
+        public CompleteCondition inDynRealms(final String dynRealm, final String... moreDynRealms) {
+            throw new UnsupportedOperationException();
         }
 
         @Override
-        public CompleteCondition withMembers(final String member, final String... moreMembers) {
-            this.result = SpecialAttr.MEMBER.toString();
-            return condition(FiqlParser.EQ, member, (Object[]) moreMembers);
+        public CompleteCondition notInDynRealms(final String dynRealm, final String... moreDynRealms) {
+            throw new UnsupportedOperationException();
         }
 
         @Override
-        public CompleteCondition withoutMembers(final String member, final String... moreMembers) {
-            this.result = SpecialAttr.MEMBER.toString();
-            return condition(FiqlParser.NEQ, member, (Object[]) moreMembers);
+        public CompleteCondition hasResources(final String resource, final String... moreResources) {
+            throw new UnsupportedOperationException();
         }
 
+        @Override
+        public CompleteCondition hasNotResources(final String resource, final String... moreResources) {
+            throw new UnsupportedOperationException();
+        }
     }
 }
diff --git a/common/lib/src/main/java/org/apache/syncope/common/lib/search/GroupFiqlSearchConditionBuilder.java b/common/lib/src/main/java/org/apache/syncope/common/lib/search/GroupFiqlSearchConditionBuilder.java
index 188b5e0..727b1a3 100644
--- a/common/lib/src/main/java/org/apache/syncope/common/lib/search/GroupFiqlSearchConditionBuilder.java
+++ b/common/lib/src/main/java/org/apache/syncope/common/lib/search/GroupFiqlSearchConditionBuilder.java
@@ -93,6 +93,5 @@ public class GroupFiqlSearchConditionBuilder extends AbstractFiqlSearchCondition
             this.result = SpecialAttr.MEMBER.toString();
             return condition(FiqlParser.NEQ, member, (Object[]) moreMembers);
         }
-
     }
 }
diff --git a/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ConnObjectTOListQuery.java b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ConnObjectTOQuery.java
similarity index 84%
rename from common/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ConnObjectTOListQuery.java
rename to common/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ConnObjectTOQuery.java
index 17322ca..db874ce 100644
--- a/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ConnObjectTOListQuery.java
+++ b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ConnObjectTOQuery.java
@@ -25,7 +25,7 @@ import javax.ws.rs.DefaultValue;
 import javax.ws.rs.QueryParam;
 import org.apache.syncope.common.rest.api.service.JAXRSService;
 
-public class ConnObjectTOListQuery implements Serializable {
+public class ConnObjectTOQuery implements Serializable {
 
     private static final long serialVersionUID = -371488230250055359L;
 
@@ -33,7 +33,7 @@ public class ConnObjectTOListQuery implements Serializable {
 
     public static class Builder {
 
-        private final ConnObjectTOListQuery instance = new ConnObjectTOListQuery();
+        private final ConnObjectTOQuery instance = new ConnObjectTOQuery();
 
         public Builder size(final Integer size) {
             instance.setSize(size);
@@ -50,10 +50,14 @@ public class ConnObjectTOListQuery implements Serializable {
             return this;
         }
 
-        public ConnObjectTOListQuery build() {
-            return instance;
+        public Builder fiql(final String fiql) {
+            instance.setFiql(fiql);
+            return this;
         }
 
+        public ConnObjectTOQuery build() {
+            return instance;
+        }
     }
 
     private Integer size;
@@ -62,6 +66,8 @@ public class ConnObjectTOListQuery implements Serializable {
 
     private String orderBy;
 
+    private String fiql;
+
     public Integer getSize() {
         return size == null
                 ? 25
@@ -95,4 +101,13 @@ public class ConnObjectTOListQuery implements Serializable {
     public void setOrderBy(final String orderBy) {
         this.orderBy = orderBy;
     }
+
+    public String getFiql() {
+        return fiql;
+    }
+
+    @QueryParam(JAXRSService.PARAM_FIQL)
+    public void setFiql(final String fiql) {
+        this.fiql = fiql;
+    }
 }
diff --git a/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ResourceService.java b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ResourceService.java
index 27b7813..cca2ac0 100644
--- a/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ResourceService.java
+++ b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ResourceService.java
@@ -45,7 +45,7 @@ import org.apache.syncope.common.lib.to.ConnObjectTO;
 import org.apache.syncope.common.lib.to.PagedConnObjectTOResult;
 import org.apache.syncope.common.lib.to.ResourceTO;
 import org.apache.syncope.common.rest.api.RESTHeaders;
-import org.apache.syncope.common.rest.api.beans.ConnObjectTOListQuery;
+import org.apache.syncope.common.rest.api.beans.ConnObjectTOQuery;
 
 /**
  * REST operations for external resources.
@@ -62,16 +62,17 @@ public interface ResourceService extends JAXRSService {
      *
      * @param key name of resource to read connector object from
      * @param anyTypeKey any object type
-     * @param anyKey any object key
+     * @param value if value looks like a UUID then it is interpreted as user, group or any object key, otherwise
+     * as key value on the resource
      * @return connector object from the external resource, for the given type and key
      */
     @GET
-    @Path("{key}/{anyTypeKey}/{anyKey}")
+    @Path("{key}/{anyTypeKey}/{value}")
     @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
     ConnObjectTO readConnObject(
             @NotNull @PathParam("key") String key,
             @NotNull @PathParam("anyTypeKey") String anyTypeKey,
-            @NotNull @PathParam("anyKey") String anyKey);
+            @NotNull @PathParam("value") String value);
 
     /**
      * Returns a paged list of connector objects from external resource, for the given type, matching
@@ -79,16 +80,16 @@ public interface ResourceService extends JAXRSService {
      *
      * @param key name of resource to read connector object from
      * @param anyTypeKey any object type
-     * @param listQuery query conditions
+     * @param connObjectTOQuery query conditions
      * @return connector objects from the external resource, for the given type
      */
     @GET
     @Path("{key}/{anyTypeKey}")
     @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
-    PagedConnObjectTOResult listConnObjects(
+    PagedConnObjectTOResult searchConnObjects(
             @NotNull @PathParam("key") String key,
             @NotNull @PathParam("anyTypeKey") String anyTypeKey,
-            @BeanParam ConnObjectTOListQuery listQuery);
+            @BeanParam ConnObjectTOQuery connObjectTOQuery);
 
     /**
      * Returns the resource with matching name.
diff --git a/core/logic/src/main/java/org/apache/syncope/core/logic/ResourceLogic.java b/core/logic/src/main/java/org/apache/syncope/core/logic/ResourceLogic.java
index 47a892a..b71ddd6 100644
--- a/core/logic/src/main/java/org/apache/syncope/core/logic/ResourceLogic.java
+++ b/core/logic/src/main/java/org/apache/syncope/core/logic/ResourceLogic.java
@@ -27,35 +27,30 @@ import java.util.Set;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.ArrayUtils;
-import org.apache.commons.lang3.tuple.ImmutableTriple;
 import org.apache.commons.lang3.tuple.Pair;
-import org.apache.commons.lang3.tuple.Triple;
 import org.apache.syncope.common.lib.collections.IteratorChain;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.to.ConnObjectTO;
 import org.apache.syncope.common.lib.to.ResourceTO;
-import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.lib.types.StandardEntitlement;
 import org.apache.syncope.core.persistence.api.dao.DuplicateException;
 import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
-import org.apache.syncope.core.persistence.api.dao.GroupDAO;
-import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.resource.MappingItem;
 import org.apache.syncope.core.provisioning.api.Connector;
 import org.apache.syncope.core.provisioning.api.ConnectorFactory;
 import org.apache.syncope.core.provisioning.api.data.ResourceDataBinder;
 import org.apache.syncope.core.provisioning.java.utils.ConnObjectUtils;
-import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
 import org.apache.syncope.core.persistence.api.dao.ConnInstanceDAO;
 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.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.AnyType;
+import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.ConnInstance;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
 import org.apache.syncope.core.provisioning.api.MappingManager;
@@ -73,6 +68,7 @@ import org.identityconnectors.framework.common.objects.ObjectClass;
 import org.identityconnectors.framework.common.objects.OperationOptions;
 import org.identityconnectors.framework.common.objects.SearchResult;
 import org.identityconnectors.framework.common.objects.Uid;
+import org.identityconnectors.framework.common.objects.filter.Filter;
 import org.identityconnectors.framework.spi.SearchResultsHandler;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -89,18 +85,9 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
     private AnyTypeDAO anyTypeDAO;
 
     @Autowired
-    private AnyObjectDAO anyObjectDAO;
-
-    @Autowired
     private ConnInstanceDAO connInstanceDAO;
 
     @Autowired
-    private UserDAO userDAO;
-
-    @Autowired
-    private GroupDAO groupDAO;
-
-    @Autowired
     private VirSchemaDAO virSchemaDAO;
 
     @Autowired
@@ -115,6 +102,9 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
     @Autowired
     private ConnectorFactory connFactory;
 
+    @Autowired
+    private AnyUtilsFactory anyUtilsFactory;
+
     protected void securityChecks(final Set<String> effectiveRealms, final String realm, final String key) {
         boolean authorized = effectiveRealms.stream().anyMatch(ownedRealm -> realm.startsWith(ownedRealm));
         if (!authorized) {
@@ -276,69 +266,57 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
         return resourceDAO.findAll().stream().map(binder::getResourceTO).collect(Collectors.toList());
     }
 
-    private Triple<ExternalResource, AnyType, Provision> connObjectInit(
+    private Pair<AnyType, Provision> connObjectInit(
             final String resourceKey, final String anyTypeKey) {
 
         ExternalResource resource = resourceDAO.authFind(resourceKey);
         if (resource == null) {
             throw new NotFoundException("Resource '" + resourceKey + "'");
         }
+
         AnyType anyType = anyTypeDAO.find(anyTypeKey);
         if (anyType == null) {
             throw new NotFoundException("AnyType '" + anyTypeKey + "'");
         }
-        Optional<? extends Provision> provision = resource.getProvision(anyType);
-        if (!provision.isPresent()) {
-            throw new NotFoundException("Provision on resource '" + resourceKey + "' for type '" + anyTypeKey + "'");
-        }
 
-        return ImmutableTriple.of(resource, anyType, provision.get());
+        Provision provision = resource.getProvision(anyType).
+                orElseThrow(() -> new NotFoundException(
+                "Provision on resource '" + resourceKey + "' for type '" + anyTypeKey + "'"));
+
+        return Pair.of(anyType, provision);
     }
 
-    @PreAuthorize("hasRole('" + StandardEntitlement.RESOURCE_GET_CONNOBJECT + "')")
-    @Transactional(readOnly = true)
-    public ConnObjectTO readConnObject(final String key, final String anyTypeKey, final String anyKey) {
-        Triple<ExternalResource, AnyType, Provision> init = connObjectInit(key, anyTypeKey);
+    private ConnObjectTO readConnObject(
+            final Provision provision,
+            final String connObjectKeyValue) {
 
-        // 1. find any
-        Any<?> any = init.getMiddle().getKind() == AnyTypeKind.USER
-                ? userDAO.find(anyKey)
-                : init.getMiddle().getKind() == AnyTypeKind.ANY_OBJECT
-                ? anyObjectDAO.find(anyKey)
-                : groupDAO.find(anyKey);
-        if (any == null) {
-            throw new NotFoundException(init.getMiddle() + " " + anyKey);
-        }
-
-        // 2. build connObjectKeyItem
-        MappingItem connObjectKeyItem = MappingUtils.getConnObjectKeyItem(init.getRight()).
-                orElseThrow(() -> new NotFoundException(
-                "ConnObjectKey mapping for " + init.getMiddle() + " " + anyKey + " on resource '" + key + "'"));
-        String connObjectKeyValue = mappingManager.getConnObjectKeyValue(any, init.getRight()).
+        // 0. build connObjectKeyItem
+        MappingItem connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision).
                 orElseThrow(() -> new NotFoundException(
-                "ConnObjectKey value for " + init.getMiddle() + " " + anyKey + " on resource '" + key + "'"));
+                "ConnObjectKey mapping for " + provision.getAnyType().getKey()
+                + " on resource '" + provision.getResource().getKey() + "'"));
 
-        // 3. determine attributes to query
-        Set<MappingItem> linkinMappingItems = virSchemaDAO.findByProvision(init.getRight()).stream().
+        // 1. determine attributes to query
+        Set<MappingItem> linkinMappingItems = virSchemaDAO.findByProvision(provision).stream().
                 map(virSchema -> virSchema.asLinkingMappingItem()).collect(Collectors.toSet());
         Iterator<MappingItem> mapItems = new IteratorChain<>(
-                init.getRight().getMapping().getItems().iterator(),
+                provision.getMapping().getItems().iterator(),
                 linkinMappingItems.iterator());
 
-        // 4. read from the underlying connector
-        Connector connector = connFactory.getConnector(init.getLeft());
+        // 2. read from the underlying connector
+        Connector connector = connFactory.getConnector(provision.getResource());
         ConnectorObject connectorObject = connector.getObject(
-                init.getRight().getObjectClass(),
+                provision.getObjectClass(),
                 AttributeBuilder.build(connObjectKeyItem.getExtAttrName(), connObjectKeyValue),
-                init.getRight().isIgnoreCaseMatch(),
+                provision.isIgnoreCaseMatch(),
                 MappingUtils.buildOperationOptions(mapItems));
         if (connectorObject == null) {
             throw new NotFoundException(
-                    "Object " + connObjectKeyValue + " with class " + init.getRight().getObjectClass()
-                    + " not found on resource " + key);
+                    "Object " + connObjectKeyValue + " with class " + provision.getObjectClass()
+                    + " not found on resource " + provision.getResource().getKey());
         }
 
-        // 5. build result
+        // 3. build result
         Set<Attribute> attributes = connectorObject.getAttributes();
         if (AttributeUtil.find(Uid.NAME, attributes) == null) {
             attributes.add(connectorObject.getUid());
@@ -350,10 +328,49 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
         return ConnObjectUtils.getConnObjectTO(connectorObject);
     }
 
+    @PreAuthorize("hasRole('" + StandardEntitlement.RESOURCE_GET_CONNOBJECT + "')")
+    @Transactional(readOnly = true)
+    public ConnObjectTO readConnObjectByAnyKey(
+            final String key,
+            final String anyTypeKey,
+            final String anyKey) {
+
+        Pair<AnyType, Provision> init = connObjectInit(key, anyTypeKey);
+
+        // 1. find any
+        Any<?> any = anyUtilsFactory.getInstance(init.getLeft().getKind()).dao().authFind(anyKey);
+        if (any == null) {
+            throw new NotFoundException(init.getLeft() + " " + anyKey);
+        }
+
+        // 2. find connObjectKeyValue
+        String connObjectKeyValue = mappingManager.getConnObjectKeyValue(any, init.getRight()).
+                orElseThrow(() -> new NotFoundException(
+                "ConnObjectKey value for " + init.getLeft() + " " + anyKey + " on resource '" + key + "'"));
+
+        return readConnObject(init.getRight(), connObjectKeyValue);
+    }
+
+    @PreAuthorize("hasRole('" + StandardEntitlement.RESOURCE_GET_CONNOBJECT + "')")
+    @Transactional(readOnly = true)
+    public ConnObjectTO readConnObjectByConnObjectKey(
+            final String key,
+            final String anyTypeKey,
+            final String connObjectKeyValue) {
+
+        Pair<AnyType, Provision> init = connObjectInit(key, anyTypeKey);
+        return readConnObject(init.getRight(), connObjectKeyValue);
+    }
+
     @PreAuthorize("hasRole('" + StandardEntitlement.RESOURCE_LIST_CONNOBJECT + "')")
     @Transactional(readOnly = true)
-    public Pair<SearchResult, List<ConnObjectTO>> listConnObjects(final String key, final String anyTypeKey,
-            final int size, final String pagedResultsCookie, final List<OrderByClause> orderBy) {
+    public Pair<SearchResult, List<ConnObjectTO>> searchConnObjects(
+            final Filter filter,
+            final String key,
+            final String anyTypeKey,
+            final int size,
+            final String pagedResultsCookie,
+            final List<OrderByClause> orderBy) {
 
         ExternalResource resource;
         ObjectClass objectClass;
@@ -371,8 +388,8 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
             options = MappingUtils.buildOperationOptions(
                     MappingUtils.getPropagationItems(resource.getOrgUnit().getItems()).iterator());
         } else {
-            Triple<ExternalResource, AnyType, Provision> init = connObjectInit(key, anyTypeKey);
-            resource = init.getLeft();
+            Pair<AnyType, Provision> init = connObjectInit(key, anyTypeKey);
+            resource = init.getRight().getResource();
             objectClass = init.getRight().getObjectClass();
             init.getRight().getMapping().getItems();
 
@@ -386,7 +403,7 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
 
         List<ConnObjectTO> connObjects = new ArrayList<>();
         SearchResult searchResult = connFactory.getConnector(resource).
-                search(objectClass, null, new SearchResultsHandler() {
+                search(objectClass, filter, new SearchResultsHandler() {
 
                     private int count;
 
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/FilterConverter.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/FilterConverter.java
new file mode 100644
index 0000000..124a478
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/FilterConverter.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.persistence.api.search;
+
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+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.search.AbstractFiqlSearchConditionBuilder;
+import org.apache.syncope.common.lib.search.SyncopeFiqlParser;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.identityconnectors.framework.common.objects.filter.Filter;
+
+/**
+ * Converts FIQL expressions to ConnId's {@link Filter}.
+ */
+public final class FilterConverter {
+
+    /**
+     * Parses a FIQL expression into ConnId's {@link Filter}, using {@link SyncopeFiqlParser}.
+     *
+     * @param fiql FIQL string
+     * @return {@link Filter} instance for given FIQL expression
+     */
+    public static Filter convert(final String fiql) {
+        SyncopeFiqlParser<SearchBean> parser = new SyncopeFiqlParser<>(
+                SearchBean.class, AbstractFiqlSearchConditionBuilder.CONTEXTUAL_PROPERTIES);
+
+        try {
+            FilterVisitor visitor = new FilterVisitor();
+            SearchCondition<SearchBean> sc = parser.parse(URLDecoder.decode(fiql, StandardCharsets.UTF_8.name()));
+            sc.accept(visitor);
+
+            return visitor.getQuery();
+        } catch (Exception e) {
+            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidSearchExpression);
+            sce.getElements().add(fiql);
+            sce.getElements().add(ExceptionUtils.getRootCauseMessage(e));
+            throw sce;
+        }
+    }
+
+    private FilterConverter() {
+        // empty constructor for static utility class        
+    }
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/FilterVisitor.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/FilterVisitor.java
new file mode 100644
index 0000000..73d3980
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/FilterVisitor.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.persistence.api.search;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import org.apache.cxf.jaxrs.ext.search.ConditionType;
+import org.apache.cxf.jaxrs.ext.search.SearchBean;
+import org.apache.cxf.jaxrs.ext.search.SearchCondition;
+import org.apache.cxf.jaxrs.ext.search.SearchUtils;
+import org.apache.cxf.jaxrs.ext.search.visitor.AbstractSearchConditionVisitor;
+import org.apache.syncope.common.lib.search.SpecialAttr;
+import org.apache.syncope.common.lib.search.SyncopeFiqlParser;
+import org.apache.syncope.common.lib.search.SyncopeFiqlSearchCondition;
+import org.identityconnectors.framework.common.objects.Attribute;
+import org.identityconnectors.framework.common.objects.AttributeBuilder;
+import org.identityconnectors.framework.common.objects.filter.Filter;
+import org.identityconnectors.framework.common.objects.filter.FilterBuilder;
+
+public class FilterVisitor extends AbstractSearchConditionVisitor<SearchBean, Filter> {
+
+    private Filter filter;
+
+    public FilterVisitor() {
+        super(null);
+    }
+
+    private Filter visitPrimitive(final SearchCondition<SearchBean> sc) {
+        String name = getRealPropertyName(sc.getStatement().getProperty());
+        Optional<SpecialAttr> specialAttrName = SpecialAttr.fromString(name);
+
+        String value = null;
+        try {
+            value = SearchUtils.toSqlWildcardString(
+                    URLDecoder.decode(sc.getStatement().getValue().toString(), StandardCharsets.UTF_8.name()), false).
+                    replaceAll("\\\\_", "_");
+        } catch (UnsupportedEncodingException e) {
+            throw new IllegalArgumentException("While decoding " + sc.getStatement().getValue(), e);
+        }
+        Optional<SpecialAttr> specialAttrValue = SpecialAttr.fromString(value);
+
+        ConditionType ct = sc.getConditionType();
+        if (sc instanceof SyncopeFiqlSearchCondition && sc.getConditionType() == ConditionType.CUSTOM) {
+            SyncopeFiqlSearchCondition<SearchBean> sfsc = (SyncopeFiqlSearchCondition<SearchBean>) sc;
+            switch (sfsc.getOperator()) {
+                case SyncopeFiqlParser.IEQ:
+                    ct = ConditionType.EQUALS;
+                    break;
+
+                case SyncopeFiqlParser.NIEQ:
+                    ct = ConditionType.NOT_EQUALS;
+                    break;
+
+                default:
+                    throw new IllegalArgumentException(
+                            String.format("Condition type %s is not supported", sfsc.getOperator()));
+            }
+        }
+
+        Attribute attr = AttributeBuilder.build(name, value);
+
+        Filter leaf;
+        switch (ct) {
+            case EQUALS:
+            case NOT_EQUALS:
+                if (!specialAttrName.isPresent()) {
+                    if (specialAttrValue.isPresent() && specialAttrValue.get() == SpecialAttr.NULL) {
+                        leaf = FilterBuilder.equalTo(AttributeBuilder.build(name));
+                    } else if (value.indexOf('%') == -1) {
+                        leaf = sc.getConditionType() == ConditionType.CUSTOM
+                                ? FilterBuilder.equalsIgnoreCase(attr)
+                                : FilterBuilder.equalTo(attr);
+                    } else if (sc.getConditionType() != ConditionType.CUSTOM && value.startsWith("%")) {
+                        leaf = FilterBuilder.endsWith(
+                                AttributeBuilder.build(name, value.substring(1)));
+                    } else if (sc.getConditionType() != ConditionType.CUSTOM && value.endsWith("%")) {
+                        leaf = FilterBuilder.startsWith(
+                                AttributeBuilder.build(name, value.substring(0, value.length() - 1)));
+                    } else {
+                        throw new IllegalArgumentException(
+                                String.format("Unsupported search value %s", value));
+                    }
+                } else {
+                    throw new IllegalArgumentException(
+                            String.format("Special attr name %s is not supported", specialAttrName));
+                }
+                if (ct == ConditionType.NOT_EQUALS) {
+                    leaf = FilterBuilder.not(leaf);
+                }
+                break;
+
+            case GREATER_OR_EQUALS:
+                leaf = FilterBuilder.greaterThanOrEqualTo(attr);
+                break;
+
+            case GREATER_THAN:
+                leaf = FilterBuilder.greaterThan(attr);
+                break;
+
+            case LESS_OR_EQUALS:
+                leaf = FilterBuilder.lessThanOrEqualTo(attr);
+                break;
+
+            case LESS_THAN:
+                leaf = FilterBuilder.lessThan(attr);
+                break;
+
+            default:
+                throw new IllegalArgumentException(String.format("Condition type %s is not supported", ct.name()));
+        }
+
+        return leaf;
+    }
+
+    private Filter visitCompount(final SearchCondition<SearchBean> sc) {
+        List<Filter> searchConds = new ArrayList<>();
+        sc.getSearchConditions().forEach(searchCond -> {
+            searchConds.add(searchCond.getStatement() == null
+                    ? visitCompount(searchCond)
+                    : visitPrimitive(searchCond));
+        });
+
+        Filter compound;
+        switch (sc.getConditionType()) {
+            case AND:
+                compound = FilterBuilder.and(searchConds);
+                break;
+
+            case OR:
+                compound = FilterBuilder.or(searchConds);
+                break;
+
+            default:
+                throw new IllegalArgumentException(
+                        String.format("Condition type %s is not supported", sc.getConditionType().name()));
+        }
+
+        return compound;
+    }
+
+    @Override
+    public void visit(final SearchCondition<SearchBean> sc) {
+        filter = sc.getStatement() == null
+                ? visitCompount(sc)
+                : visitPrimitive(sc);
+    }
+
+    @Override
+    public Filter getQuery() {
+        return filter;
+    }
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondVisitor.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondVisitor.java
index 7d01f8a..8f6a097 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondVisitor.java
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondVisitor.java
@@ -103,13 +103,18 @@ public class SearchCondVisitor extends AbstractSearchConditionVisitor<SearchBean
         ConditionType ct = sc.getConditionType();
         if (sc instanceof SyncopeFiqlSearchCondition && sc.getConditionType() == ConditionType.CUSTOM) {
             SyncopeFiqlSearchCondition<SearchBean> sfsc = (SyncopeFiqlSearchCondition<SearchBean>) sc;
-            if (SyncopeFiqlParser.IEQ.equals(sfsc.getOperator())) {
-                ct = ConditionType.EQUALS;
-            } else if (SyncopeFiqlParser.NIEQ.equals(sfsc.getOperator())) {
-                ct = ConditionType.NOT_EQUALS;
-            } else {
-                throw new IllegalArgumentException(
-                        String.format("Condition type %s is not supported", sfsc.getOperator()));
+            switch (sfsc.getOperator()) {
+                case SyncopeFiqlParser.IEQ:
+                    ct = ConditionType.EQUALS;
+                    break;
+
+                case SyncopeFiqlParser.NIEQ:
+                    ct = ConditionType.NOT_EQUALS;
+                    break;
+
+                default:
+                    throw new IllegalArgumentException(
+                            String.format("Condition type %s is not supported", sfsc.getOperator()));
             }
         }
 
@@ -257,10 +262,10 @@ public class SearchCondVisitor extends AbstractSearchConditionVisitor<SearchBean
 
     private SearchCond visitCompount(final SearchCondition<SearchBean> sc) {
         List<SearchCond> searchConds = new ArrayList<>();
-        sc.getSearchConditions().forEach(searchCondition -> {
-            searchConds.add(searchCondition.getStatement() == null
-                    ? visitCompount(searchCondition)
-                    : visitPrimitive(searchCondition));
+        sc.getSearchConditions().forEach(searchCond -> {
+            searchConds.add(searchCond.getStatement() == null
+                    ? visitCompount(searchCond)
+                    : visitPrimitive(searchCond));
         });
 
         SearchCond compound;
diff --git a/core/persistence-api/src/test/java/org/apache/syncope/core/persistence/api/search/FilterConverterTest.java b/core/persistence-api/src/test/java/org/apache/syncope/core/persistence/api/search/FilterConverterTest.java
new file mode 100644
index 0000000..40c9b69
--- /dev/null
+++ b/core/persistence-api/src/test/java/org/apache/syncope/core/persistence/api/search/FilterConverterTest.java
@@ -0,0 +1,289 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.persistence.api.search;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.List;
+import java.util.ListIterator;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.search.SpecialAttr;
+import org.apache.syncope.common.lib.search.ConnObjectTOFiqlSearchConditionBuilder;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.identityconnectors.framework.common.objects.AttributeBuilder;
+import org.identityconnectors.framework.common.objects.filter.AndFilter;
+import org.identityconnectors.framework.common.objects.filter.Filter;
+import org.identityconnectors.framework.common.objects.filter.FilterBuilder;
+import org.identityconnectors.framework.common.objects.filter.NotFilter;
+import org.identityconnectors.framework.common.objects.filter.OrFilter;
+import org.junit.jupiter.api.Test;
+
+public class FilterConverterTest {
+
+    private boolean equals(final Filter filter1, final Filter filter2) {
+        return EqualsBuilder.reflectionEquals(filter1, filter2);
+    }
+
+    private boolean equals(final List<Filter> filters1, final List<Filter> filters2) {
+        ListIterator<Filter> e1 = filters1.listIterator();
+        ListIterator<Filter> e2 = filters2.listIterator();
+        while (e1.hasNext() && e2.hasNext()) {
+            Filter o1 = e1.next();
+            Filter o2 = e2.next();
+            if (!equals(o1, o2)) {
+                return false;
+            }
+        }
+        return !(e1.hasNext() || e2.hasNext());
+    }
+
+    @Test
+    public void eq() {
+        String fiql = new ConnObjectTOFiqlSearchConditionBuilder().is("username").equalTo("rossini").query();
+        assertEquals("username==rossini", fiql);
+
+        Filter filter = FilterBuilder.equalTo(AttributeBuilder.build("username", "rossini"));
+
+        assertTrue(equals(filter, FilterConverter.convert(fiql)));
+    }
+
+    @Test
+    public void ieq() {
+        String fiql = new ConnObjectTOFiqlSearchConditionBuilder().is("username").equalToIgnoreCase("rossini").query();
+        assertEquals("username=~rossini", fiql);
+
+        Filter filter = FilterBuilder.equalsIgnoreCase(AttributeBuilder.build("username", "rossini"));
+
+        assertTrue(equals(filter, FilterConverter.convert(fiql)));
+    }
+
+    @Test
+    public void nieq() {
+        String fiql = new ConnObjectTOFiqlSearchConditionBuilder().is("username").notEqualTolIgnoreCase("rossini").
+                query();
+        assertEquals("username!~rossini", fiql);
+
+        Filter filter = FilterBuilder.not(
+                FilterBuilder.equalsIgnoreCase(AttributeBuilder.build("username", "rossini")));
+        assertTrue(filter instanceof NotFilter);
+
+        Filter converted = FilterConverter.convert(fiql);
+        assertTrue(converted instanceof NotFilter);
+
+        assertTrue(equals(
+                ((NotFilter) filter).getFilter(), ((NotFilter) converted).getFilter()));
+    }
+
+    @Test
+    public void like() {
+        String fiql = new ConnObjectTOFiqlSearchConditionBuilder().is("username").equalTo("ros*").query();
+        assertEquals("username==ros*", fiql);
+
+        Filter filter = FilterBuilder.startsWith(AttributeBuilder.build("username", "ros"));
+
+        assertTrue(equals(filter, FilterConverter.convert(fiql)));
+
+        fiql = new ConnObjectTOFiqlSearchConditionBuilder().is("username").equalTo("*ini").query();
+        assertEquals("username==*ini", fiql);
+
+        filter = FilterBuilder.endsWith(AttributeBuilder.build("username", "ini"));
+
+        assertTrue(equals(filter, FilterConverter.convert(fiql)));
+
+        fiql = new ConnObjectTOFiqlSearchConditionBuilder().is("username").equalTo("r*ini").query();
+        assertEquals("username==r*ini", fiql);
+
+        try {
+            FilterConverter.convert(fiql);
+            fail();
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.InvalidSearchExpression, e.getType());
+        }
+    }
+
+    @Test
+    public void ilike() {
+        String fiql = new ConnObjectTOFiqlSearchConditionBuilder().is("username").equalToIgnoreCase("ros*").query();
+        assertEquals("username=~ros*", fiql);
+
+        try {
+            FilterConverter.convert(fiql);
+            fail();
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.InvalidSearchExpression, e.getType());
+        }
+    }
+
+    @Test
+    public void nilike() {
+        String fiql = new ConnObjectTOFiqlSearchConditionBuilder().is("username").notEqualTolIgnoreCase("ros*").query();
+        assertEquals("username!~ros*", fiql);
+
+        try {
+            FilterConverter.convert(fiql);
+            fail();
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.InvalidSearchExpression, e.getType());
+        }
+    }
+
+    @Test
+    public void isNull() {
+        String fiql = new ConnObjectTOFiqlSearchConditionBuilder().is("loginDate").nullValue().query();
+        assertEquals("loginDate==" + SpecialAttr.NULL, fiql);
+
+        Filter filter = FilterBuilder.equalTo(AttributeBuilder.build("loginDate"));
+
+        assertTrue(equals(filter, FilterConverter.convert(fiql)));
+    }
+
+    @Test
+    public void isNotNull() {
+        String fiql = new ConnObjectTOFiqlSearchConditionBuilder().is("loginDate").notNullValue().query();
+        assertEquals("loginDate!=" + SpecialAttr.NULL, fiql);
+
+        Filter filter = FilterBuilder.not(FilterBuilder.equalTo(AttributeBuilder.build("loginDate")));
+        assertTrue(filter instanceof NotFilter);
+
+        Filter converted = FilterConverter.convert(fiql);
+        assertTrue(converted instanceof NotFilter);
+
+        assertTrue(equals(
+                ((NotFilter) filter).getFilter(), ((NotFilter) converted).getFilter()));
+    }
+
+    @Test
+    public void inDynRealms() {
+        try {
+            new ConnObjectTOFiqlSearchConditionBuilder().inDynRealms("realm").query();
+            fail();
+        } catch (UnsupportedOperationException e) {
+            assertNotNull(e);
+        }
+
+        try {
+            FilterConverter.convert(SpecialAttr.DYNREALMS + "==realm");
+            fail();
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.InvalidSearchExpression, e.getType());
+        }
+    }
+
+    @Test
+    public void notInDynRealms() {
+        try {
+            new ConnObjectTOFiqlSearchConditionBuilder().notInDynRealms("realm").query();
+            fail();
+        } catch (UnsupportedOperationException e) {
+            assertNotNull(e);
+        }
+
+        try {
+            FilterConverter.convert(SpecialAttr.DYNREALMS + "!=realm");
+            fail();
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.InvalidSearchExpression, e.getType());
+        }
+    }
+
+    @Test
+    public void hasResources() {
+        try {
+            new ConnObjectTOFiqlSearchConditionBuilder().hasResources("resource").query();
+            fail();
+        } catch (UnsupportedOperationException e) {
+            assertNotNull(e);
+        }
+
+        try {
+            FilterConverter.convert(SpecialAttr.RESOURCES + "==resource");
+            fail();
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.InvalidSearchExpression, e.getType());
+        }
+    }
+
+    @Test
+    public void hasNotResources() {
+        try {
+            new ConnObjectTOFiqlSearchConditionBuilder().hasNotResources("resource").query();
+            fail();
+        } catch (UnsupportedOperationException e) {
+            assertNotNull(e);
+        }
+
+        try {
+            FilterConverter.convert(SpecialAttr.RESOURCES + "!=resource");
+            fail();
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.InvalidSearchExpression, e.getType());
+        }
+    }
+
+    @Test
+    public void and() {
+        String fiql = new ConnObjectTOFiqlSearchConditionBuilder().
+                is("fullname").equalTo("ro*").and("fullname").equalTo("*i").query();
+        assertEquals("fullname==ro*;fullname==*i", fiql);
+
+        Filter filter1 = FilterBuilder.startsWith(AttributeBuilder.build("fullname", "ro"));
+        Filter filter2 = FilterBuilder.endsWith(AttributeBuilder.build("fullname", "i"));
+
+        Filter filter = FilterBuilder.and(filter1, filter2);
+        assertTrue(filter instanceof AndFilter);
+
+        Filter converted = FilterConverter.convert(fiql);
+        assertTrue(converted instanceof AndFilter);
+
+        assertTrue(equals(
+                (List<Filter>) ((AndFilter) filter).getFilters(), (List<Filter>) ((AndFilter) converted).getFilters()));
+    }
+
+    @Test
+    public void or() {
+        String fiql = new ConnObjectTOFiqlSearchConditionBuilder().
+                is("fullname").equalTo("ro*").or("fullname").equalTo("*i").query();
+        assertEquals("fullname==ro*,fullname==*i", fiql);
+
+        Filter filter1 = FilterBuilder.startsWith(AttributeBuilder.build("fullname", "ro"));
+        Filter filter2 = FilterBuilder.endsWith(AttributeBuilder.build("fullname", "i"));
+
+        Filter filter = FilterBuilder.or(filter1, filter2);
+        assertTrue(filter instanceof OrFilter);
+
+        Filter converted = FilterConverter.convert(fiql);
+        assertTrue(converted instanceof OrFilter);
+
+        assertTrue(equals(
+                (List<Filter>) ((OrFilter) filter).getFilters(), (List<Filter>) ((OrFilter) converted).getFilters()));
+    }
+
+    @Test
+    public void issueSYNCOPE1223() {
+        String fiql = new ConnObjectTOFiqlSearchConditionBuilder().is("ctype").equalTo("ou=sample%252Co=isp").query();
+
+        Filter filter = FilterBuilder.equalTo(AttributeBuilder.build("ctype", "ou=sample,o=isp"));
+
+        assertTrue(equals(filter, FilterConverter.convert(fiql)));
+    }
+}
diff --git a/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java
index 2076fbb..ea255de 100644
--- a/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java
+++ b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java
@@ -25,15 +25,23 @@ import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
 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.ConnObjectTO;
 import org.apache.syncope.common.lib.to.PagedConnObjectTOResult;
 import org.apache.syncope.common.lib.to.ResourceTO;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.rest.api.RESTHeaders;
-import org.apache.syncope.common.rest.api.beans.ConnObjectTOListQuery;
+import org.apache.syncope.common.rest.api.beans.ConnObjectTOQuery;
 import org.apache.syncope.common.rest.api.service.ResourceService;
 import org.apache.syncope.core.logic.ResourceLogic;
+import org.apache.syncope.core.persistence.api.search.FilterVisitor;
 import org.identityconnectors.framework.common.objects.SearchResult;
+import org.identityconnectors.framework.common.objects.filter.Filter;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -83,16 +91,41 @@ public class ResourceServiceImpl extends AbstractServiceImpl implements Resource
     }
 
     @Override
-    public ConnObjectTO readConnObject(final String key, final String anyTypeKey, final String anyKey) {
-        return logic.readConnObject(key, anyTypeKey, anyKey);
+    public ConnObjectTO readConnObject(final String key, final String anyTypeKey, final String value) {
+        return SyncopeConstants.UUID_PATTERN.matcher(value).matches()
+                ? logic.readConnObjectByAnyKey(key, anyTypeKey, value)
+                : logic.readConnObjectByConnObjectKey(key, anyTypeKey, value);
     }
 
     @Override
-    public PagedConnObjectTOResult listConnObjects(
-            final String key, final String anyTypeKey, final ConnObjectTOListQuery listQuery) {
+    public PagedConnObjectTOResult searchConnObjects(
+            final String key, final String anyTypeKey, final ConnObjectTOQuery query) {
+
+        Filter filter = null;
+        if (StringUtils.isNotBlank(query.getFiql())) {
+            try {
+                FilterVisitor visitor = new FilterVisitor();
+                SearchCondition<SearchBean> sc = searchContext.getCondition(query.getFiql(), SearchBean.class);
+                sc.accept(visitor);
+
+                filter = visitor.getQuery();
+            } catch (Exception e) {
+                LOG.error("Invalid FIQL expression: {}", query.getFiql(), e);
+
+                SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidSearchExpression);
+                sce.getElements().add(query.getFiql());
+                sce.getElements().add(ExceptionUtils.getRootCauseMessage(e));
+                throw sce;
+            }
+        }
 
-        Pair<SearchResult, List<ConnObjectTO>> list = logic.listConnObjects(key, anyTypeKey,
-                listQuery.getSize(), listQuery.getPagedResultsCookie(), getOrderByClauses(listQuery.getOrderBy()));
+        Pair<SearchResult, List<ConnObjectTO>> list = logic.searchConnObjects(
+                filter,
+                key,
+                anyTypeKey,
+                query.getSize(),
+                query.getPagedResultsCookie(),
+                getOrderByClauses(query.getOrderBy()));
 
         PagedConnObjectTOResult result = new PagedConnObjectTOResult();
         if (list.getLeft() != null) {
@@ -111,7 +144,7 @@ public class ResourceServiceImpl extends AbstractServiceImpl implements Resource
         if (StringUtils.isNotBlank(result.getPagedResultsCookie())) {
             result.setNext(builder.
                     replaceQueryParam(PARAM_CONNID_PAGED_RESULTS_COOKIE, result.getPagedResultsCookie()).
-                    replaceQueryParam(PARAM_SIZE, listQuery.getSize()).
+                    replaceQueryParam(PARAM_SIZE, query.getSize()).
                     build());
         }
 
diff --git a/ext/flowable/client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestsResource.java b/ext/flowable/client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestsResource.java
index 6a25c5f..ab6f054 100644
--- a/ext/flowable/client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestsResource.java
+++ b/ext/flowable/client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestsResource.java
@@ -18,10 +18,6 @@
  */
 package org.apache.syncope.client.enduser.resources;
 
-import org.apache.syncope.client.enduser.model.UserRequestWrapper;
-
-import static org.apache.syncope.client.enduser.resources.BaseResource.LOG;
-
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.stream.Collectors;
@@ -31,6 +27,7 @@ import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import org.apache.syncope.client.enduser.SyncopeEnduserSession;
 import org.apache.syncope.client.enduser.annotations.Resource;
+import org.apache.syncope.client.enduser.model.UserRequestWrapper;
 import org.apache.syncope.common.lib.BaseBean;
 import org.apache.syncope.common.lib.to.PagedResult;
 import org.apache.syncope.common.lib.to.UserRequest;
diff --git a/ext/flowable/client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestsStartResource.java b/ext/flowable/client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestsStartResource.java
index 4c65768..fd2503a 100644
--- a/ext/flowable/client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestsStartResource.java
+++ b/ext/flowable/client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestsStartResource.java
@@ -18,8 +18,6 @@
  */
 package org.apache.syncope.client.enduser.resources;
 
-import static org.apache.syncope.client.enduser.resources.BaseResource.LOG;
-
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import javax.servlet.http.HttpServletRequest;
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LinkedAccountITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LinkedAccountITCase.java
index b7b3320..49c7c82 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LinkedAccountITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LinkedAccountITCase.java
@@ -30,8 +30,6 @@ import java.util.List;
 import java.util.Optional;
 import java.util.UUID;
 import javax.naming.NamingException;
-import javax.naming.directory.Attributes;
-import javax.naming.ldap.LdapContext;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
@@ -44,6 +42,7 @@ import org.apache.syncope.common.lib.patch.LinkedAccountPatch;
 import org.apache.syncope.common.lib.patch.UserPatch;
 import org.apache.syncope.common.lib.policy.PullPolicyTO;
 import org.apache.syncope.common.lib.to.AttrTO;
+import org.apache.syncope.common.lib.to.ConnObjectTO;
 import org.apache.syncope.common.lib.to.ExecTO;
 import org.apache.syncope.common.lib.to.ImplementationTO;
 import org.apache.syncope.common.lib.to.LinkedAccountTO;
@@ -102,15 +101,11 @@ public class LinkedAccountITCase extends AbstractITCase {
         assertEquals(ResourceOperation.CREATE, tasks.getResult().get(0).getOperation());
         assertEquals(ExecStatus.SUCCESS.name(), tasks.getResult().get(0).getLatestExecStatus());
 
-        LdapContext ldapObj = (LdapContext) getLdapRemoteObject(
-                RESOURCE_LDAP_ADMIN_DN, RESOURCE_LDAP_ADMIN_PWD, connObjectKeyValue);
+        ConnObjectTO ldapObj = resourceService.readConnObject(
+                RESOURCE_NAME_LDAP, AnyTypeKind.USER.name(), connObjectKeyValue);
         assertNotNull(ldapObj);
-
-        Attributes ldapAttrs = ldapObj.getAttributes("");
-        assertEquals(
-                user.getPlainAttr("email").get().getValues().get(0),
-                ldapAttrs.get("mail").getAll().next().toString());
-        assertEquals("LINKED_SURNAME", ldapAttrs.get("sn").getAll().next().toString());
+        assertEquals(user.getPlainAttr("email").get().getValues(), ldapObj.getAttr("mail").get().getValues());
+        assertEquals("LINKED_SURNAME", ldapObj.getAttr("sn").get().getValues().get(0));
 
         // 3. update linked account
         UserPatch userPatch = new UserPatch();
@@ -125,12 +120,11 @@ public class LinkedAccountITCase extends AbstractITCase {
         assertEquals(1, user.getLinkedAccounts().size());
 
         // 4 verify that account was updated on resource
-        ldapObj = (LdapContext) getLdapRemoteObject(RESOURCE_LDAP_ADMIN_DN, RESOURCE_LDAP_ADMIN_PWD, connObjectKeyValue);
+        ldapObj = resourceService.readConnObject(RESOURCE_NAME_LDAP, AnyTypeKind.USER.name(), connObjectKeyValue);
         assertNotNull(ldapObj);
 
-        ldapAttrs = ldapObj.getAttributes("");
-        assertEquals("UPDATED_EMAIL@syncope.apache.org", ldapAttrs.get("mail").getAll().next().toString());
-        assertEquals("UPDATED_SURNAME", ldapAttrs.get("sn").getAll().next().toString());
+        assertTrue(ldapObj.getAttr("mail").get().getValues().contains("UPDATED_EMAIL@syncope.apache.org"));
+        assertEquals("UPDATED_SURNAME", ldapObj.getAttr("sn").get().getValues().get(0));
 
         // 5. remove linked account from user
         userPatch = new UserPatch();
@@ -196,15 +190,11 @@ public class LinkedAccountITCase extends AbstractITCase {
         assertEquals(ResourceOperation.CREATE, tasks.getResult().get(0).getOperation());
         assertEquals(ExecStatus.SUCCESS.name(), tasks.getResult().get(0).getLatestExecStatus());
 
-        LdapContext ldapObj = (LdapContext) getLdapRemoteObject(
-                RESOURCE_LDAP_ADMIN_DN, RESOURCE_LDAP_ADMIN_PWD, connObjectKeyValue);
+        ConnObjectTO ldapObj = resourceService.readConnObject(
+                RESOURCE_NAME_LDAP, AnyTypeKind.USER.name(), connObjectKeyValue);
         assertNotNull(ldapObj);
-
-        Attributes ldapAttrs = ldapObj.getAttributes("");
-        assertEquals(
-                user.getPlainAttr("email").get().getValues().get(0),
-                ldapAttrs.get("mail").getAll().next().toString());
-        assertEquals("LINKED_SURNAME", ldapAttrs.get("sn").getAll().next().toString());
+        assertEquals(user.getPlainAttr("email").get().getValues(), ldapObj.getAttr("mail").get().getValues());
+        assertEquals("LINKED_SURNAME", ldapObj.getAttr("sn").get().getValues().get(0));
     }
 
     @Test
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ResourceITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ResourceITCase.java
index 2515264..d892acf 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ResourceITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ResourceITCase.java
@@ -20,34 +20,26 @@ package org.apache.syncope.fit.core;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
-import java.util.stream.Collectors;
 import javax.ws.rs.core.Response;
-import org.apache.commons.lang3.SerializationUtils;
-import org.apache.syncope.client.console.commons.ConnIdSpecialName;
 import org.apache.syncope.client.lib.SyncopeClient;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.to.AnyObjectTO;
-import org.apache.syncope.common.lib.to.GroupTO;
 import org.apache.syncope.common.lib.to.ItemTO;
 import org.apache.syncope.common.lib.to.MappingTO;
 import org.apache.syncope.common.lib.to.OrgUnitTO;
-import org.apache.syncope.common.lib.to.PagedConnObjectTOResult;
 import org.apache.syncope.common.lib.to.ProvisionTO;
-import org.apache.syncope.common.lib.to.ResourceHistoryConfTO;
 import org.apache.syncope.common.lib.to.ResourceTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
@@ -57,7 +49,6 @@ import org.apache.syncope.common.lib.types.EntityViolationType;
 import org.apache.syncope.common.lib.types.ImplementationType;
 import org.apache.syncope.common.lib.types.MappingPurpose;
 import org.apache.syncope.common.lib.types.TraceLevel;
-import org.apache.syncope.common.rest.api.beans.ConnObjectTOListQuery;
 import org.apache.syncope.common.rest.api.service.ResourceService;
 import org.identityconnectors.framework.common.objects.ObjectClass;
 import org.apache.syncope.fit.AbstractITCase;
@@ -514,96 +505,6 @@ public class ResourceITCase extends AbstractITCase {
     }
 
     @Test
-    public void listConnObjects() {
-        List<String> groupKeys = new ArrayList<>();
-        for (int i = 0; i < 10; i++) {
-            GroupTO group = GroupITCase.getSampleTO("group");
-            group.getResources().add(RESOURCE_NAME_LDAP);
-            group = createGroup(group).getEntity();
-            groupKeys.add(group.getKey());
-        }
-
-        int totalRead = 0;
-        Set<String> read = new HashSet<>();
-        try {
-            ConnObjectTOListQuery.Builder builder = new ConnObjectTOListQuery.Builder().size(10);
-            PagedConnObjectTOResult list;
-            do {
-                list = null;
-
-                boolean succeeded = false;
-                // needed because ApacheDS seems to randomly fail when searching with cookie
-                for (int i = 0; i < 5 && !succeeded; i++) {
-                    try {
-                        list = resourceService.listConnObjects(
-                                RESOURCE_NAME_LDAP,
-                                AnyTypeKind.GROUP.name(),
-                                builder.build());
-                        succeeded = true;
-                    } catch (SyncopeClientException e) {
-                        assertEquals(ClientExceptionType.ConnectorException, e.getType());
-                    }
-                }
-                assertNotNull(list);
-
-                totalRead += list.getResult().size();
-                read.addAll(list.getResult().stream().
-                        map(input -> input.getAttr(ConnIdSpecialName.NAME).get().getValues().get(0)).
-                        collect(Collectors.toList()));
-
-                if (list.getPagedResultsCookie() != null) {
-                    builder.pagedResultsCookie(list.getPagedResultsCookie());
-                }
-            } while (list.getPagedResultsCookie() != null);
-
-            assertEquals(totalRead, read.size());
-            assertTrue(totalRead >= 10);
-        } finally {
-            groupKeys.forEach(key -> {
-                groupService.delete(key);
-            });
-        }
-    }
-
-    @Test
-    public void history() {
-        List<ResourceHistoryConfTO> history = resourceHistoryService.list(RESOURCE_NAME_LDAP);
-        assertNotNull(history);
-        int pre = history.size();
-
-        ResourceTO ldap = resourceService.read(RESOURCE_NAME_LDAP);
-        TraceLevel originalTraceLevel = SerializationUtils.clone(ldap.getUpdateTraceLevel());
-        assertEquals(TraceLevel.ALL, originalTraceLevel);
-        ProvisionTO originalProvision = SerializationUtils.clone(ldap.getProvision(AnyTypeKind.USER.name()).get());
-        assertEquals(ObjectClass.ACCOUNT_NAME, originalProvision.getObjectClass());
-        boolean originalFlag = ldap.isRandomPwdIfNotProvided();
-        assertTrue(originalFlag);
-
-        ldap.setUpdateTraceLevel(TraceLevel.FAILURES);
-        ldap.getProvision(AnyTypeKind.USER.name()).get().setObjectClass("ANOTHER");
-        ldap.setRandomPwdIfNotProvided(false);
-        resourceService.update(ldap);
-
-        ldap = resourceService.read(RESOURCE_NAME_LDAP);
-        assertNotEquals(originalTraceLevel, ldap.getUpdateTraceLevel());
-        assertNotEquals(
-                originalProvision.getObjectClass(), ldap.getProvision(AnyTypeKind.USER.name()).get().getObjectClass());
-        assertNotEquals(originalFlag, ldap.isRandomPwdIfNotProvided());
-
-        history = resourceHistoryService.list(RESOURCE_NAME_LDAP);
-        assertEquals(pre + 1, history.size());
-
-        resourceHistoryService.restore(history.get(0).getKey());
-
-        ldap = resourceService.read(RESOURCE_NAME_LDAP);
-        assertEquals(originalTraceLevel, ldap.getUpdateTraceLevel());
-        assertEquals(
-                originalProvision.getObjectClass(),
-                ldap.getProvision(AnyTypeKind.USER.name()).get().getObjectClass());
-        assertEquals(originalFlag, ldap.isRandomPwdIfNotProvided());
-    }
-
-    @Test
     public void authorizations() {
         SyncopeClient puccini = clientFactory.create("puccini", ADMIN_PWD);
         ResourceService prs = puccini.getService(ResourceService.class);
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
index 4c5bb76..5eb40d2 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
@@ -25,8 +25,14 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
 import javax.ws.rs.core.Response;
 import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.syncope.client.console.commons.ConnIdSpecialName;
 import org.apache.syncope.client.lib.SyncopeClient;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.SyncopeConstants;
@@ -34,19 +40,24 @@ import org.apache.syncope.common.lib.patch.AnyObjectPatch;
 import org.apache.syncope.common.lib.patch.AttrPatch;
 import org.apache.syncope.common.lib.patch.MembershipPatch;
 import org.apache.syncope.common.lib.patch.UserPatch;
+import org.apache.syncope.common.lib.search.ConnObjectTOFiqlSearchConditionBuilder;
 import org.apache.syncope.common.lib.to.AnyObjectTO;
 import org.apache.syncope.common.lib.to.AnyTypeTO;
+import org.apache.syncope.common.lib.to.ConnObjectTO;
 import org.apache.syncope.common.lib.to.PagedResult;
 import org.apache.syncope.common.lib.to.GroupTO;
 import org.apache.syncope.common.lib.to.MembershipTO;
+import org.apache.syncope.common.lib.to.PagedConnObjectTOResult;
 import org.apache.syncope.common.lib.to.RoleTO;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.rest.api.beans.AnyQuery;
+import org.apache.syncope.common.rest.api.beans.ConnObjectTOQuery;
 import org.apache.syncope.common.rest.api.service.RoleService;
 import org.apache.syncope.fit.AbstractITCase;
 import org.apache.syncope.fit.ElasticsearchDetector;
+import org.identityconnectors.framework.common.objects.Name;
 import org.junit.jupiter.api.Test;
 
 public class SearchITCase extends AbstractITCase {
@@ -447,6 +458,107 @@ public class SearchITCase extends AbstractITCase {
     }
 
     @Test
+    public void searchConnObjectsBrowsePagedResult() {
+        List<String> groupKeys = new ArrayList<>();
+        for (int i = 0; i < 10; i++) {
+            GroupTO group = GroupITCase.getSampleTO("group");
+            group.getResources().add(RESOURCE_NAME_LDAP);
+            group = createGroup(group).getEntity();
+            groupKeys.add(group.getKey());
+        }
+
+        int totalRead = 0;
+        Set<String> read = new HashSet<>();
+        try {
+            // 1. first search with no filters
+            ConnObjectTOQuery.Builder builder = new ConnObjectTOQuery.Builder().size(10);
+            PagedConnObjectTOResult matches;
+            do {
+                matches = null;
+
+                boolean succeeded = false;
+                // needed because ApacheDS seems to randomly fail when searching with cookie
+                for (int i = 0; i < 5 && !succeeded; i++) {
+                    try {
+                        matches = resourceService.searchConnObjects(
+                                RESOURCE_NAME_LDAP,
+                                AnyTypeKind.GROUP.name(),
+                                builder.build());
+                        succeeded = true;
+                    } catch (SyncopeClientException e) {
+                        assertEquals(ClientExceptionType.ConnectorException, e.getType());
+                    }
+                }
+                assertNotNull(matches);
+
+                totalRead += matches.getResult().size();
+                read.addAll(matches.getResult().stream().
+                        map(input -> input.getAttr(ConnIdSpecialName.NAME).get().getValues().get(0)).
+                        collect(Collectors.toList()));
+
+                if (matches.getPagedResultsCookie() != null) {
+                    builder.pagedResultsCookie(matches.getPagedResultsCookie());
+                }
+            } while (matches.getPagedResultsCookie() != null);
+
+            assertEquals(totalRead, read.size());
+            assertTrue(totalRead >= 10);
+        } finally {
+            groupKeys.forEach(key -> {
+                groupService.delete(key);
+            });
+        }
+    }
+
+    @Test
+    public void searchConnObjectsWithFilter() {
+        ConnObjectTO user = resourceService.readConnObject(RESOURCE_NAME_LDAP, AnyTypeKind.USER.name(), "pullFromLDAP");
+        assertNotNull(user);
+
+        PagedConnObjectTOResult matches = resourceService.searchConnObjects(
+                RESOURCE_NAME_LDAP,
+                AnyTypeKind.USER.name(),
+                new ConnObjectTOQuery.Builder().size(100).fiql(new ConnObjectTOFiqlSearchConditionBuilder().
+                        is("givenName").equalTo("pullFromLDAP").query()).build());
+        assertTrue(matches.getResult().contains(user));
+
+        matches = resourceService.searchConnObjects(
+                RESOURCE_NAME_LDAP,
+                AnyTypeKind.USER.name(),
+                new ConnObjectTOQuery.Builder().size(100).fiql(new ConnObjectTOFiqlSearchConditionBuilder().
+                        is("mail").equalTo("pullFromLDAP*").query()).build());
+        assertTrue(matches.getResult().contains(user));
+
+        matches = resourceService.searchConnObjects(
+                RESOURCE_NAME_LDAP,
+                AnyTypeKind.USER.name(),
+                new ConnObjectTOQuery.Builder().size(100).fiql(new ConnObjectTOFiqlSearchConditionBuilder().
+                        is("mail").equalTo("*@syncope.apache.org").query()).build());
+        assertTrue(matches.getResult().contains(user));
+
+        matches = resourceService.searchConnObjects(
+                RESOURCE_NAME_LDAP,
+                AnyTypeKind.USER.name(),
+                new ConnObjectTOQuery.Builder().size(100).fiql(new ConnObjectTOFiqlSearchConditionBuilder().
+                        is("givenName").equalToIgnoreCase("pullfromldap").query()).build());
+        assertTrue(matches.getResult().contains(user));
+
+        matches = resourceService.searchConnObjects(
+                RESOURCE_NAME_LDAP,
+                AnyTypeKind.USER.name(),
+                new ConnObjectTOQuery.Builder().size(100).fiql(new ConnObjectTOFiqlSearchConditionBuilder().
+                        is(Name.NAME).equalTo("uid=pullFromLDAP%252Cou=people%252Co=isp").query()).build());
+        assertTrue(matches.getResult().contains(user));
+
+        matches = resourceService.searchConnObjects(
+                RESOURCE_NAME_LDAP,
+                AnyTypeKind.USER.name(),
+                new ConnObjectTOQuery.Builder().size(100).fiql(new ConnObjectTOFiqlSearchConditionBuilder().
+                        is("givenName").notEqualTo("pullFromLDAP").query()).build());
+        assertFalse(matches.getResult().contains(user));
+    }
+
+    @Test
     public void issueSYNCOPE768() {
         int usersWithNullable = userService.search(new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).
                 fiql(SyncopeClient.getUserSearchConditionBuilder().is("ctype").nullValue().query()).build()).