github-advanced-security[bot] commented on code in PR #1300:
URL: https://github.com/apache/syncope/pull/1300#discussion_r2841160847


##########
ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAO.java:
##########
@@ -105,7 +132,7 @@
                     orElse(null);
             return realmDAO.findById(result).map(Realm.class::cast);
         } catch (Exception e) {
-            LOG.error("While searching OpenSearch for Realm path {} with 
request {}", fullPath, request, e);
+            LOG.error("While searching Elasticsearch for Realm path {} with 
request {}", fullPath, request, e);

Review Comment:
   ## Log Injection
   
   This log entry depends on a [user-provided value](1).
   This log entry depends on a [user-provided value](2).
   This log entry depends on a [user-provided value](3).
   This log entry depends on a [user-provided value](4).
   This log entry depends on a [user-provided value](5).
   This log entry depends on a [user-provided value](6).
   This log entry depends on a [user-provided value](7).
   This log entry depends on a [user-provided value](8).
   This log entry depends on a [user-provided value](9).
   This log entry depends on a [user-provided value](10).
   This log entry depends on a [user-provided value](11).
   This log entry depends on a [user-provided value](12).
   This log entry depends on a [user-provided value](13).
   This log entry depends on a [user-provided value](14).
   This log entry depends on a [user-provided value](15).
   This log entry depends on a [user-provided value](16).
   This log entry depends on a [user-provided value](17).
   This log entry depends on a [user-provided value](18).
   This log entry depends on a [user-provided value](19).
   This log entry depends on a [user-provided value](20).
   This log entry depends on a [user-provided value](21).
   This log entry depends on a [user-provided value](22).
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2563)



##########
core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractSearchDAO.java:
##########
@@ -0,0 +1,288 @@
+/*
+ * 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.common.dao;
+
+import jakarta.validation.ValidationException;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+import org.apache.commons.jexl3.parser.Parser;
+import org.apache.commons.jexl3.parser.ParserConstants;
+import org.apache.commons.jexl3.parser.Token;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.types.AttrSchemaType;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import 
org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AbstractSearchDAO {
+
+    public record CheckResult<C extends AttrCond>(PlainSchema schema, 
PlainAttrValue value, C cond) {
+
+    }
+
+    protected static final Logger LOG = 
LoggerFactory.getLogger(AbstractSearchDAO.class);
+
+    protected static final String ALWAYS_FALSE_CLAUSE = "1=2";
+
+    public static String key(final AttrSchemaType schemaType) {
+        return switch (schemaType) {
+            case Boolean ->
+                "booleanValue";
+
+            case Date ->
+                "dateValue";
+
+            case Double ->
+                "doubleValue";
+
+            case Long ->
+                "longValue";
+
+            case Binary ->
+                "binaryValue";
+
+            default ->
+                "stringValue";
+        };
+    }
+
+    protected static final Comparator<String> LITERAL_COMPARATOR = (l1, l2) -> 
{
+        if (l1 == null && l2 == null) {
+            return 0;
+        } else if (l1 != null && l2 == null) {
+            return -1;
+        } else if (l1 == null) {
+            return 1;
+        } else if (l1.length() == l2.length()) {
+            return 0;
+        } else if (l1.length() > l2.length()) {
+            return -1;
+        } else {
+            return 1;
+        }
+    };
+
+    /**
+     * Split an attribute value recurring on provided literals/tokens.
+     *
+     * @param attrValue value to be split
+     * @param literals literals/tokens
+     * @return split value
+     */
+    protected static List<String> split(final String attrValue, final 
List<String> literals) {
+        final List<String> attrValues = new ArrayList<>();
+
+        if (literals.isEmpty()) {
+            attrValues.add(attrValue);
+        } else {
+            for (String token : 
attrValue.split(Pattern.quote(literals.getFirst()))) {
+                if (!token.isEmpty()) {
+                    attrValues.addAll(split(token, literals.subList(1, 
literals.size())));
+                }
+            }
+        }
+
+        return attrValues;
+    }
+
+    protected static Supplier<SyncopeClientException> 
syncopeClientException(final String message) {
+        return () -> {
+            SyncopeClientException sce = 
SyncopeClientException.build(ClientExceptionType.InvalidSearchParameters);
+            sce.getElements().add(message);
+            return sce;
+        };
+    }
+
+    protected final PlainSchemaDAO plainSchemaDAO;
+
+    protected final EntityFactory entityFactory;
+
+    protected final PlainAttrValidationManager validator;
+
+    protected AbstractSearchDAO(
+            final PlainSchemaDAO plainSchemaDAO,
+            final EntityFactory entityFactory,
+            final PlainAttrValidationManager validator) {
+
+        this.plainSchemaDAO = plainSchemaDAO;
+        this.entityFactory = entityFactory;
+        this.validator = validator;
+    }
+
+    protected List<SearchCond> buildDerAttrValueConditions(
+            final String expression,
+            final String value,
+            final boolean ignoreCaseMatch) {
+
+        Parser parser = new Parser(expression);
+
+        // Schema keys
+        List<String> identifiers = new ArrayList<>();
+
+        // Literals
+        List<String> literals = new ArrayList<>();
+
+        // Get schema keys and literals
+        for (Token token = parser.getNextToken(); token != null && 
StringUtils.isNotBlank(token.toString());
+                token = parser.getNextToken()) {
+
+            if (token.kind == ParserConstants.STRING_LITERAL) {
+                literals.add(token.toString().substring(1, 
token.toString().length() - 1));
+            }
+
+            if (token.kind == ParserConstants.IDENTIFIER) {
+                identifiers.add(token.toString());
+            }
+        }
+
+        // Sort literals in order to process later literals included into 
others
+        literals.sort(LITERAL_COMPARATOR);
+
+        // Split value on provided literals
+        List<String> attrValues = split(value, literals);
+
+        if (attrValues.size() != identifiers.size()) {
+            LOG.error("Ambiguous JEXL expression resolution: literals and 
values have different size");
+            return List.of();
+        }
+
+        List<SearchCond> conditions = new ArrayList<>();
+
+        // Contains used identifiers in order to avoid replications
+        Set<String> used = new HashSet<>();
+
+        // Create several clauses: one for eanch identifiers
+        for (int i = 0; i < identifiers.size() && 
!used.contains(identifiers.get(i)); i++) {
+            used.add(identifiers.get(i));
+
+            AttrCond cond = plainSchemaDAO.findById(identifiers.get(i)).
+                    map(schema -> new AttrCond()).

Review Comment:
   ## Useless parameter
   
   The parameter 'schema' is never used.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2565)



##########
core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractSearchDAO.java:
##########
@@ -0,0 +1,288 @@
+/*
+ * 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.common.dao;
+
+import jakarta.validation.ValidationException;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+import org.apache.commons.jexl3.parser.Parser;
+import org.apache.commons.jexl3.parser.ParserConstants;
+import org.apache.commons.jexl3.parser.Token;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.types.AttrSchemaType;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import 
org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AbstractSearchDAO {
+
+    public record CheckResult<C extends AttrCond>(PlainSchema schema, 
PlainAttrValue value, C cond) {
+
+    }
+
+    protected static final Logger LOG = 
LoggerFactory.getLogger(AbstractSearchDAO.class);
+
+    protected static final String ALWAYS_FALSE_CLAUSE = "1=2";
+
+    public static String key(final AttrSchemaType schemaType) {
+        return switch (schemaType) {
+            case Boolean ->
+                "booleanValue";
+
+            case Date ->
+                "dateValue";
+
+            case Double ->
+                "doubleValue";
+
+            case Long ->
+                "longValue";
+
+            case Binary ->
+                "binaryValue";
+
+            default ->
+                "stringValue";
+        };
+    }
+
+    protected static final Comparator<String> LITERAL_COMPARATOR = (l1, l2) -> 
{
+        if (l1 == null && l2 == null) {
+            return 0;
+        } else if (l1 != null && l2 == null) {
+            return -1;
+        } else if (l1 == null) {
+            return 1;
+        } else if (l1.length() == l2.length()) {
+            return 0;
+        } else if (l1.length() > l2.length()) {
+            return -1;
+        } else {
+            return 1;
+        }
+    };
+
+    /**
+     * Split an attribute value recurring on provided literals/tokens.
+     *
+     * @param attrValue value to be split
+     * @param literals literals/tokens
+     * @return split value
+     */
+    protected static List<String> split(final String attrValue, final 
List<String> literals) {
+        final List<String> attrValues = new ArrayList<>();
+
+        if (literals.isEmpty()) {
+            attrValues.add(attrValue);
+        } else {
+            for (String token : 
attrValue.split(Pattern.quote(literals.getFirst()))) {
+                if (!token.isEmpty()) {
+                    attrValues.addAll(split(token, literals.subList(1, 
literals.size())));
+                }
+            }
+        }
+
+        return attrValues;
+    }
+
+    protected static Supplier<SyncopeClientException> 
syncopeClientException(final String message) {
+        return () -> {
+            SyncopeClientException sce = 
SyncopeClientException.build(ClientExceptionType.InvalidSearchParameters);
+            sce.getElements().add(message);
+            return sce;
+        };
+    }
+
+    protected final PlainSchemaDAO plainSchemaDAO;
+
+    protected final EntityFactory entityFactory;
+
+    protected final PlainAttrValidationManager validator;
+
+    protected AbstractSearchDAO(
+            final PlainSchemaDAO plainSchemaDAO,
+            final EntityFactory entityFactory,
+            final PlainAttrValidationManager validator) {
+
+        this.plainSchemaDAO = plainSchemaDAO;
+        this.entityFactory = entityFactory;
+        this.validator = validator;
+    }
+
+    protected List<SearchCond> buildDerAttrValueConditions(
+            final String expression,
+            final String value,
+            final boolean ignoreCaseMatch) {
+
+        Parser parser = new Parser(expression);
+
+        // Schema keys
+        List<String> identifiers = new ArrayList<>();
+
+        // Literals
+        List<String> literals = new ArrayList<>();
+
+        // Get schema keys and literals
+        for (Token token = parser.getNextToken(); token != null && 
StringUtils.isNotBlank(token.toString());
+                token = parser.getNextToken()) {
+
+            if (token.kind == ParserConstants.STRING_LITERAL) {
+                literals.add(token.toString().substring(1, 
token.toString().length() - 1));
+            }
+
+            if (token.kind == ParserConstants.IDENTIFIER) {
+                identifiers.add(token.toString());
+            }
+        }
+
+        // Sort literals in order to process later literals included into 
others
+        literals.sort(LITERAL_COMPARATOR);
+
+        // Split value on provided literals
+        List<String> attrValues = split(value, literals);
+
+        if (attrValues.size() != identifiers.size()) {
+            LOG.error("Ambiguous JEXL expression resolution: literals and 
values have different size");
+            return List.of();
+        }
+
+        List<SearchCond> conditions = new ArrayList<>();
+
+        // Contains used identifiers in order to avoid replications
+        Set<String> used = new HashSet<>();
+
+        // Create several clauses: one for eanch identifiers
+        for (int i = 0; i < identifiers.size() && 
!used.contains(identifiers.get(i)); i++) {
+            used.add(identifiers.get(i));
+
+            AttrCond cond = plainSchemaDAO.findById(identifiers.get(i)).
+                    map(schema -> new AttrCond()).
+                    orElseGet(() -> new AnyCond());
+            cond.setType(ignoreCaseMatch ? AttrCond.Type.IEQ : 
AttrCond.Type.EQ);
+            cond.setSchema(identifiers.get(i));
+            cond.setExpression(attrValues.get(i));
+            conditions.add(SearchCond.of(cond));
+        }
+
+        return conditions;
+    }
+
+    protected CheckResult<AttrCond> check(final AttrCond cond) {
+        PlainSchema schema = plainSchemaDAO.findById(cond.getSchema()).
+                orElseThrow(() -> new IllegalArgumentException("Invalid schema 
" + cond.getSchema()));
+
+        PlainAttrValue attrValue = new PlainAttrValue();
+
+        if (AttrSchemaType.Encrypted == schema.getType()) {
+            throw new IllegalArgumentException("Cannot search by encrypted 
schema " + cond.getSchema());
+        }
+
+        try {
+            if (cond.getType() != AttrCond.Type.LIKE
+                    && cond.getType() != AttrCond.Type.ILIKE
+                    && cond.getType() != AttrCond.Type.ISNULL
+                    && cond.getType() != AttrCond.Type.ISNOTNULL) {
+
+                validator.validate(schema, cond.getExpression(), attrValue);
+            }
+        } catch (ValidationException e) {
+            throw new IllegalArgumentException("Could not validate expression 
" + cond.getExpression());
+        }
+
+        return new CheckResult<>(schema, attrValue, cond);
+    }
+
+    protected CheckResult<AnyCond> check(final AnyCond cond, final Field 
field, final Set<String> relationshipsFields) {
+        AnyCond computed = new AnyCond(cond.getType());
+        computed.setSchema(cond.getSchema());
+        computed.setExpression(cond.getExpression());
+
+        // Keeps track of difference between entity's getKey() and @Id fields
+        if ("key".equals(computed.getSchema())) {
+            computed.setSchema("id");
+        }
+
+        PlainSchema schema = entityFactory.newEntity(PlainSchema.class);
+        schema.setKey(field.getName());
+        for (AttrSchemaType attrSchemaType : AttrSchemaType.values()) {
+            if (field.getType().isAssignableFrom(attrSchemaType.getType())) {
+                schema.setType(attrSchemaType);
+            }
+        }
+        if (schema.getType() == null || schema.getType() == 
AttrSchemaType.Dropdown) {
+            schema.setType(AttrSchemaType.String);
+        }
+
+        // Deal with any Integer fields logically mapping to boolean values
+        boolean foundBooleanMin = false;
+        boolean foundBooleanMax = false;
+        if (Integer.class.equals(field.getType())) {
+            for (Annotation annotation : field.getAnnotations()) {
+                if (Min.class.equals(annotation.annotationType())) {
+                    foundBooleanMin = ((Min) annotation).value() == 0;
+                } else if (Max.class.equals(annotation.annotationType())) {
+                    foundBooleanMax = ((Max) annotation).value() == 1;
+                }
+            }
+        }
+        if (foundBooleanMin && foundBooleanMax) {
+            schema.setType(AttrSchemaType.Boolean);
+        }
+
+        // Deal with fields representing relationships to other entities
+        if (relationshipsFields.contains(computed.getSchema())) {
+            computed.setSchema(computed.getSchema() + "_id");
+            schema.setType(AttrSchemaType.String);
+        }
+
+        PlainAttrValue attrValue = new PlainAttrValue();
+        if (computed.getType() != AttrCond.Type.LIKE
+                && computed.getType() != AttrCond.Type.ILIKE
+                && computed.getType() != AttrCond.Type.ISNULL
+                && computed.getType() != AttrCond.Type.ISNOTNULL) {
+
+            try {
+                validator.validate(schema, computed.getExpression(), 
attrValue);
+            } catch (ValidationException e) {
+                LOG.error("Could not validate expression {}", 
computed.getExpression(), e);

Review Comment:
   ## Log Injection
   
   This log entry depends on a [user-provided value](1).
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2564)



##########
ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java:
##########
@@ -240,8 +245,6 @@
         return builder;
     }
 
-    protected void customizeDocument(
-            final Map<String, Object> builder,
-            final AuditEvent auditEvent) {
+    protected void customizeDocument(final Map<String, Object> builder, final 
AuditEvent auditEvent) {

Review Comment:
   ## Useless parameter
   
   The parameter 'builder' is never used.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2571)



##########
ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAO.java:
##########
@@ -159,28 +189,199 @@
         });
         Query prefix = new 
Query.Builder().disMax(QueryBuilders.disMax().queries(basesQueries).build()).build();
 
-        if (keyword == null) {
-            return prefix;
+        BoolQuery.Builder boolBuilder = QueryBuilders.bool().filter(prefix);
+        if (searchCond != null) {
+            boolBuilder.filter(getQuery(searchCond));
         }
+        return new Query.Builder().bool(boolBuilder.build()).build();
+    }
 
-        return new Query.Builder().bool(QueryBuilders.bool().filter(
-                prefix,
-                new Query.Builder().wildcard(QueryBuilders.wildcard().
-                        field("name").value(keyword.replace('%', 
'*').replace("\\_", "_")).
-                        caseInsensitive(true).build()).build()).build()).
-                build();
+    protected Query getQuery(final SearchCond cond) {
+        Query query = null;
+
+        switch (cond.getType()) {
+            case LEAF, NOT_LEAF -> {
+                query = 
cond.asLeaf(AnyCond.class).map(this::getQuery).orElse(null);
+                if (query == null) {
+                    query = 
cond.asLeaf(AttrCond.class).map(this::getQuery).orElse(null);
+                }
+                if (query == null) {
+                    query = getQueryForCustomConds(cond);
+                }
+                if (query == null) {
+                    throw new IllegalArgumentException("Cannot construct 
QueryBuilder");
+                }
+                if (cond.getType() == SearchCond.Type.NOT_LEAF) {
+                    query = new 
Query.Builder().bool(QueryBuilders.bool().mustNot(query).build()).build();
+                }
+            }
+            case AND -> {
+                List<Query> andCompound = new ArrayList<>();
+                Query andLeft = getQuery(cond.getLeft());
+                if (andLeft.isBool() && !andLeft.bool().filter().isEmpty()) {
+                    andCompound.addAll(andLeft.bool().filter());
+                } else {
+                    andCompound.add(andLeft);
+                }
+                Query andRight = getQuery(cond.getRight());
+                if (andRight.isBool() && !andRight.bool().filter().isEmpty()) {
+                    andCompound.addAll(andRight.bool().filter());
+                } else {
+                    andCompound.add(andRight);
+                }
+                query = new 
Query.Builder().bool(QueryBuilders.bool().filter(andCompound).build()).build();
+            }
+            case OR -> {
+                List<Query> orCompound = new ArrayList<>();
+                Query orLeft = getQuery(cond.getLeft());
+                if (orLeft.isDisMax()) {
+                    orCompound.addAll(orLeft.disMax().queries());
+                } else {
+                    orCompound.add(orLeft);
+                }
+                Query orRight = getQuery(cond.getRight());
+                if (orRight.isDisMax()) {
+                    orCompound.addAll(orRight.disMax().queries());
+                } else {
+                    orCompound.add(orRight);
+                }
+                query = new 
Query.Builder().disMax(QueryBuilders.disMax().queries(orCompound).build()).build();
+            }
+            default -> {
+            }
+        }
+
+        return query;
     }
 
     @Override
-    public long countDescendants(final String base, final String keyword) {
-        return countDescendants(Set.of(base), keyword);
+    protected CheckResult<AnyCond> check(final AnyCond cond, final Field 
field, final Set<String> relationshipsFields) {
+        CheckResult<AnyCond> checked = super.check(cond, field, 
relationshipsFields);
+
+        // Manage difference between external id attribute and internal _id
+        if ("id".equals(checked.cond().getSchema())) {
+            checked.cond().setSchema("_id");
+        }
+        if ("id".equals(checked.schema().getKey())) {
+            checked.schema().setKey("_id");
+        }
+
+        return checked;
+    }
+
+    protected Query fillAttrQuery(
+            final PlainSchema schema,
+            final PlainAttrValue attrValue,
+            final AttrCond cond) {
+
+        Object value = schema.getType() == AttrSchemaType.Date && 
attrValue.getDateValue() != null
+                ? FormatUtils.format(attrValue.getDateValue())
+                : attrValue.getValue();
+
+        Query query = null;
+        switch (cond.getType()) {
+            case ISNOTNULL:
+                query = new 
Query.Builder().exists(QueryBuilders.exists().field(schema.getKey()).build()).build();
+                break;
+
+            case ISNULL:
+                query = new Query.Builder().bool(QueryBuilders.bool().mustNot(
+                        new 
Query.Builder().exists(QueryBuilders.exists().field(schema.getKey()).build())
+                                .build()).build()).build();
+                break;
+
+            case ILIKE:
+                query = new Query.Builder().wildcard(QueryBuilders.wildcard().
+                        
field(schema.getKey()).value(cond.getExpression().replace('%', 
'*').replace("\\_", "_")).
+                        caseInsensitive(true).build()).build();
+                break;
+
+            case LIKE:
+                query = new Query.Builder().wildcard(QueryBuilders.wildcard().
+                        
field(schema.getKey()).value(cond.getExpression().replace('%', 
'*').replace("\\_", "_")).
+                        caseInsensitive(false).build()).build();
+                break;
+
+            case IEQ:
+                query = new Query.Builder().term(QueryBuilders.term().
+                        
field(schema.getKey()).value(FieldValue.of(cond.getExpression())).caseInsensitive(true).
+                        build()).build();
+                break;
+
+            case EQ:
+                FieldValue fieldValue = switch (value) {
+                    case Double aDouble ->
+                        FieldValue.of(aDouble);
+                    case Long aLong ->
+                        FieldValue.of(aLong);
+                    case Boolean aBoolean ->
+                        FieldValue.of(aBoolean);
+                    default ->
+                        FieldValue.of(value.toString());
+                };
+                query = new Query.Builder().term(QueryBuilders.term().
+                        
field(schema.getKey()).value(fieldValue).caseInsensitive(false).build()).
+                        build();
+                break;
+
+            case GE:
+                query = new Query.Builder().range(RangeQuery.of(r -> 
r.untyped(n -> n.
+                        field(schema.getKey()).
+                        gte(JsonData.of(value))))).
+                        build();
+                break;
+
+            case GT:
+                query = new Query.Builder().range(RangeQuery.of(r -> 
r.untyped(n -> n.
+                        field(schema.getKey()).
+                        gt(JsonData.of(value))))).
+                        build();
+                break;
+
+            case LE:
+                query = new Query.Builder().range(RangeQuery.of(r -> 
r.untyped(n -> n.
+                        field(schema.getKey()).
+                        lte(JsonData.of(value))))).
+                        build();
+                break;
+
+            case LT:
+                query = new Query.Builder().range(RangeQuery.of(r -> 
r.untyped(n -> n.
+                        field(schema.getKey()).
+                        lt(JsonData.of(value))))).
+                        build();
+                break;
+
+            default:
+                break;
+        }
+
+        return query;
+    }
+
+    protected Query getQuery(final AttrCond cond) {
+        CheckResult<AttrCond> checked = check(cond);
+        return fillAttrQuery(checked.schema(), checked.value(), cond);
+    }
+
+    protected Query getQuery(final AnyCond cond) {

Review Comment:
   ## Confusing overloading of methods
   
   Method ElasticsearchRealmSearchDAO.getQuery(..) could be confused with 
overloaded method [getQuery](1), since dispatch depends on static types.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2575)



##########
ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java:
##########
@@ -240,8 +245,6 @@
         return builder;
     }
 
-    protected void customizeDocument(
-            final Map<String, Object> builder,
-            final AuditEvent auditEvent) {
+    protected void customizeDocument(final Map<String, Object> builder, final 
AuditEvent auditEvent) {

Review Comment:
   ## Useless parameter
   
   The parameter 'builder' is never used.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2573)



##########
ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAO.java:
##########
@@ -161,53 +188,252 @@
         });
         Query prefix = new 
Query.Builder().disMax(QueryBuilders.disMax().queries(basesQueries).build()).build();
 
-        if (keyword == null) {
-            return prefix;
+        BoolQuery.Builder boolBuilder = QueryBuilders.bool().filter(prefix);
+        if (searchCond != null) {
+            boolBuilder.filter(getQuery(searchCond));
         }
+        return new Query.Builder().bool(boolBuilder.build()).build();
+    }
 
-        return new Query.Builder().bool(QueryBuilders.bool().filter(
-                prefix,
-                new Query.Builder().wildcard(QueryBuilders.wildcard().
-                        field("name").value(keyword.replace('%', 
'*').replace("\\_", "_")).
-                        caseInsensitive(true).build()).build()).build()).
-                build();
+    protected Query getQuery(final SearchCond cond) {
+        Query query = null;
+
+        switch (cond.getType()) {
+            case LEAF, NOT_LEAF -> {
+                query = 
cond.asLeaf(AnyCond.class).map(this::getQuery).orElse(null);
+                if (query == null) {
+                    query = 
cond.asLeaf(AttrCond.class).map(this::getQuery).orElse(null);
+                }
+                if (query == null) {
+                    query = getQueryForCustomConds(cond);
+                }
+                if (query == null) {
+                    throw new IllegalArgumentException("Cannot construct 
QueryBuilder");
+                }
+                if (cond.getType() == SearchCond.Type.NOT_LEAF) {
+                    query = new 
Query.Builder().bool(QueryBuilders.bool().mustNot(query).build()).build();
+                }
+            }
+            case AND -> {
+                List<Query> andCompound = new ArrayList<>();
+                Query andLeft = getQuery(cond.getLeft());
+                if (andLeft.isBool() && !andLeft.bool().filter().isEmpty()) {
+                    andCompound.addAll(andLeft.bool().filter());
+                } else {
+                    andCompound.add(andLeft);
+                }
+                Query andRight = getQuery(cond.getRight());
+                if (andRight.isBool() && !andRight.bool().filter().isEmpty()) {
+                    andCompound.addAll(andRight.bool().filter());
+                } else {
+                    andCompound.add(andRight);
+                }
+                query = new 
Query.Builder().bool(QueryBuilders.bool().filter(andCompound).build()).build();
+            }
+            case OR -> {
+                List<Query> orCompound = new ArrayList<>();
+                Query orLeft = getQuery(cond.getLeft());
+                if (orLeft.isDisMax()) {
+                    orCompound.addAll(orLeft.disMax().queries());
+                } else {
+                    orCompound.add(orLeft);
+                }
+                Query orRight = getQuery(cond.getRight());
+                if (orRight.isDisMax()) {
+                    orCompound.addAll(orRight.disMax().queries());
+                } else {
+                    orCompound.add(orRight);
+                }
+                query = new 
Query.Builder().disMax(QueryBuilders.disMax().queries(orCompound).build()).build();
+            }
+            default -> {
+            }
+        }
+
+        return query;
     }
 
     @Override
-    public long countDescendants(final String base, final String keyword) {
-        return countDescendants(Set.of(base), keyword);
+    protected CheckResult<AnyCond> check(final AnyCond cond, final Field 
field, final Set<String> relationshipsFields) {
+        CheckResult<AnyCond> checked = super.check(cond, field, 
relationshipsFields);
+
+        // Manage difference between external id attribute and internal _id
+        if ("id".equals(checked.cond().getSchema())) {
+            checked.cond().setSchema("_id");
+        }
+        if ("id".equals(checked.schema().getKey())) {
+            checked.schema().setKey("_id");
+        }
+
+        return checked;
+    }
+
+    protected Query fillAttrQuery(
+            final PlainSchema schema,
+            final PlainAttrValue attrValue,
+            final AttrCond cond) {
+
+        Object value = schema.getType() == AttrSchemaType.Date && 
attrValue.getDateValue() != null
+                ? FormatUtils.format(attrValue.getDateValue())
+                : attrValue.getValue();
+
+        Query query = null;
+        switch (cond.getType()) {
+            case ISNOTNULL:
+                query = new 
Query.Builder().exists(QueryBuilders.exists().field(schema.getKey()).build()).build();
+                break;
+
+            case ISNULL:
+                query = new Query.Builder().bool(QueryBuilders.bool().mustNot(
+                        new 
Query.Builder().exists(QueryBuilders.exists().field(schema.getKey()).build())
+                                .build()).build()).build();
+                break;
+
+            case ILIKE:
+                query = new Query.Builder().wildcard(QueryBuilders.wildcard().
+                        
field(schema.getKey()).value(cond.getExpression().replace('%', 
'*').replace("\\_", "_")).
+                        caseInsensitive(true).build()).build();
+                break;
+
+            case LIKE:
+                query = new Query.Builder().wildcard(QueryBuilders.wildcard().
+                        
field(schema.getKey()).value(cond.getExpression().replace('%', 
'*').replace("\\_", "_")).
+                        caseInsensitive(false).build()).build();
+                break;
+
+            case IEQ:
+                query = new Query.Builder().term(QueryBuilders.term().
+                        
field(schema.getKey()).value(FieldValue.of(cond.getExpression())).caseInsensitive(true).
+                        build()).build();
+                break;
+
+            case EQ:
+                FieldValue fieldValue = switch (value) {
+                    case Double aDouble ->
+                        FieldValue.of(aDouble);
+                    case Long aLong ->
+                        FieldValue.of(aLong);
+                    case Boolean aBoolean ->
+                        FieldValue.of(aBoolean);
+                    default ->
+                        FieldValue.of(value.toString());
+                };
+                query = new Query.Builder().term(QueryBuilders.term().
+                        
field(schema.getKey()).value(fieldValue).caseInsensitive(false).build()).
+                        build();
+                break;
+
+            case GE:
+                query = new Query.Builder().range(QueryBuilders.range().
+                        
field(schema.getKey()).gte(JsonData.of(value)).build()).
+                        build();
+                break;
+
+            case GT:
+                query = new Query.Builder().range(QueryBuilders.range().
+                        field(schema.getKey()).gt(JsonData.of(value)).build()).
+                        build();
+                break;
+
+            case LE:
+                query = new Query.Builder().range(QueryBuilders.range().
+                        
field(schema.getKey()).lte(JsonData.of(value)).build()).
+                        build();
+                break;
+
+            case LT:
+                query = new Query.Builder().range(QueryBuilders.range().
+                        field(schema.getKey()).lt(JsonData.of(value)).build()).
+                        build();
+                break;
+
+            default:
+                break;
+        }
+
+        return query;
+    }
+
+    protected Query getQuery(final AttrCond cond) {
+        CheckResult<AttrCond> checked = check(cond);
+        return fillAttrQuery(checked.schema(), checked.value(), cond);
+    }
+
+    protected Query getQuery(final AnyCond cond) {

Review Comment:
   ## Confusing overloading of methods
   
   Method OpenSearchRealmSearchDAO.getQuery(..) could be confused with 
overloaded method [getQuery](1), since dispatch depends on static types.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2576)



##########
ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java:
##########
@@ -240,8 +245,6 @@
         return builder;
     }
 
-    protected void customizeDocument(
-            final Map<String, Object> builder,
-            final AuditEvent auditEvent) {
+    protected void customizeDocument(final Map<String, Object> builder, final 
AuditEvent auditEvent) {

Review Comment:
   ## Useless parameter
   
   The parameter 'auditEvent' is never used.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2572)



##########
core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPARealmSearchDAO.java:
##########
@@ -0,0 +1,537 @@
+/*
+ * 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.jpa.dao;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.types.AttrSchemaType;
+import 
org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
+import org.apache.syncope.core.persistence.api.dao.MalformedPathException;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.apache.syncope.core.persistence.api.entity.Realm;
+import org.apache.syncope.core.persistence.api.utils.RealmUtils;
+import org.apache.syncope.core.persistence.common.dao.AbstractRealmSearchDAO;
+import org.apache.syncope.core.persistence.jpa.entity.JPARealm;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.transaction.annotation.Transactional;
+
+public abstract class AbstractJPARealmSearchDAO extends AbstractRealmSearchDAO 
{
+
+    protected record QueryInfo(RealmSearchNode node, Set<String> plainSchemas) 
{
+
+    }
+
+    protected record AttrCondQuery(Boolean addPlainSchemas, RealmSearchNode 
node) {
+
+    }
+
+    protected static int setParameter(final List<Object> parameters, final 
Object parameter) {
+        parameters.add(parameter);
+        return parameters.size();
+    }
+
+    protected static void fillWithParameters(final Query query, final 
List<Object> parameters) {
+        for (int i = 0; i < parameters.size(); i++) {
+            if (parameters.get(i) instanceof Boolean aBoolean) {
+                query.setParameter(i + 1, aBoolean ? 1 : 0);
+            } else {
+                query.setParameter(i + 1, parameters.get(i));
+            }
+        }
+    }
+
+    protected final EntityManager entityManager;
+
+    protected final RealmUtils realmUtils;
+
+    protected AbstractJPARealmSearchDAO(
+            final EntityManager entityManager,
+            final PlainSchemaDAO plainSchemaDAO,
+            final EntityFactory entityFactory,
+            final PlainAttrValidationManager validator,
+            final RealmUtils realmUtils) {
+
+        super(plainSchemaDAO, entityFactory, validator);
+
+        this.entityManager = entityManager;
+        this.realmUtils = realmUtils;
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public Optional<Realm> findByFullPath(final String fullPath) {
+        if (StringUtils.isBlank(fullPath)
+                || (!SyncopeConstants.ROOT_REALM.equals(fullPath)
+                && !RealmDAO.PATH_PATTERN.matcher(fullPath).matches())) {
+
+            throw new MalformedPathException(fullPath);
+        }
+
+        TypedQuery<Realm> query = entityManager.createQuery(
+                "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE 
e.fullPath=:fullPath", Realm.class);
+        query.setParameter("fullPath", fullPath);
+
+        Realm result = null;
+        try {
+            result = query.getSingleResult();
+        } catch (NoResultException e) {
+            LOG.debug("Realm with fullPath {} not found", fullPath, e);
+        }
+
+        return Optional.ofNullable(result);
+    }
+
+    @Override
+    public List<Realm> findByName(final String name) {
+        TypedQuery<Realm> query = entityManager.createQuery(
+                "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE 
e.name=:name", Realm.class);
+        query.setParameter("name", name);
+
+        return query.getResultList();
+    }
+
+    @Override
+    public List<Realm> findChildren(final Realm realm) {
+        TypedQuery<Realm> query = entityManager.createQuery(
+                "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE 
e.parent=:realm", Realm.class);
+        query.setParameter("realm", realm);
+
+        return query.getResultList();
+    }
+
+    @Override
+    public List<Realm> findDescendants(final String base, final String prefix) 
{
+        List<Object> parameters = new ArrayList<>();
+
+        StringBuilder queryString = new StringBuilder("SELECT e FROM ").
+                append(JPARealm.class.getSimpleName()).
+                append(" e WHERE 
(e.fullPath=?").append(setParameter(parameters, base)).
+                append(" OR e.fullPath LIKE ?").append(setParameter(
+                parameters,
+                SyncopeConstants.ROOT_REALM.equals(base) ? "/%" : base + 
"/%")).
+                append(") ");
+        if (prefix != null) {
+            queryString.append("AND 
(e.fullPath=?").append(setParameter(parameters, prefix)).
+                    append(" OR e.fullPath LIKE ?").append(setParameter(
+                    parameters,
+                    SyncopeConstants.ROOT_REALM.equals(prefix) ? "/%" : prefix 
+ "/%")).
+                    append(") ");
+        }
+        queryString.append("ORDER BY e.fullPath");
+
+        TypedQuery<Realm> query = 
entityManager.createQuery(queryString.toString(), Realm.class);
+
+        fillWithParameters(query, parameters);
+
+        return query.getResultList();
+    }
+
+    // ------------------------------------------ //
+    protected Optional<RealmSearchNode> getQueryForCustomConds(
+            final SearchCond cond,
+            final boolean not,
+            final List<Object> parameters) {
+
+        return Optional.empty();
+    }
+
+    protected Optional<QueryInfo> getQuery(final SearchCond cond, final 
List<Object> parameters) {
+        if (cond == null) {
+            return Optional.empty();
+        }
+
+        boolean not = cond.getType() == SearchCond.Type.NOT_LEAF;
+
+        Optional<RealmSearchNode> node = Optional.empty();
+        Set<String> plainSchemas = new HashSet<>();
+
+        switch (cond.getType()) {
+            case LEAF:
+            case NOT_LEAF:
+                node = cond.asLeaf(AnyCond.class).
+                        map(anyCond -> getQuery(anyCond, not, parameters)).
+                        or(() -> cond.asLeaf(AttrCond.class).
+                        map(attrCond -> {
+                            CheckResult<AttrCond> checked = check(attrCond);
+                            AttrCondQuery query = getQuery(attrCond, not, 
checked, parameters);
+                            if (query.addPlainSchemas()) {
+                                plainSchemas.add(checked.schema().getKey());
+                            }
+                            return query.node();
+                        }));
+
+                if (node.isEmpty()) {
+                    node = getQueryForCustomConds(cond, not, parameters);
+                }
+                break;
+
+            case AND:
+                RealmSearchNode andNode = new 
RealmSearchNode(RealmSearchNode.Type.AND);
+
+                getQuery(cond.getLeft(), parameters).ifPresent(left -> {
+                    andNode.add(left.node());
+                    plainSchemas.addAll(left.plainSchemas());
+                });
+
+                getQuery(cond.getRight(), parameters).ifPresent(right -> {
+                    andNode.add(right.node());
+                    plainSchemas.addAll(right.plainSchemas());
+                });
+
+                if (!andNode.getChildren().isEmpty()) {
+                    node = Optional.of(andNode);
+                }
+                break;
+
+            case OR:
+                RealmSearchNode orNode = new 
RealmSearchNode(RealmSearchNode.Type.OR);
+
+                getQuery(cond.getLeft(), parameters).ifPresent(left -> {
+                    orNode.add(left.node());
+                    plainSchemas.addAll(left.plainSchemas());
+                });
+
+                getQuery(cond.getRight(), parameters).ifPresent(right -> {
+                    orNode.add(right.node());
+                    plainSchemas.addAll(right.plainSchemas());
+                });
+
+                if (!orNode.getChildren().isEmpty()) {
+                    node = Optional.of(orNode);
+                }
+                break;
+
+            default:
+        }
+
+        return node.map(n -> new QueryInfo(n, plainSchemas));
+    }
+
+    protected abstract AttrCondQuery getQuery(
+            AttrCond cond,
+            boolean not,
+            CheckResult<AttrCond> checked,
+            List<Object> parameters);
+
+    protected RealmSearchNode getQuery(final AnyCond cond, final boolean not, 
final List<Object> parameters) {
+        CheckResult<AnyCond> checked = check(
+                cond,
+                realmUtils.getField(cond.getSchema()).
+                        orElseThrow(() -> new 
IllegalArgumentException("Invalid schema " + cond.getSchema())),
+                RELATIONSHIP_FIELDS);
+
+        return switch (checked.cond().getType()) {
+            case ISNULL ->
+                new RealmSearchNode.Leaf("r." + checked.cond().getSchema() + 
(not ? " IS NOT NULL" : " IS NULL"));
+
+            case ISNOTNULL ->
+                new RealmSearchNode.Leaf("r." + checked.cond().getSchema() + 
(not ? " IS NULL" : " IS NOT NULL"));
+
+            default ->
+                fillAttrQuery(
+                "r." + checked.cond().getSchema(),
+                checked.value(),
+                checked.schema(),
+                checked.cond(),
+                not,
+                parameters);
+        };
+    }
+
+    protected RealmSearchNode.Leaf fillAttrQuery(
+            final String column,
+            final PlainAttrValue attrValue,
+            final PlainSchema schema,
+            final AttrCond cond,
+            final boolean not,
+            final List<Object> parameters) {
+
+        boolean ignoreCase = AttrCond.Type.ILIKE == cond.getType() || 
AttrCond.Type.IEQ == cond.getType();
+
+        String left = column;
+        if (ignoreCase && (schema.getType() == AttrSchemaType.String || 
schema.getType() == AttrSchemaType.Enum)) {
+            left = "LOWER(" + left + ')';
+        }
+
+        StringBuilder clause = new StringBuilder(left);
+        switch (cond.getType()) {
+
+            case ILIKE:
+            case LIKE:
+                if (schema.getType() == AttrSchemaType.String || 
schema.getType() == AttrSchemaType.Enum) {
+                    if (not) {
+                        clause.append(" NOT");
+                    }
+                    clause.append(" LIKE ");
+                    if (ignoreCase) {
+                        
clause.append("LOWER(?").append(setParameter(parameters, 
cond.getExpression())).append(')');
+                    } else {
+                        clause.append('?').append(setParameter(parameters, 
cond.getExpression()));
+                    }
+                    if (this instanceof OracleJPARealmSearchDAO) {
+                        clause.append(" ESCAPE '\\'");
+                    }
+                } else {
+                    LOG.error("LIKE is only compatible with string or enum 
schemas");
+                    return new RealmSearchNode.Leaf(ALWAYS_FALSE_CLAUSE);
+                }
+                break;
+
+            case IEQ:
+            case EQ:
+            default:
+                clause.append(not ? "<>" : "=");
+                if (ignoreCase
+                        && (schema.getType() == AttrSchemaType.String || 
schema.getType() == AttrSchemaType.Enum)) {
+                    clause.append("LOWER(?").append(setParameter(parameters, 
attrValue.getValue())).append(')');
+                } else {
+                    clause.append('?').append(setParameter(parameters, 
attrValue.getValue()));
+                }
+                break;
+
+            case GE:
+                clause.append(not ? "<" : ">=");
+                clause.append('?').append(setParameter(parameters, 
attrValue.getValue()));
+                break;
+
+            case GT:
+                clause.append(not ? "<=" : ">");
+                clause.append('?').append(setParameter(parameters, 
attrValue.getValue()));
+                break;
+
+            case LE:
+                clause.append(not ? ">" : "<=");
+                clause.append('?').append(setParameter(parameters, 
attrValue.getValue()));
+                break;
+
+            case LT:
+                clause.append(not ? ">=" : "<");
+                clause.append('?').append(setParameter(parameters, 
attrValue.getValue()));
+                break;
+        }
+
+        return new RealmSearchNode.Leaf(
+                cond instanceof AnyCond
+                        ? clause.toString()
+                        : "r.schema_id='" + schema.getKey() + "' AND " + 
clause);
+    }
+
+    protected void visitNode(final RealmSearchNode node, final List<String> 
where) {
+        node.asLeaf().ifPresentOrElse(
+                leaf -> where.add(leaf.getClause()),
+                () -> {
+                    List<String> nodeWhere = new ArrayList<>();
+                    node.getChildren().forEach(child -> visitNode(child, 
nodeWhere));
+                    String op = " " + node.getType().name() + " ";
+                    where.add(nodeWhere.stream().
+                            map(w -> w.contains(" AND ") || w.contains(" OR ") 
? "(" + w + ")" : w).
+                            collect(Collectors.joining(op)));
+                });
+    }
+
+    protected String buildFrom(final Set<String> plainSchemas, final 
OrderBySupport obs) {
+        return JPARealm.TABLE + " r";
+    }
+
+    protected String buildWhere(final Set<String> bases, final QueryInfo 
queryInfo, final List<Object> parameters) {
+        String fullPaths = bases.stream().
+                map(base -> "r.fullPath=?" + setParameter(parameters, base)
+                + " OR r.fullPath LIKE ?" + setParameter(
+                        parameters, SyncopeConstants.ROOT_REALM.equals(base) ? 
"/%" : base + "/%")).
+                collect(Collectors.joining(" OR "));
+
+        RealmSearchNode root;
+        if (queryInfo.node().getType() == RealmSearchNode.Type.AND) {
+            root = queryInfo.node();
+        } else {
+            root = new RealmSearchNode(RealmSearchNode.Type.AND);
+            root.add(queryInfo.node());
+        }
+
+        List<String> where = new ArrayList<>();
+        visitNode(root, where);
+
+        return "(" + fullPaths + ')'
+                + " AND (" + where.stream().
+                        map(w -> w.contains(" AND ") || w.contains(" OR ") ? 
"(" + w + ")" : w).
+                        collect(Collectors.joining(' ' + root.getType().name() 
+ ' '))
+                + ')';
+    }
+
+    @Override
+    protected long doCount(final Set<String> bases, final SearchCond cond) {
+        List<Object> parameters = new ArrayList<>();
+
+        QueryInfo queryInfo = getQuery(cond, parameters).orElse(null);
+        if (queryInfo == null) {
+            LOG.error("Invalid search condition: {}", cond);
+            return 0;
+        }
+
+        String queryString = new StringBuilder("SELECT COUNT(DISTINCT r.id)").
+                append(" FROM ").append(buildFrom(queryInfo.plainSchemas(), 
null)).
+                append(" WHERE ").append(buildWhere(bases, queryInfo, 
parameters)).
+                toString();
+
+        LOG.debug("Query: {}, parameters: {}", queryString, parameters);
+
+        Query query = entityManager.createNativeQuery(queryString);
+        fillWithParameters(query, parameters);
+
+        return ((Number) query.getSingleResult()).longValue();
+    }
+
+    protected abstract void parseOrderByForPlainSchema(
+            OrderBySupport obs,
+            OrderBySupport.Item item,
+            Sort.Order clause,
+            PlainSchema schema,
+            String fieldName);
+
+    protected void parseOrderByForField(
+            final OrderBySupport.Item item,
+            final String fieldName,
+            final Sort.Order clause) {
+
+        item.select = "r." + fieldName;
+        item.where = StringUtils.EMPTY;
+        item.orderBy = "r." + fieldName + ' ' + clause.getDirection().name();
+    }
+
+    protected void parseOrderByForCustom(
+            final Sort.Order clause,
+            final OrderBySupport.Item item,

Review Comment:
   ## Useless parameter
   
   The parameter 'item' is never used.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2568)



##########
ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java:
##########
@@ -240,8 +245,6 @@
         return builder;
     }
 
-    protected void customizeDocument(
-            final Map<String, Object> builder,
-            final AuditEvent auditEvent) {
+    protected void customizeDocument(final Map<String, Object> builder, final 
AuditEvent auditEvent) {

Review Comment:
   ## Useless parameter
   
   The parameter 'auditEvent' is never used.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2574)



##########
core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPARealmSearchDAO.java:
##########
@@ -0,0 +1,537 @@
+/*
+ * 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.jpa.dao;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.types.AttrSchemaType;
+import 
org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
+import org.apache.syncope.core.persistence.api.dao.MalformedPathException;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.apache.syncope.core.persistence.api.entity.Realm;
+import org.apache.syncope.core.persistence.api.utils.RealmUtils;
+import org.apache.syncope.core.persistence.common.dao.AbstractRealmSearchDAO;
+import org.apache.syncope.core.persistence.jpa.entity.JPARealm;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.transaction.annotation.Transactional;
+
+public abstract class AbstractJPARealmSearchDAO extends AbstractRealmSearchDAO 
{
+
+    protected record QueryInfo(RealmSearchNode node, Set<String> plainSchemas) 
{
+
+    }
+
+    protected record AttrCondQuery(Boolean addPlainSchemas, RealmSearchNode 
node) {
+
+    }
+
+    protected static int setParameter(final List<Object> parameters, final 
Object parameter) {
+        parameters.add(parameter);
+        return parameters.size();
+    }
+
+    protected static void fillWithParameters(final Query query, final 
List<Object> parameters) {
+        for (int i = 0; i < parameters.size(); i++) {
+            if (parameters.get(i) instanceof Boolean aBoolean) {
+                query.setParameter(i + 1, aBoolean ? 1 : 0);
+            } else {
+                query.setParameter(i + 1, parameters.get(i));
+            }
+        }
+    }
+
+    protected final EntityManager entityManager;
+
+    protected final RealmUtils realmUtils;
+
+    protected AbstractJPARealmSearchDAO(
+            final EntityManager entityManager,
+            final PlainSchemaDAO plainSchemaDAO,
+            final EntityFactory entityFactory,
+            final PlainAttrValidationManager validator,
+            final RealmUtils realmUtils) {
+
+        super(plainSchemaDAO, entityFactory, validator);
+
+        this.entityManager = entityManager;
+        this.realmUtils = realmUtils;
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public Optional<Realm> findByFullPath(final String fullPath) {
+        if (StringUtils.isBlank(fullPath)
+                || (!SyncopeConstants.ROOT_REALM.equals(fullPath)
+                && !RealmDAO.PATH_PATTERN.matcher(fullPath).matches())) {
+
+            throw new MalformedPathException(fullPath);
+        }
+
+        TypedQuery<Realm> query = entityManager.createQuery(
+                "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE 
e.fullPath=:fullPath", Realm.class);
+        query.setParameter("fullPath", fullPath);
+
+        Realm result = null;
+        try {
+            result = query.getSingleResult();
+        } catch (NoResultException e) {
+            LOG.debug("Realm with fullPath {} not found", fullPath, e);
+        }
+
+        return Optional.ofNullable(result);
+    }
+
+    @Override
+    public List<Realm> findByName(final String name) {
+        TypedQuery<Realm> query = entityManager.createQuery(
+                "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE 
e.name=:name", Realm.class);
+        query.setParameter("name", name);
+
+        return query.getResultList();
+    }
+
+    @Override
+    public List<Realm> findChildren(final Realm realm) {
+        TypedQuery<Realm> query = entityManager.createQuery(
+                "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE 
e.parent=:realm", Realm.class);
+        query.setParameter("realm", realm);
+
+        return query.getResultList();
+    }
+
+    @Override
+    public List<Realm> findDescendants(final String base, final String prefix) 
{
+        List<Object> parameters = new ArrayList<>();
+
+        StringBuilder queryString = new StringBuilder("SELECT e FROM ").
+                append(JPARealm.class.getSimpleName()).
+                append(" e WHERE 
(e.fullPath=?").append(setParameter(parameters, base)).
+                append(" OR e.fullPath LIKE ?").append(setParameter(
+                parameters,
+                SyncopeConstants.ROOT_REALM.equals(base) ? "/%" : base + 
"/%")).
+                append(") ");
+        if (prefix != null) {
+            queryString.append("AND 
(e.fullPath=?").append(setParameter(parameters, prefix)).
+                    append(" OR e.fullPath LIKE ?").append(setParameter(
+                    parameters,
+                    SyncopeConstants.ROOT_REALM.equals(prefix) ? "/%" : prefix 
+ "/%")).
+                    append(") ");
+        }
+        queryString.append("ORDER BY e.fullPath");
+
+        TypedQuery<Realm> query = 
entityManager.createQuery(queryString.toString(), Realm.class);
+
+        fillWithParameters(query, parameters);
+
+        return query.getResultList();
+    }
+
+    // ------------------------------------------ //
+    protected Optional<RealmSearchNode> getQueryForCustomConds(
+            final SearchCond cond,
+            final boolean not,
+            final List<Object> parameters) {
+
+        return Optional.empty();
+    }
+
+    protected Optional<QueryInfo> getQuery(final SearchCond cond, final 
List<Object> parameters) {
+        if (cond == null) {
+            return Optional.empty();
+        }
+
+        boolean not = cond.getType() == SearchCond.Type.NOT_LEAF;
+
+        Optional<RealmSearchNode> node = Optional.empty();
+        Set<String> plainSchemas = new HashSet<>();
+
+        switch (cond.getType()) {
+            case LEAF:
+            case NOT_LEAF:
+                node = cond.asLeaf(AnyCond.class).
+                        map(anyCond -> getQuery(anyCond, not, parameters)).
+                        or(() -> cond.asLeaf(AttrCond.class).
+                        map(attrCond -> {
+                            CheckResult<AttrCond> checked = check(attrCond);
+                            AttrCondQuery query = getQuery(attrCond, not, 
checked, parameters);
+                            if (query.addPlainSchemas()) {
+                                plainSchemas.add(checked.schema().getKey());
+                            }
+                            return query.node();
+                        }));
+
+                if (node.isEmpty()) {
+                    node = getQueryForCustomConds(cond, not, parameters);
+                }
+                break;
+
+            case AND:
+                RealmSearchNode andNode = new 
RealmSearchNode(RealmSearchNode.Type.AND);
+
+                getQuery(cond.getLeft(), parameters).ifPresent(left -> {
+                    andNode.add(left.node());
+                    plainSchemas.addAll(left.plainSchemas());
+                });
+
+                getQuery(cond.getRight(), parameters).ifPresent(right -> {
+                    andNode.add(right.node());
+                    plainSchemas.addAll(right.plainSchemas());
+                });
+
+                if (!andNode.getChildren().isEmpty()) {
+                    node = Optional.of(andNode);
+                }
+                break;
+
+            case OR:
+                RealmSearchNode orNode = new 
RealmSearchNode(RealmSearchNode.Type.OR);
+
+                getQuery(cond.getLeft(), parameters).ifPresent(left -> {
+                    orNode.add(left.node());
+                    plainSchemas.addAll(left.plainSchemas());
+                });
+
+                getQuery(cond.getRight(), parameters).ifPresent(right -> {
+                    orNode.add(right.node());
+                    plainSchemas.addAll(right.plainSchemas());
+                });
+
+                if (!orNode.getChildren().isEmpty()) {
+                    node = Optional.of(orNode);
+                }
+                break;
+
+            default:
+        }
+
+        return node.map(n -> new QueryInfo(n, plainSchemas));
+    }
+
+    protected abstract AttrCondQuery getQuery(
+            AttrCond cond,
+            boolean not,
+            CheckResult<AttrCond> checked,
+            List<Object> parameters);
+
+    protected RealmSearchNode getQuery(final AnyCond cond, final boolean not, 
final List<Object> parameters) {
+        CheckResult<AnyCond> checked = check(
+                cond,
+                realmUtils.getField(cond.getSchema()).
+                        orElseThrow(() -> new 
IllegalArgumentException("Invalid schema " + cond.getSchema())),
+                RELATIONSHIP_FIELDS);
+
+        return switch (checked.cond().getType()) {
+            case ISNULL ->
+                new RealmSearchNode.Leaf("r." + checked.cond().getSchema() + 
(not ? " IS NOT NULL" : " IS NULL"));
+
+            case ISNOTNULL ->
+                new RealmSearchNode.Leaf("r." + checked.cond().getSchema() + 
(not ? " IS NULL" : " IS NOT NULL"));
+
+            default ->
+                fillAttrQuery(
+                "r." + checked.cond().getSchema(),
+                checked.value(),
+                checked.schema(),
+                checked.cond(),
+                not,
+                parameters);
+        };
+    }
+
+    protected RealmSearchNode.Leaf fillAttrQuery(
+            final String column,
+            final PlainAttrValue attrValue,
+            final PlainSchema schema,
+            final AttrCond cond,
+            final boolean not,
+            final List<Object> parameters) {
+
+        boolean ignoreCase = AttrCond.Type.ILIKE == cond.getType() || 
AttrCond.Type.IEQ == cond.getType();
+
+        String left = column;
+        if (ignoreCase && (schema.getType() == AttrSchemaType.String || 
schema.getType() == AttrSchemaType.Enum)) {
+            left = "LOWER(" + left + ')';
+        }
+
+        StringBuilder clause = new StringBuilder(left);
+        switch (cond.getType()) {
+
+            case ILIKE:
+            case LIKE:
+                if (schema.getType() == AttrSchemaType.String || 
schema.getType() == AttrSchemaType.Enum) {
+                    if (not) {
+                        clause.append(" NOT");
+                    }
+                    clause.append(" LIKE ");
+                    if (ignoreCase) {
+                        
clause.append("LOWER(?").append(setParameter(parameters, 
cond.getExpression())).append(')');
+                    } else {
+                        clause.append('?').append(setParameter(parameters, 
cond.getExpression()));
+                    }
+                    if (this instanceof OracleJPARealmSearchDAO) {
+                        clause.append(" ESCAPE '\\'");
+                    }
+                } else {
+                    LOG.error("LIKE is only compatible with string or enum 
schemas");
+                    return new RealmSearchNode.Leaf(ALWAYS_FALSE_CLAUSE);
+                }
+                break;
+
+            case IEQ:
+            case EQ:
+            default:
+                clause.append(not ? "<>" : "=");
+                if (ignoreCase
+                        && (schema.getType() == AttrSchemaType.String || 
schema.getType() == AttrSchemaType.Enum)) {
+                    clause.append("LOWER(?").append(setParameter(parameters, 
attrValue.getValue())).append(')');
+                } else {
+                    clause.append('?').append(setParameter(parameters, 
attrValue.getValue()));
+                }
+                break;
+
+            case GE:
+                clause.append(not ? "<" : ">=");
+                clause.append('?').append(setParameter(parameters, 
attrValue.getValue()));
+                break;
+
+            case GT:
+                clause.append(not ? "<=" : ">");
+                clause.append('?').append(setParameter(parameters, 
attrValue.getValue()));
+                break;
+
+            case LE:
+                clause.append(not ? ">" : "<=");
+                clause.append('?').append(setParameter(parameters, 
attrValue.getValue()));
+                break;
+
+            case LT:
+                clause.append(not ? ">=" : "<");
+                clause.append('?').append(setParameter(parameters, 
attrValue.getValue()));
+                break;
+        }
+
+        return new RealmSearchNode.Leaf(
+                cond instanceof AnyCond
+                        ? clause.toString()
+                        : "r.schema_id='" + schema.getKey() + "' AND " + 
clause);
+    }
+
+    protected void visitNode(final RealmSearchNode node, final List<String> 
where) {
+        node.asLeaf().ifPresentOrElse(
+                leaf -> where.add(leaf.getClause()),
+                () -> {
+                    List<String> nodeWhere = new ArrayList<>();
+                    node.getChildren().forEach(child -> visitNode(child, 
nodeWhere));
+                    String op = " " + node.getType().name() + " ";
+                    where.add(nodeWhere.stream().
+                            map(w -> w.contains(" AND ") || w.contains(" OR ") 
? "(" + w + ")" : w).
+                            collect(Collectors.joining(op)));
+                });
+    }
+
+    protected String buildFrom(final Set<String> plainSchemas, final 
OrderBySupport obs) {
+        return JPARealm.TABLE + " r";
+    }
+
+    protected String buildWhere(final Set<String> bases, final QueryInfo 
queryInfo, final List<Object> parameters) {
+        String fullPaths = bases.stream().
+                map(base -> "r.fullPath=?" + setParameter(parameters, base)
+                + " OR r.fullPath LIKE ?" + setParameter(
+                        parameters, SyncopeConstants.ROOT_REALM.equals(base) ? 
"/%" : base + "/%")).
+                collect(Collectors.joining(" OR "));
+
+        RealmSearchNode root;
+        if (queryInfo.node().getType() == RealmSearchNode.Type.AND) {
+            root = queryInfo.node();
+        } else {
+            root = new RealmSearchNode(RealmSearchNode.Type.AND);
+            root.add(queryInfo.node());
+        }
+
+        List<String> where = new ArrayList<>();
+        visitNode(root, where);
+
+        return "(" + fullPaths + ')'
+                + " AND (" + where.stream().
+                        map(w -> w.contains(" AND ") || w.contains(" OR ") ? 
"(" + w + ")" : w).
+                        collect(Collectors.joining(' ' + root.getType().name() 
+ ' '))
+                + ')';
+    }
+
+    @Override
+    protected long doCount(final Set<String> bases, final SearchCond cond) {
+        List<Object> parameters = new ArrayList<>();
+
+        QueryInfo queryInfo = getQuery(cond, parameters).orElse(null);
+        if (queryInfo == null) {
+            LOG.error("Invalid search condition: {}", cond);
+            return 0;
+        }
+
+        String queryString = new StringBuilder("SELECT COUNT(DISTINCT r.id)").
+                append(" FROM ").append(buildFrom(queryInfo.plainSchemas(), 
null)).
+                append(" WHERE ").append(buildWhere(bases, queryInfo, 
parameters)).
+                toString();
+
+        LOG.debug("Query: {}, parameters: {}", queryString, parameters);
+
+        Query query = entityManager.createNativeQuery(queryString);
+        fillWithParameters(query, parameters);
+
+        return ((Number) query.getSingleResult()).longValue();
+    }
+
+    protected abstract void parseOrderByForPlainSchema(
+            OrderBySupport obs,
+            OrderBySupport.Item item,
+            Sort.Order clause,
+            PlainSchema schema,
+            String fieldName);
+
+    protected void parseOrderByForField(
+            final OrderBySupport.Item item,
+            final String fieldName,
+            final Sort.Order clause) {
+
+        item.select = "r." + fieldName;
+        item.where = StringUtils.EMPTY;
+        item.orderBy = "r." + fieldName + ' ' + clause.getDirection().name();
+    }
+
+    protected void parseOrderByForCustom(
+            final Sort.Order clause,

Review Comment:
   ## Useless parameter
   
   The parameter 'clause' is never used.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2567)



##########
ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAO.java:
##########
@@ -159,28 +189,199 @@
         });
         Query prefix = new 
Query.Builder().disMax(QueryBuilders.disMax().queries(basesQueries).build()).build();
 
-        if (keyword == null) {
-            return prefix;
+        BoolQuery.Builder boolBuilder = QueryBuilders.bool().filter(prefix);
+        if (searchCond != null) {
+            boolBuilder.filter(getQuery(searchCond));
         }
+        return new Query.Builder().bool(boolBuilder.build()).build();
+    }
 
-        return new Query.Builder().bool(QueryBuilders.bool().filter(
-                prefix,
-                new Query.Builder().wildcard(QueryBuilders.wildcard().
-                        field("name").value(keyword.replace('%', 
'*').replace("\\_", "_")).
-                        caseInsensitive(true).build()).build()).build()).
-                build();
+    protected Query getQuery(final SearchCond cond) {
+        Query query = null;
+
+        switch (cond.getType()) {
+            case LEAF, NOT_LEAF -> {
+                query = 
cond.asLeaf(AnyCond.class).map(this::getQuery).orElse(null);
+                if (query == null) {
+                    query = 
cond.asLeaf(AttrCond.class).map(this::getQuery).orElse(null);
+                }
+                if (query == null) {
+                    query = getQueryForCustomConds(cond);
+                }
+                if (query == null) {
+                    throw new IllegalArgumentException("Cannot construct 
QueryBuilder");
+                }
+                if (cond.getType() == SearchCond.Type.NOT_LEAF) {
+                    query = new 
Query.Builder().bool(QueryBuilders.bool().mustNot(query).build()).build();
+                }
+            }
+            case AND -> {
+                List<Query> andCompound = new ArrayList<>();
+                Query andLeft = getQuery(cond.getLeft());
+                if (andLeft.isBool() && !andLeft.bool().filter().isEmpty()) {
+                    andCompound.addAll(andLeft.bool().filter());
+                } else {
+                    andCompound.add(andLeft);
+                }
+                Query andRight = getQuery(cond.getRight());
+                if (andRight.isBool() && !andRight.bool().filter().isEmpty()) {
+                    andCompound.addAll(andRight.bool().filter());
+                } else {
+                    andCompound.add(andRight);
+                }
+                query = new 
Query.Builder().bool(QueryBuilders.bool().filter(andCompound).build()).build();
+            }
+            case OR -> {
+                List<Query> orCompound = new ArrayList<>();
+                Query orLeft = getQuery(cond.getLeft());
+                if (orLeft.isDisMax()) {
+                    orCompound.addAll(orLeft.disMax().queries());
+                } else {
+                    orCompound.add(orLeft);
+                }
+                Query orRight = getQuery(cond.getRight());
+                if (orRight.isDisMax()) {
+                    orCompound.addAll(orRight.disMax().queries());
+                } else {
+                    orCompound.add(orRight);
+                }
+                query = new 
Query.Builder().disMax(QueryBuilders.disMax().queries(orCompound).build()).build();
+            }
+            default -> {
+            }
+        }
+
+        return query;
     }
 
     @Override
-    public long countDescendants(final String base, final String keyword) {
-        return countDescendants(Set.of(base), keyword);
+    protected CheckResult<AnyCond> check(final AnyCond cond, final Field 
field, final Set<String> relationshipsFields) {
+        CheckResult<AnyCond> checked = super.check(cond, field, 
relationshipsFields);
+
+        // Manage difference between external id attribute and internal _id
+        if ("id".equals(checked.cond().getSchema())) {
+            checked.cond().setSchema("_id");
+        }
+        if ("id".equals(checked.schema().getKey())) {
+            checked.schema().setKey("_id");
+        }
+
+        return checked;
+    }
+
+    protected Query fillAttrQuery(
+            final PlainSchema schema,
+            final PlainAttrValue attrValue,
+            final AttrCond cond) {
+
+        Object value = schema.getType() == AttrSchemaType.Date && 
attrValue.getDateValue() != null
+                ? FormatUtils.format(attrValue.getDateValue())
+                : attrValue.getValue();
+
+        Query query = null;
+        switch (cond.getType()) {
+            case ISNOTNULL:
+                query = new 
Query.Builder().exists(QueryBuilders.exists().field(schema.getKey()).build()).build();
+                break;
+
+            case ISNULL:
+                query = new Query.Builder().bool(QueryBuilders.bool().mustNot(
+                        new 
Query.Builder().exists(QueryBuilders.exists().field(schema.getKey()).build())
+                                .build()).build()).build();
+                break;
+
+            case ILIKE:
+                query = new Query.Builder().wildcard(QueryBuilders.wildcard().
+                        
field(schema.getKey()).value(cond.getExpression().replace('%', 
'*').replace("\\_", "_")).
+                        caseInsensitive(true).build()).build();
+                break;
+
+            case LIKE:
+                query = new Query.Builder().wildcard(QueryBuilders.wildcard().
+                        
field(schema.getKey()).value(cond.getExpression().replace('%', 
'*').replace("\\_", "_")).
+                        caseInsensitive(false).build()).build();
+                break;
+
+            case IEQ:
+                query = new Query.Builder().term(QueryBuilders.term().
+                        
field(schema.getKey()).value(FieldValue.of(cond.getExpression())).caseInsensitive(true).
+                        build()).build();
+                break;
+
+            case EQ:
+                FieldValue fieldValue = switch (value) {
+                    case Double aDouble ->
+                        FieldValue.of(aDouble);
+                    case Long aLong ->
+                        FieldValue.of(aLong);
+                    case Boolean aBoolean ->
+                        FieldValue.of(aBoolean);
+                    default ->
+                        FieldValue.of(value.toString());
+                };
+                query = new Query.Builder().term(QueryBuilders.term().
+                        
field(schema.getKey()).value(fieldValue).caseInsensitive(false).build()).
+                        build();
+                break;
+
+            case GE:
+                query = new Query.Builder().range(RangeQuery.of(r -> 
r.untyped(n -> n.
+                        field(schema.getKey()).
+                        gte(JsonData.of(value))))).
+                        build();
+                break;
+
+            case GT:
+                query = new Query.Builder().range(RangeQuery.of(r -> 
r.untyped(n -> n.
+                        field(schema.getKey()).
+                        gt(JsonData.of(value))))).
+                        build();
+                break;
+
+            case LE:
+                query = new Query.Builder().range(RangeQuery.of(r -> 
r.untyped(n -> n.
+                        field(schema.getKey()).
+                        lte(JsonData.of(value))))).
+                        build();
+                break;
+
+            case LT:
+                query = new Query.Builder().range(RangeQuery.of(r -> 
r.untyped(n -> n.
+                        field(schema.getKey()).
+                        lt(JsonData.of(value))))).
+                        build();
+                break;
+
+            default:
+                break;
+        }
+
+        return query;
+    }
+
+    protected Query getQuery(final AttrCond cond) {
+        CheckResult<AttrCond> checked = check(cond);
+        return fillAttrQuery(checked.schema(), checked.value(), cond);
+    }
+
+    protected Query getQuery(final AnyCond cond) {
+        CheckResult<AnyCond> checked = check(
+                cond,
+                realmUtils.getField(cond.getSchema()).
+                        orElseThrow(() -> new 
IllegalArgumentException("Invalid schema " + cond.getSchema())),
+                RELATIONSHIP_FIELDS);
+        return fillAttrQuery(checked.schema(), checked.value(), 
checked.cond());
+    }
+
+    protected Query getQueryForCustomConds(final SearchCond cond) {

Review Comment:
   ## Useless parameter
   
   The parameter 'cond' is never used.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2566)



##########
core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPARealmSearchDAO.java:
##########
@@ -0,0 +1,537 @@
+/*
+ * 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.jpa.dao;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.types.AttrSchemaType;
+import 
org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
+import org.apache.syncope.core.persistence.api.dao.MalformedPathException;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.apache.syncope.core.persistence.api.entity.Realm;
+import org.apache.syncope.core.persistence.api.utils.RealmUtils;
+import org.apache.syncope.core.persistence.common.dao.AbstractRealmSearchDAO;
+import org.apache.syncope.core.persistence.jpa.entity.JPARealm;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.transaction.annotation.Transactional;
+
+public abstract class AbstractJPARealmSearchDAO extends AbstractRealmSearchDAO 
{
+
+    protected record QueryInfo(RealmSearchNode node, Set<String> plainSchemas) 
{
+
+    }
+
+    protected record AttrCondQuery(Boolean addPlainSchemas, RealmSearchNode 
node) {
+
+    }
+
+    protected static int setParameter(final List<Object> parameters, final 
Object parameter) {
+        parameters.add(parameter);
+        return parameters.size();
+    }
+
+    protected static void fillWithParameters(final Query query, final 
List<Object> parameters) {
+        for (int i = 0; i < parameters.size(); i++) {
+            if (parameters.get(i) instanceof Boolean aBoolean) {
+                query.setParameter(i + 1, aBoolean ? 1 : 0);
+            } else {
+                query.setParameter(i + 1, parameters.get(i));
+            }
+        }
+    }
+
+    protected final EntityManager entityManager;
+
+    protected final RealmUtils realmUtils;
+
+    protected AbstractJPARealmSearchDAO(
+            final EntityManager entityManager,
+            final PlainSchemaDAO plainSchemaDAO,
+            final EntityFactory entityFactory,
+            final PlainAttrValidationManager validator,
+            final RealmUtils realmUtils) {
+
+        super(plainSchemaDAO, entityFactory, validator);
+
+        this.entityManager = entityManager;
+        this.realmUtils = realmUtils;
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public Optional<Realm> findByFullPath(final String fullPath) {
+        if (StringUtils.isBlank(fullPath)
+                || (!SyncopeConstants.ROOT_REALM.equals(fullPath)
+                && !RealmDAO.PATH_PATTERN.matcher(fullPath).matches())) {
+
+            throw new MalformedPathException(fullPath);
+        }
+
+        TypedQuery<Realm> query = entityManager.createQuery(
+                "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE 
e.fullPath=:fullPath", Realm.class);
+        query.setParameter("fullPath", fullPath);
+
+        Realm result = null;
+        try {
+            result = query.getSingleResult();
+        } catch (NoResultException e) {
+            LOG.debug("Realm with fullPath {} not found", fullPath, e);
+        }
+
+        return Optional.ofNullable(result);
+    }
+
+    @Override
+    public List<Realm> findByName(final String name) {
+        TypedQuery<Realm> query = entityManager.createQuery(
+                "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE 
e.name=:name", Realm.class);
+        query.setParameter("name", name);
+
+        return query.getResultList();
+    }
+
+    @Override
+    public List<Realm> findChildren(final Realm realm) {
+        TypedQuery<Realm> query = entityManager.createQuery(
+                "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE 
e.parent=:realm", Realm.class);
+        query.setParameter("realm", realm);
+
+        return query.getResultList();
+    }
+
+    @Override
+    public List<Realm> findDescendants(final String base, final String prefix) 
{
+        List<Object> parameters = new ArrayList<>();
+
+        StringBuilder queryString = new StringBuilder("SELECT e FROM ").
+                append(JPARealm.class.getSimpleName()).
+                append(" e WHERE 
(e.fullPath=?").append(setParameter(parameters, base)).
+                append(" OR e.fullPath LIKE ?").append(setParameter(
+                parameters,
+                SyncopeConstants.ROOT_REALM.equals(base) ? "/%" : base + 
"/%")).
+                append(") ");
+        if (prefix != null) {
+            queryString.append("AND 
(e.fullPath=?").append(setParameter(parameters, prefix)).
+                    append(" OR e.fullPath LIKE ?").append(setParameter(
+                    parameters,
+                    SyncopeConstants.ROOT_REALM.equals(prefix) ? "/%" : prefix 
+ "/%")).
+                    append(") ");
+        }
+        queryString.append("ORDER BY e.fullPath");
+
+        TypedQuery<Realm> query = 
entityManager.createQuery(queryString.toString(), Realm.class);
+
+        fillWithParameters(query, parameters);
+
+        return query.getResultList();
+    }
+
+    // ------------------------------------------ //
+    protected Optional<RealmSearchNode> getQueryForCustomConds(
+            final SearchCond cond,
+            final boolean not,
+            final List<Object> parameters) {
+
+        return Optional.empty();
+    }
+
+    protected Optional<QueryInfo> getQuery(final SearchCond cond, final 
List<Object> parameters) {
+        if (cond == null) {
+            return Optional.empty();
+        }
+
+        boolean not = cond.getType() == SearchCond.Type.NOT_LEAF;
+
+        Optional<RealmSearchNode> node = Optional.empty();
+        Set<String> plainSchemas = new HashSet<>();
+
+        switch (cond.getType()) {
+            case LEAF:
+            case NOT_LEAF:
+                node = cond.asLeaf(AnyCond.class).
+                        map(anyCond -> getQuery(anyCond, not, parameters)).
+                        or(() -> cond.asLeaf(AttrCond.class).
+                        map(attrCond -> {
+                            CheckResult<AttrCond> checked = check(attrCond);
+                            AttrCondQuery query = getQuery(attrCond, not, 
checked, parameters);
+                            if (query.addPlainSchemas()) {
+                                plainSchemas.add(checked.schema().getKey());
+                            }
+                            return query.node();
+                        }));
+
+                if (node.isEmpty()) {
+                    node = getQueryForCustomConds(cond, not, parameters);
+                }
+                break;
+
+            case AND:
+                RealmSearchNode andNode = new 
RealmSearchNode(RealmSearchNode.Type.AND);
+
+                getQuery(cond.getLeft(), parameters).ifPresent(left -> {
+                    andNode.add(left.node());
+                    plainSchemas.addAll(left.plainSchemas());
+                });
+
+                getQuery(cond.getRight(), parameters).ifPresent(right -> {
+                    andNode.add(right.node());
+                    plainSchemas.addAll(right.plainSchemas());
+                });
+
+                if (!andNode.getChildren().isEmpty()) {
+                    node = Optional.of(andNode);
+                }
+                break;
+
+            case OR:
+                RealmSearchNode orNode = new 
RealmSearchNode(RealmSearchNode.Type.OR);
+
+                getQuery(cond.getLeft(), parameters).ifPresent(left -> {
+                    orNode.add(left.node());
+                    plainSchemas.addAll(left.plainSchemas());
+                });
+
+                getQuery(cond.getRight(), parameters).ifPresent(right -> {
+                    orNode.add(right.node());
+                    plainSchemas.addAll(right.plainSchemas());
+                });
+
+                if (!orNode.getChildren().isEmpty()) {
+                    node = Optional.of(orNode);
+                }
+                break;
+
+            default:
+        }
+
+        return node.map(n -> new QueryInfo(n, plainSchemas));
+    }
+
+    protected abstract AttrCondQuery getQuery(
+            AttrCond cond,
+            boolean not,
+            CheckResult<AttrCond> checked,
+            List<Object> parameters);
+
+    protected RealmSearchNode getQuery(final AnyCond cond, final boolean not, 
final List<Object> parameters) {
+        CheckResult<AnyCond> checked = check(
+                cond,
+                realmUtils.getField(cond.getSchema()).
+                        orElseThrow(() -> new 
IllegalArgumentException("Invalid schema " + cond.getSchema())),
+                RELATIONSHIP_FIELDS);
+
+        return switch (checked.cond().getType()) {
+            case ISNULL ->
+                new RealmSearchNode.Leaf("r." + checked.cond().getSchema() + 
(not ? " IS NOT NULL" : " IS NULL"));
+
+            case ISNOTNULL ->
+                new RealmSearchNode.Leaf("r." + checked.cond().getSchema() + 
(not ? " IS NULL" : " IS NOT NULL"));
+
+            default ->
+                fillAttrQuery(
+                "r." + checked.cond().getSchema(),
+                checked.value(),
+                checked.schema(),
+                checked.cond(),
+                not,
+                parameters);
+        };
+    }
+
+    protected RealmSearchNode.Leaf fillAttrQuery(
+            final String column,
+            final PlainAttrValue attrValue,
+            final PlainSchema schema,
+            final AttrCond cond,
+            final boolean not,
+            final List<Object> parameters) {
+
+        boolean ignoreCase = AttrCond.Type.ILIKE == cond.getType() || 
AttrCond.Type.IEQ == cond.getType();
+
+        String left = column;
+        if (ignoreCase && (schema.getType() == AttrSchemaType.String || 
schema.getType() == AttrSchemaType.Enum)) {
+            left = "LOWER(" + left + ')';
+        }
+
+        StringBuilder clause = new StringBuilder(left);
+        switch (cond.getType()) {
+
+            case ILIKE:
+            case LIKE:
+                if (schema.getType() == AttrSchemaType.String || 
schema.getType() == AttrSchemaType.Enum) {
+                    if (not) {
+                        clause.append(" NOT");
+                    }
+                    clause.append(" LIKE ");
+                    if (ignoreCase) {
+                        
clause.append("LOWER(?").append(setParameter(parameters, 
cond.getExpression())).append(')');
+                    } else {
+                        clause.append('?').append(setParameter(parameters, 
cond.getExpression()));
+                    }
+                    if (this instanceof OracleJPARealmSearchDAO) {
+                        clause.append(" ESCAPE '\\'");
+                    }
+                } else {
+                    LOG.error("LIKE is only compatible with string or enum 
schemas");
+                    return new RealmSearchNode.Leaf(ALWAYS_FALSE_CLAUSE);
+                }
+                break;
+
+            case IEQ:
+            case EQ:
+            default:
+                clause.append(not ? "<>" : "=");
+                if (ignoreCase
+                        && (schema.getType() == AttrSchemaType.String || 
schema.getType() == AttrSchemaType.Enum)) {
+                    clause.append("LOWER(?").append(setParameter(parameters, 
attrValue.getValue())).append(')');
+                } else {
+                    clause.append('?').append(setParameter(parameters, 
attrValue.getValue()));
+                }
+                break;
+
+            case GE:
+                clause.append(not ? "<" : ">=");
+                clause.append('?').append(setParameter(parameters, 
attrValue.getValue()));
+                break;
+
+            case GT:
+                clause.append(not ? "<=" : ">");
+                clause.append('?').append(setParameter(parameters, 
attrValue.getValue()));
+                break;
+
+            case LE:
+                clause.append(not ? ">" : "<=");
+                clause.append('?').append(setParameter(parameters, 
attrValue.getValue()));
+                break;
+
+            case LT:
+                clause.append(not ? ">=" : "<");
+                clause.append('?').append(setParameter(parameters, 
attrValue.getValue()));
+                break;
+        }
+
+        return new RealmSearchNode.Leaf(
+                cond instanceof AnyCond
+                        ? clause.toString()
+                        : "r.schema_id='" + schema.getKey() + "' AND " + 
clause);
+    }
+
+    protected void visitNode(final RealmSearchNode node, final List<String> 
where) {
+        node.asLeaf().ifPresentOrElse(
+                leaf -> where.add(leaf.getClause()),
+                () -> {
+                    List<String> nodeWhere = new ArrayList<>();
+                    node.getChildren().forEach(child -> visitNode(child, 
nodeWhere));
+                    String op = " " + node.getType().name() + " ";
+                    where.add(nodeWhere.stream().
+                            map(w -> w.contains(" AND ") || w.contains(" OR ") 
? "(" + w + ")" : w).
+                            collect(Collectors.joining(op)));
+                });
+    }
+
+    protected String buildFrom(final Set<String> plainSchemas, final 
OrderBySupport obs) {
+        return JPARealm.TABLE + " r";
+    }
+
+    protected String buildWhere(final Set<String> bases, final QueryInfo 
queryInfo, final List<Object> parameters) {
+        String fullPaths = bases.stream().
+                map(base -> "r.fullPath=?" + setParameter(parameters, base)
+                + " OR r.fullPath LIKE ?" + setParameter(
+                        parameters, SyncopeConstants.ROOT_REALM.equals(base) ? 
"/%" : base + "/%")).
+                collect(Collectors.joining(" OR "));
+
+        RealmSearchNode root;
+        if (queryInfo.node().getType() == RealmSearchNode.Type.AND) {
+            root = queryInfo.node();
+        } else {
+            root = new RealmSearchNode(RealmSearchNode.Type.AND);
+            root.add(queryInfo.node());
+        }
+
+        List<String> where = new ArrayList<>();
+        visitNode(root, where);
+
+        return "(" + fullPaths + ')'
+                + " AND (" + where.stream().
+                        map(w -> w.contains(" AND ") || w.contains(" OR ") ? 
"(" + w + ")" : w).
+                        collect(Collectors.joining(' ' + root.getType().name() 
+ ' '))
+                + ')';
+    }
+
+    @Override
+    protected long doCount(final Set<String> bases, final SearchCond cond) {
+        List<Object> parameters = new ArrayList<>();
+
+        QueryInfo queryInfo = getQuery(cond, parameters).orElse(null);
+        if (queryInfo == null) {
+            LOG.error("Invalid search condition: {}", cond);
+            return 0;
+        }
+
+        String queryString = new StringBuilder("SELECT COUNT(DISTINCT r.id)").
+                append(" FROM ").append(buildFrom(queryInfo.plainSchemas(), 
null)).
+                append(" WHERE ").append(buildWhere(bases, queryInfo, 
parameters)).
+                toString();
+
+        LOG.debug("Query: {}, parameters: {}", queryString, parameters);
+
+        Query query = entityManager.createNativeQuery(queryString);
+        fillWithParameters(query, parameters);
+
+        return ((Number) query.getSingleResult()).longValue();
+    }
+
+    protected abstract void parseOrderByForPlainSchema(
+            OrderBySupport obs,
+            OrderBySupport.Item item,
+            Sort.Order clause,
+            PlainSchema schema,
+            String fieldName);
+
+    protected void parseOrderByForField(
+            final OrderBySupport.Item item,
+            final String fieldName,
+            final Sort.Order clause) {
+
+        item.select = "r." + fieldName;
+        item.where = StringUtils.EMPTY;
+        item.orderBy = "r." + fieldName + ' ' + clause.getDirection().name();
+    }
+
+    protected void parseOrderByForCustom(
+            final Sort.Order clause,
+            final OrderBySupport.Item item,
+            final OrderBySupport obs) {

Review Comment:
   ## Useless parameter
   
   The parameter 'obs' is never used.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2569)



##########
ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAO.java:
##########
@@ -161,53 +188,252 @@
         });
         Query prefix = new 
Query.Builder().disMax(QueryBuilders.disMax().queries(basesQueries).build()).build();
 
-        if (keyword == null) {
-            return prefix;
+        BoolQuery.Builder boolBuilder = QueryBuilders.bool().filter(prefix);
+        if (searchCond != null) {
+            boolBuilder.filter(getQuery(searchCond));
         }
+        return new Query.Builder().bool(boolBuilder.build()).build();
+    }
 
-        return new Query.Builder().bool(QueryBuilders.bool().filter(
-                prefix,
-                new Query.Builder().wildcard(QueryBuilders.wildcard().
-                        field("name").value(keyword.replace('%', 
'*').replace("\\_", "_")).
-                        caseInsensitive(true).build()).build()).build()).
-                build();
+    protected Query getQuery(final SearchCond cond) {
+        Query query = null;
+
+        switch (cond.getType()) {
+            case LEAF, NOT_LEAF -> {
+                query = 
cond.asLeaf(AnyCond.class).map(this::getQuery).orElse(null);
+                if (query == null) {
+                    query = 
cond.asLeaf(AttrCond.class).map(this::getQuery).orElse(null);
+                }
+                if (query == null) {
+                    query = getQueryForCustomConds(cond);
+                }
+                if (query == null) {
+                    throw new IllegalArgumentException("Cannot construct 
QueryBuilder");
+                }
+                if (cond.getType() == SearchCond.Type.NOT_LEAF) {
+                    query = new 
Query.Builder().bool(QueryBuilders.bool().mustNot(query).build()).build();
+                }
+            }
+            case AND -> {
+                List<Query> andCompound = new ArrayList<>();
+                Query andLeft = getQuery(cond.getLeft());
+                if (andLeft.isBool() && !andLeft.bool().filter().isEmpty()) {
+                    andCompound.addAll(andLeft.bool().filter());
+                } else {
+                    andCompound.add(andLeft);
+                }
+                Query andRight = getQuery(cond.getRight());
+                if (andRight.isBool() && !andRight.bool().filter().isEmpty()) {
+                    andCompound.addAll(andRight.bool().filter());
+                } else {
+                    andCompound.add(andRight);
+                }
+                query = new 
Query.Builder().bool(QueryBuilders.bool().filter(andCompound).build()).build();
+            }
+            case OR -> {
+                List<Query> orCompound = new ArrayList<>();
+                Query orLeft = getQuery(cond.getLeft());
+                if (orLeft.isDisMax()) {
+                    orCompound.addAll(orLeft.disMax().queries());
+                } else {
+                    orCompound.add(orLeft);
+                }
+                Query orRight = getQuery(cond.getRight());
+                if (orRight.isDisMax()) {
+                    orCompound.addAll(orRight.disMax().queries());
+                } else {
+                    orCompound.add(orRight);
+                }
+                query = new 
Query.Builder().disMax(QueryBuilders.disMax().queries(orCompound).build()).build();
+            }
+            default -> {
+            }
+        }
+
+        return query;
     }
 
     @Override
-    public long countDescendants(final String base, final String keyword) {
-        return countDescendants(Set.of(base), keyword);
+    protected CheckResult<AnyCond> check(final AnyCond cond, final Field 
field, final Set<String> relationshipsFields) {
+        CheckResult<AnyCond> checked = super.check(cond, field, 
relationshipsFields);
+
+        // Manage difference between external id attribute and internal _id
+        if ("id".equals(checked.cond().getSchema())) {
+            checked.cond().setSchema("_id");
+        }
+        if ("id".equals(checked.schema().getKey())) {
+            checked.schema().setKey("_id");
+        }
+
+        return checked;
+    }
+
+    protected Query fillAttrQuery(
+            final PlainSchema schema,
+            final PlainAttrValue attrValue,
+            final AttrCond cond) {
+
+        Object value = schema.getType() == AttrSchemaType.Date && 
attrValue.getDateValue() != null
+                ? FormatUtils.format(attrValue.getDateValue())
+                : attrValue.getValue();
+
+        Query query = null;
+        switch (cond.getType()) {
+            case ISNOTNULL:
+                query = new 
Query.Builder().exists(QueryBuilders.exists().field(schema.getKey()).build()).build();
+                break;
+
+            case ISNULL:
+                query = new Query.Builder().bool(QueryBuilders.bool().mustNot(
+                        new 
Query.Builder().exists(QueryBuilders.exists().field(schema.getKey()).build())
+                                .build()).build()).build();
+                break;
+
+            case ILIKE:
+                query = new Query.Builder().wildcard(QueryBuilders.wildcard().
+                        
field(schema.getKey()).value(cond.getExpression().replace('%', 
'*').replace("\\_", "_")).
+                        caseInsensitive(true).build()).build();
+                break;
+
+            case LIKE:
+                query = new Query.Builder().wildcard(QueryBuilders.wildcard().
+                        
field(schema.getKey()).value(cond.getExpression().replace('%', 
'*').replace("\\_", "_")).
+                        caseInsensitive(false).build()).build();
+                break;
+
+            case IEQ:
+                query = new Query.Builder().term(QueryBuilders.term().
+                        
field(schema.getKey()).value(FieldValue.of(cond.getExpression())).caseInsensitive(true).
+                        build()).build();
+                break;
+
+            case EQ:
+                FieldValue fieldValue = switch (value) {
+                    case Double aDouble ->
+                        FieldValue.of(aDouble);
+                    case Long aLong ->
+                        FieldValue.of(aLong);
+                    case Boolean aBoolean ->
+                        FieldValue.of(aBoolean);
+                    default ->
+                        FieldValue.of(value.toString());
+                };
+                query = new Query.Builder().term(QueryBuilders.term().
+                        
field(schema.getKey()).value(fieldValue).caseInsensitive(false).build()).
+                        build();
+                break;
+
+            case GE:
+                query = new Query.Builder().range(QueryBuilders.range().
+                        
field(schema.getKey()).gte(JsonData.of(value)).build()).
+                        build();
+                break;
+
+            case GT:
+                query = new Query.Builder().range(QueryBuilders.range().
+                        field(schema.getKey()).gt(JsonData.of(value)).build()).
+                        build();
+                break;
+
+            case LE:
+                query = new Query.Builder().range(QueryBuilders.range().
+                        
field(schema.getKey()).lte(JsonData.of(value)).build()).
+                        build();
+                break;
+
+            case LT:
+                query = new Query.Builder().range(QueryBuilders.range().
+                        field(schema.getKey()).lt(JsonData.of(value)).build()).
+                        build();
+                break;
+
+            default:
+                break;
+        }
+
+        return query;
+    }
+
+    protected Query getQuery(final AttrCond cond) {
+        CheckResult<AttrCond> checked = check(cond);
+        return fillAttrQuery(checked.schema(), checked.value(), cond);
+    }
+
+    protected Query getQuery(final AnyCond cond) {
+        CheckResult<AnyCond> checked = check(
+                cond,
+                realmUtils.getField(cond.getSchema()).
+                        orElseThrow(() -> new 
IllegalArgumentException("Invalid schema " + cond.getSchema())),
+                RELATIONSHIP_FIELDS);
+        return fillAttrQuery(checked.schema(), checked.value(), 
checked.cond());
+    }
+
+    protected Query getQueryForCustomConds(final SearchCond cond) {

Review Comment:
   ## Useless parameter
   
   The parameter 'cond' is never used.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2570)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to