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 13:49:56 UTC

[syncope] 01/02: [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 master
in repository https://gitbox.apache.org/repos/asf/syncope.git

commit e0c2fd972523fe2d34f5ce1e98db23c2f63989f5
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 +-
 .../apache/syncope/client/lib/SyncopeClient.java   |  10 +
 .../common/rest/api/service/ResourceService.java   |  15 +-
 .../search/AbstractFiqlSearchConditionBuilder.java |   1 -
 .../AnyObjectFiqlSearchConditionBuilder.java       |   1 -
 .../ConnObjectTOFiqlSearchConditionBuilder.java    |  80 ++++++
 .../search/GroupFiqlSearchConditionBuilder.java    |   1 -
 ...jectTOListQuery.java => ConnObjectTOQuery.java} |  33 ++-
 .../apache/syncope/core/logic/ResourceLogic.java   | 133 +++++-----
 .../core/rest/cxf/service/ResourceServiceImpl.java |  49 +++-
 .../persistence/api/search/FilterConverter.java    |  64 +++++
 .../core/persistence/api/search/FilterVisitor.java | 166 ++++++++++++
 .../persistence/api/search/SearchCondVisitor.java  |  26 +-
 .../api/search/FilterConverterTest.java            | 289 +++++++++++++++++++++
 .../syncope/fit/core/LinkedAccountITCase.java      |  34 +--
 .../apache/syncope/fit/core/ResourceITCase.java    |  58 -----
 .../org/apache/syncope/fit/core/SearchITCase.java  | 122 ++++++++-
 17 files changed, 907 insertions(+), 181 deletions(-)

diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/rest/ResourceRestClient.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/rest/ResourceRestClient.java
index 71dfd5c..ff5e201 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/rest/ResourceRestClient.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/rest/ResourceRestClient.java
@@ -26,7 +26,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;
 
@@ -62,7 +62,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));
@@ -72,7 +72,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/idrepo/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClient.java b/client/idrepo/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClient.java
index d5e6362..2979c9b 100644
--- a/client/idrepo/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClient.java
+++ b/client/idrepo/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClient.java
@@ -42,6 +42,7 @@ import org.apache.cxf.transport.http.HTTPConduit;
 import org.apache.cxf.transport.http.URLConnectionHTTPConduit;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.search.AnyObjectFiqlSearchConditionBuilder;
+import org.apache.syncope.common.lib.search.ConnObjectTOFiqlSearchConditionBuilder;
 import org.apache.syncope.common.lib.search.OrderByClauseBuilder;
 import org.apache.syncope.common.lib.search.GroupFiqlSearchConditionBuilder;
 import org.apache.syncope.common.lib.search.UserFiqlSearchConditionBuilder;
@@ -188,6 +189,15 @@ public class SyncopeClient {
     }
 
     /**
+     * Returns a new instance of {@link ConnObjectTOFiqlSearchConditionBuilder}, for assisted building of FIQL queries.
+     *
+     * @return default instance of {@link ConnObjectTOFiqlSearchConditionBuilder}
+     */
+    public static ConnObjectTOFiqlSearchConditionBuilder getConnObjectTOFiqlSearchConditionBuilder() {
+        return new ConnObjectTOFiqlSearchConditionBuilder();
+    }
+
+    /**
      * Returns a new instance of {@link OrderByClauseBuilder}, for assisted building of {@code orderby} clauses.
      *
      * @return default instance of {@link OrderByClauseBuilder}
diff --git a/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ResourceService.java b/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ResourceService.java
index 27b7813..cca2ac0 100644
--- a/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ResourceService.java
+++ b/common/idm/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/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/AbstractFiqlSearchConditionBuilder.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/AbstractFiqlSearchConditionBuilder.java
index 18fee59..5f8aa43 100644
--- a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/AbstractFiqlSearchConditionBuilder.java
+++ b/common/idrepo/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/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/AnyObjectFiqlSearchConditionBuilder.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/AnyObjectFiqlSearchConditionBuilder.java
index 9276015..b497c7b 100644
--- a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/AnyObjectFiqlSearchConditionBuilder.java
+++ b/common/idrepo/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/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/ConnObjectTOFiqlSearchConditionBuilder.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/ConnObjectTOFiqlSearchConditionBuilder.java
new file mode 100644
index 0000000..d028a71
--- /dev/null
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/ConnObjectTOFiqlSearchConditionBuilder.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.lib.search;
+
+import java.util.Map;
+import org.apache.cxf.jaxrs.ext.search.client.CompleteCondition;
+
+/**
+ * Extends {@link AbstractFiqlSearchConditionBuilder} by providing some additional facilities for searching
+ * connector objects.
+ */
+public class ConnObjectTOFiqlSearchConditionBuilder extends AbstractFiqlSearchConditionBuilder {
+
+    private static final long serialVersionUID = 4983742159694010935L;
+
+    @Override
+    protected Builder newBuilderInstance() {
+        return new Builder(properties);
+    }
+
+    @Override
+    public SyncopeProperty is(final String property) {
+        return newBuilderInstance().is(property);
+    }
+
+    protected class Builder extends AbstractFiqlSearchConditionBuilder.Builder
+            implements SyncopeProperty, CompleteCondition {
+
+        public Builder(final Map<String, String> properties) {
+            super(properties);
+        }
+
+        public Builder(final Builder parent) {
+            super(parent);
+        }
+
+        @Override
+        public SyncopeProperty is(final String property) {
+            Builder b = new Builder(this);
+            b.result = property;
+            return b;
+        }
+
+        @Override
+        public CompleteCondition inDynRealms(final String dynRealm, final String... moreDynRealms) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public CompleteCondition notInDynRealms(final String dynRealm, final String... moreDynRealms) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        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/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/GroupFiqlSearchConditionBuilder.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/GroupFiqlSearchConditionBuilder.java
index 232ba6b..1a875c9 100644
--- a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/GroupFiqlSearchConditionBuilder.java
+++ b/common/idrepo/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/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ConnObjectTOListQuery.java b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ConnObjectTOQuery.java
similarity index 79%
rename from common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ConnObjectTOListQuery.java
rename to common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ConnObjectTOQuery.java
index 9c8d172..db874ce 100644
--- a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ConnObjectTOListQuery.java
+++ b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ConnObjectTOQuery.java
@@ -19,15 +19,13 @@
 package org.apache.syncope.common.rest.api.beans;
 
 import java.io.Serializable;
-import java.util.Optional;
-
 import javax.validation.constraints.Max;
 import javax.validation.constraints.Min;
 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;
 
@@ -35,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);
@@ -52,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;
@@ -64,10 +66,14 @@ public class ConnObjectTOListQuery implements Serializable {
 
     private String orderBy;
 
+    private String fiql;
+
     public Integer getSize() {
-        return Optional.ofNullable(size).map(integer -> integer > MAX_SIZE
-            ? MAX_SIZE
-            : integer).orElse(25);
+        return size == null
+                ? 25
+                : size > MAX_SIZE
+                        ? MAX_SIZE
+                        : size;
     }
 
     @Min(1)
@@ -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/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 704cbe8..9dd0c93 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
@@ -27,37 +27,32 @@ 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.IdMEntitlement;
 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.VirSchema;
 import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.resource.MappingItem;
 import org.apache.syncope.core.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.VirSchema;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
 import org.apache.syncope.core.provisioning.api.MappingManager;
 import org.apache.syncope.core.provisioning.api.data.ConnInstanceDataBinder;
@@ -74,6 +69,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;
@@ -90,18 +86,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
@@ -116,6 +103,9 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
     @Autowired
     private ConnectorFactory connFactory;
 
+    @Autowired
+    private AnyUtilsFactory anyUtilsFactory;
+
     protected static void securityChecks(final Set<String> effectiveRealms, final String realm, final String key) {
         boolean authorized = effectiveRealms.stream().anyMatch(realm::startsWith);
         if (!authorized) {
@@ -277,69 +267,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.isEmpty()) {
-            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 + "'"));
 
-    @PreAuthorize("hasRole('" + IdMEntitlement.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);
+        return Pair.of(anyType, provision);
+    }
 
-        // 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);
-        }
+    private ConnObjectTO readConnObject(
+            final Provision provision,
+            final String connObjectKeyValue) {
 
-        // 2. build connObjectKeyItem
-        MappingItem connObjectKeyItem = MappingUtils.getConnObjectKeyItem(init.getRight()).
+        // 0. build connObjectKeyItem
+        MappingItem connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision).
                 orElseThrow(() -> new NotFoundException(
-                "ConnObjectKey mapping for " + init.getMiddle() + ' ' + anyKey + " on resource '" + key + '\''));
-        String connObjectKeyValue = mappingManager.getConnObjectKeyValue(any, init.getRight()).
-                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().
-                map(VirSchema::asLinkingMappingItem).collect(Collectors.toSet());
+        // 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());
@@ -351,10 +329,49 @@ public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
         return ConnObjectUtils.getConnObjectTO(connectorObject);
     }
 
+    @PreAuthorize("hasRole('" + IdMEntitlement.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('" + IdMEntitlement.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('" + IdMEntitlement.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;
@@ -372,8 +389,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();
 
@@ -387,7 +404,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/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java
index 2076fbb..ea255de 100644
--- a/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java
+++ b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java
@@ -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/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..b57c854
--- /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));
+            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..8a159d4
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/FilterVisitor.java
@@ -0,0 +1,166 @@
+/*
+ * 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 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 = SearchUtils.toSqlWildcardString(
+                URLDecoder.decode(sc.getStatement().getValue().toString(), StandardCharsets.UTF_8), false).
+                replaceAll("\\\\_", "_");
+        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 7a1a9d4..616a195 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
@@ -98,13 +98,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()));
             }
         }
 
@@ -252,9 +257,9 @@ 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;
         switch (sc.getConditionType()) {
@@ -285,5 +290,4 @@ public class SearchCondVisitor extends AbstractSearchConditionVisitor<SearchBean
     public SearchCond getQuery() {
         return searchCond;
     }
-
 }
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/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 f02f5cd..a04a07f 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.cxf.jaxrs.client.WebClient;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.policy.PullPolicyTO;
+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. remove linked account from user
         UserUR userUR = new UserUR();
@@ -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
         userUR = new UserUR();
@@ -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 74e8473..d1d8826 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
@@ -27,26 +27,19 @@ 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.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.request.AnyObjectCR;
-import org.apache.syncope.common.lib.request.GroupCR;
 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;
@@ -58,7 +51,6 @@ import org.apache.syncope.common.lib.types.EntityViolationType;
 import org.apache.syncope.common.lib.types.IdMImplementationType;
 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;
@@ -518,56 +510,6 @@ public class ResourceITCase extends AbstractITCase {
     }
 
     @Test
-    public void listConnObjects() {
-        List<String> groupKeys = new ArrayList<>();
-        for (int i = 0; i < 10; i++) {
-            GroupCR groupCR = GroupITCase.getSample("group");
-            groupCR.getResources().add(RESOURCE_NAME_LDAP);
-            GroupTO group = createGroup(groupCR).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);
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 35b298d..347e0b7 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;
@@ -39,18 +45,22 @@ import org.apache.syncope.common.lib.request.UserUR;
 import org.apache.syncope.common.lib.request.AttrPatch;
 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.junit.jupiter.api.Assertions;
+import org.identityconnectors.framework.common.objects.Name;
 import org.junit.jupiter.api.Test;
 
 public class SearchITCase extends AbstractITCase {
@@ -75,7 +85,8 @@ public class SearchITCase extends AbstractITCase {
         assertNotNull(matchingUsers);
         assertFalse(matchingUsers.getResult().isEmpty());
 
-        assertEquals(2, matchingUsers.getResult().stream().filter(user -> "74cd8ece-715a-44a4-a736-e17b46c4e7e6".equals(user.getKey())
+        assertEquals(2, matchingUsers.getResult().stream().filter(user -> "74cd8ece-715a-44a4-a736-e17b46c4e7e6".equals(
+                user.getKey())
                 || "b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee".equals(user.getKey())).count());
     }
 
@@ -445,6 +456,113 @@ public class SearchITCase extends AbstractITCase {
     }
 
     @Test
+    public void searchConnObjectsBrowsePagedResult() {
+        List<String> groupKeys = new ArrayList<>();
+        for (int i = 0; i < 10; i++) {
+            GroupCR groupCR = GroupITCase.getSample("group");
+            groupCR.getResources().add(RESOURCE_NAME_LDAP);
+            GroupTO group = createGroup(groupCR).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(
+                        SyncopeClient.getConnObjectTOFiqlSearchConditionBuilder().
+                                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(
+                        SyncopeClient.getConnObjectTOFiqlSearchConditionBuilder().
+                                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(
+                        SyncopeClient.getConnObjectTOFiqlSearchConditionBuilder().
+                                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(
+                        SyncopeClient.getConnObjectTOFiqlSearchConditionBuilder().
+                                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(
+                        SyncopeClient.getConnObjectTOFiqlSearchConditionBuilder().
+                                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(
+                        SyncopeClient.getConnObjectTOFiqlSearchConditionBuilder().
+                                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()).
@@ -534,7 +652,7 @@ public class SearchITCase extends AbstractITCase {
         req.getPlainAttrs().add(new AttrPatch.Builder(attr("ctype", "ou=sample,o=isp")).build());
         userService.update(req);
 
-	if (ElasticsearchDetector.isElasticSearchEnabled(syncopeService)) {
+        if (ElasticsearchDetector.isElasticSearchEnabled(syncopeService)) {
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException ex) {