Browse Source

Refactor ApiKeyFieldNameTranslators to expose query builder translator (#105057)

This exposes a public method that deep-copies a QueryBuilder,
to be used for querying API Keys from the .security index,
while also translating the query-level field names to index-level ones.

Relates #104895
Albert Zaharovits 1 year ago
parent
commit
7d522145de

+ 4 - 4
server/src/main/java/org/elasticsearch/index/query/SimpleQueryStringBuilder.java

@@ -280,14 +280,14 @@ public final class SimpleQueryStringBuilder extends AbstractQueryBuilder<SimpleQ
         return this;
     }
 
-    /** For testing and serialisation only. */
-    SimpleQueryStringBuilder flags(int flags) {
+    /** For testing, builder instance copy, and serialisation only. */
+    public SimpleQueryStringBuilder flags(int flags) {
         this.flags = flags;
         return this;
     }
 
-    /** For testing only: Return the flags set for this query. */
-    int flags() {
+    /** For testing and instance copy only: Return the flags set for this query. */
+    public int flags() {
         return this.flags;
     }
 

+ 0 - 20
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyRequest.java

@@ -7,11 +7,9 @@
 
 package org.elasticsearch.xpack.core.security.action.apikey;
 
-import org.elasticsearch.TransportVersions;
 import org.elasticsearch.action.ActionRequest;
 import org.elasticsearch.action.ActionRequestValidationException;
 import org.elasticsearch.action.support.TransportAction;
-import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.core.Nullable;
 import org.elasticsearch.index.query.QueryBuilder;
@@ -62,24 +60,6 @@ public final class QueryApiKeyRequest extends ActionRequest {
         this.withLimitedBy = withLimitedBy;
     }
 
-    public QueryApiKeyRequest(StreamInput in) throws IOException {
-        super(in);
-        this.queryBuilder = in.readOptionalNamedWriteable(QueryBuilder.class);
-        this.from = in.readOptionalVInt();
-        this.size = in.readOptionalVInt();
-        if (in.readBoolean()) {
-            this.fieldSortBuilders = in.readCollectionAsList(FieldSortBuilder::new);
-        } else {
-            this.fieldSortBuilders = null;
-        }
-        this.searchAfterBuilder = in.readOptionalWriteable(SearchAfterBuilder::new);
-        if (in.getTransportVersion().onOrAfter(TransportVersions.V_8_5_0)) {
-            this.withLimitedBy = in.readBoolean();
-        } else {
-            this.withLimitedBy = false;
-        }
-    }
-
     public QueryBuilder getQueryBuilder() {
         return queryBuilder;
     }

+ 1 - 43
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyAction.java

@@ -13,7 +13,6 @@ import org.elasticsearch.action.support.ActionFilters;
 import org.elasticsearch.action.support.TransportAction;
 import org.elasticsearch.common.inject.Inject;
 import org.elasticsearch.search.builder.SearchSourceBuilder;
-import org.elasticsearch.search.sort.FieldSortBuilder;
 import org.elasticsearch.tasks.Task;
 import org.elasticsearch.transport.TransportService;
 import org.elasticsearch.xpack.core.security.SecurityContext;
@@ -23,13 +22,11 @@ import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyResponse;
 import org.elasticsearch.xpack.core.security.authc.Authentication;
 import org.elasticsearch.xpack.security.authc.ApiKeyService;
 import org.elasticsearch.xpack.security.support.ApiKeyBoolQueryBuilder;
-import org.elasticsearch.xpack.security.support.ApiKeyFieldNameTranslators;
 
-import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Consumer;
 
+import static org.elasticsearch.xpack.security.support.ApiKeyFieldNameTranslators.translateFieldSortBuilders;
 import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_MAIN_ALIAS;
 
 public final class TransportQueryApiKeyAction extends TransportAction<QueryApiKeyRequest, QueryApiKeyResponse> {
@@ -109,43 +106,4 @@ public final class TransportQueryApiKeyAction extends TransportAction<QueryApiKe
         apiKeyService.queryApiKeys(searchRequest, request.withLimitedBy(), listener);
     }
 
-    // package private for testing
-    static void translateFieldSortBuilders(
-        List<FieldSortBuilder> fieldSortBuilders,
-        SearchSourceBuilder searchSourceBuilder,
-        Consumer<String> fieldNameVisitor
-    ) {
-        fieldSortBuilders.forEach(fieldSortBuilder -> {
-            if (fieldSortBuilder.getNestedSort() != null) {
-                throw new IllegalArgumentException("nested sorting is not supported for API Key query");
-            }
-            if (FieldSortBuilder.DOC_FIELD_NAME.equals(fieldSortBuilder.getFieldName())) {
-                searchSourceBuilder.sort(fieldSortBuilder);
-            } else {
-                final String translatedFieldName = ApiKeyFieldNameTranslators.translate(fieldSortBuilder.getFieldName());
-                fieldNameVisitor.accept(translatedFieldName);
-                if (translatedFieldName.equals(fieldSortBuilder.getFieldName())) {
-                    searchSourceBuilder.sort(fieldSortBuilder);
-                } else {
-                    final FieldSortBuilder translatedFieldSortBuilder = new FieldSortBuilder(translatedFieldName).order(
-                        fieldSortBuilder.order()
-                    )
-                        .missing(fieldSortBuilder.missing())
-                        .unmappedType(fieldSortBuilder.unmappedType())
-                        .setFormat(fieldSortBuilder.getFormat());
-
-                    if (fieldSortBuilder.sortMode() != null) {
-                        translatedFieldSortBuilder.sortMode(fieldSortBuilder.sortMode());
-                    }
-                    if (fieldSortBuilder.getNestedSort() != null) {
-                        translatedFieldSortBuilder.setNestedSort(fieldSortBuilder.getNestedSort());
-                    }
-                    if (fieldSortBuilder.getNumericType() != null) {
-                        translatedFieldSortBuilder.setNumericType(fieldSortBuilder.getNumericType());
-                    }
-                    searchSourceBuilder.sort(translatedFieldSortBuilder);
-                }
-            }
-        });
-    }
 }

+ 2 - 145
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilder.java

@@ -10,33 +10,20 @@ package org.elasticsearch.xpack.security.support;
 import org.apache.lucene.search.Query;
 import org.elasticsearch.core.Nullable;
 import org.elasticsearch.index.query.BoolQueryBuilder;
-import org.elasticsearch.index.query.ExistsQueryBuilder;
-import org.elasticsearch.index.query.IdsQueryBuilder;
-import org.elasticsearch.index.query.MatchAllQueryBuilder;
-import org.elasticsearch.index.query.MatchNoneQueryBuilder;
-import org.elasticsearch.index.query.MatchQueryBuilder;
-import org.elasticsearch.index.query.PrefixQueryBuilder;
 import org.elasticsearch.index.query.QueryBuilder;
 import org.elasticsearch.index.query.QueryBuilders;
 import org.elasticsearch.index.query.QueryRewriteContext;
-import org.elasticsearch.index.query.RangeQueryBuilder;
 import org.elasticsearch.index.query.SearchExecutionContext;
-import org.elasticsearch.index.query.SimpleQueryStringBuilder;
-import org.elasticsearch.index.query.TermQueryBuilder;
-import org.elasticsearch.index.query.TermsQueryBuilder;
-import org.elasticsearch.index.query.WildcardQueryBuilder;
-import org.elasticsearch.index.search.QueryParserHelper;
 import org.elasticsearch.xpack.core.security.authc.Authentication;
 import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
 import org.elasticsearch.xpack.security.authc.ApiKeyService;
 
 import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
 
 import static org.elasticsearch.xpack.security.action.apikey.TransportQueryApiKeyAction.API_KEY_TYPE_RUNTIME_MAPPING_FIELD;
+import static org.elasticsearch.xpack.security.support.ApiKeyFieldNameTranslators.translateQueryBuilderFields;
 
 public class ApiKeyBoolQueryBuilder extends BoolQueryBuilder {
 
@@ -82,7 +69,7 @@ public class ApiKeyBoolQueryBuilder extends BoolQueryBuilder {
     ) {
         final ApiKeyBoolQueryBuilder finalQuery = new ApiKeyBoolQueryBuilder();
         if (queryBuilder != null) {
-            QueryBuilder processedQuery = doProcess(queryBuilder, fieldNameVisitor);
+            QueryBuilder processedQuery = translateQueryBuilderFields(queryBuilder, fieldNameVisitor);
             finalQuery.must(processedQuery);
         }
         finalQuery.filter(QueryBuilders.termQuery("doc_type", "api_key"));
@@ -108,136 +95,6 @@ public class ApiKeyBoolQueryBuilder extends BoolQueryBuilder {
         return finalQuery;
     }
 
-    private static QueryBuilder doProcess(QueryBuilder qb, Consumer<String> fieldNameVisitor) {
-        if (qb instanceof final BoolQueryBuilder query) {
-            final BoolQueryBuilder newQuery = QueryBuilders.boolQuery()
-                .minimumShouldMatch(query.minimumShouldMatch())
-                .adjustPureNegative(query.adjustPureNegative())
-                .boost(query.boost());
-            query.must().stream().map(q -> ApiKeyBoolQueryBuilder.doProcess(q, fieldNameVisitor)).forEach(newQuery::must);
-            query.should().stream().map(q -> ApiKeyBoolQueryBuilder.doProcess(q, fieldNameVisitor)).forEach(newQuery::should);
-            query.mustNot().stream().map(q -> ApiKeyBoolQueryBuilder.doProcess(q, fieldNameVisitor)).forEach(newQuery::mustNot);
-            query.filter().stream().map(q -> ApiKeyBoolQueryBuilder.doProcess(q, fieldNameVisitor)).forEach(newQuery::filter);
-            return newQuery;
-        } else if (qb instanceof MatchAllQueryBuilder) {
-            return qb;
-        } else if (qb instanceof IdsQueryBuilder) {
-            return qb;
-        } else if (qb instanceof final TermQueryBuilder query) {
-            final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName());
-            fieldNameVisitor.accept(translatedFieldName);
-            return QueryBuilders.termQuery(translatedFieldName, query.value())
-                .caseInsensitive(query.caseInsensitive())
-                .boost(query.boost());
-        } else if (qb instanceof final ExistsQueryBuilder query) {
-            final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName());
-            fieldNameVisitor.accept(translatedFieldName);
-            return QueryBuilders.existsQuery(translatedFieldName).boost(query.boost());
-        } else if (qb instanceof final TermsQueryBuilder query) {
-            if (query.termsLookup() != null) {
-                throw new IllegalArgumentException("terms query with terms lookup is not supported for API Key query");
-            }
-            final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName());
-            fieldNameVisitor.accept(translatedFieldName);
-            return QueryBuilders.termsQuery(translatedFieldName, query.getValues()).boost(query.boost());
-        } else if (qb instanceof final PrefixQueryBuilder query) {
-            final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName());
-            fieldNameVisitor.accept(translatedFieldName);
-            return QueryBuilders.prefixQuery(translatedFieldName, query.value())
-                .caseInsensitive(query.caseInsensitive())
-                .rewrite(query.rewrite())
-                .boost(query.boost());
-        } else if (qb instanceof final WildcardQueryBuilder query) {
-            final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName());
-            fieldNameVisitor.accept(translatedFieldName);
-            return QueryBuilders.wildcardQuery(translatedFieldName, query.value())
-                .caseInsensitive(query.caseInsensitive())
-                .rewrite(query.rewrite())
-                .boost(query.boost());
-        } else if (qb instanceof final MatchQueryBuilder query) {
-            final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName());
-            fieldNameVisitor.accept(translatedFieldName);
-            final MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery(translatedFieldName, query.value());
-            if (query.operator() != null) {
-                matchQueryBuilder.operator(query.operator());
-            }
-            if (query.analyzer() != null) {
-                matchQueryBuilder.analyzer(query.analyzer());
-            }
-            if (query.fuzziness() != null) {
-                matchQueryBuilder.fuzziness(query.fuzziness());
-            }
-            if (query.minimumShouldMatch() != null) {
-                matchQueryBuilder.minimumShouldMatch(query.minimumShouldMatch());
-            }
-            if (query.fuzzyRewrite() != null) {
-                matchQueryBuilder.fuzzyRewrite(query.fuzzyRewrite());
-            }
-            if (query.zeroTermsQuery() != null) {
-                matchQueryBuilder.zeroTermsQuery(query.zeroTermsQuery());
-            }
-            matchQueryBuilder.prefixLength(query.prefixLength())
-                .maxExpansions(query.maxExpansions())
-                .fuzzyTranspositions(query.fuzzyTranspositions())
-                .lenient(query.lenient())
-                .autoGenerateSynonymsPhraseQuery(query.autoGenerateSynonymsPhraseQuery())
-                .boost(query.boost());
-            return matchQueryBuilder;
-        } else if (qb instanceof final RangeQueryBuilder query) {
-            if (query.relation() != null) {
-                throw new IllegalArgumentException("range query with relation is not supported for API Key query");
-            }
-            final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName());
-            fieldNameVisitor.accept(translatedFieldName);
-            final RangeQueryBuilder newQuery = QueryBuilders.rangeQuery(translatedFieldName);
-            if (query.format() != null) {
-                newQuery.format(query.format());
-            }
-            if (query.timeZone() != null) {
-                newQuery.timeZone(query.timeZone());
-            }
-            if (query.from() != null) {
-                newQuery.from(query.from()).includeLower(query.includeLower());
-            }
-            if (query.to() != null) {
-                newQuery.to(query.to()).includeUpper(query.includeUpper());
-            }
-            return newQuery.boost(query.boost());
-        } else if (qb instanceof final SimpleQueryStringBuilder simpleQueryStringBuilder) {
-            if (simpleQueryStringBuilder.fields().isEmpty()) {
-                simpleQueryStringBuilder.field("*");
-            }
-            // override lenient if querying all the fields, because, due to different field mappings,
-            // the query parsing will almost certainly fail otherwise
-            if (QueryParserHelper.hasAllFieldsWildcard(simpleQueryStringBuilder.fields().keySet())) {
-                simpleQueryStringBuilder.lenient(true);
-            }
-            Map<String, Float> requestedFields = new HashMap<>(simpleQueryStringBuilder.fields());
-            simpleQueryStringBuilder.fields().clear();
-            for (Map.Entry<String, Float> requestedFieldNameOrPattern : requestedFields.entrySet()) {
-                for (String translatedField : ApiKeyFieldNameTranslators.translatePattern(requestedFieldNameOrPattern.getKey())) {
-                    simpleQueryStringBuilder.fields()
-                        .compute(
-                            translatedField,
-                            (k, v) -> (v == null) ? requestedFieldNameOrPattern.getValue() : v * requestedFieldNameOrPattern.getValue()
-                        );
-                    fieldNameVisitor.accept(translatedField);
-                }
-            }
-            if (simpleQueryStringBuilder.fields().isEmpty()) {
-                // A SimpleQueryStringBuilder with empty fields() will eventually produce a SimpleQueryString query
-                // that accesses all the fields, including disallowed ones.
-                // Instead, the behavior we're after is that a query that accesses only disallowed fields should
-                // not match any docs.
-                return new MatchNoneQueryBuilder();
-            } else {
-                return simpleQueryStringBuilder;
-            }
-        } else {
-            throw new IllegalArgumentException("Query type [" + qb.getName() + "] is not supported for API Key query");
-        }
-    }
-
     @Override
     protected Query doToQuery(SearchExecutionContext context) throws IOException {
         context.setAllowedFields(ApiKeyBoolQueryBuilder::isIndexFieldNameAllowed);

+ 237 - 8
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyFieldNameTranslators.java

@@ -8,10 +8,33 @@
 package org.elasticsearch.xpack.security.support;
 
 import org.elasticsearch.common.regex.Regex;
+import org.elasticsearch.core.Nullable;
+import org.elasticsearch.index.query.AbstractQueryBuilder;
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.ExistsQueryBuilder;
+import org.elasticsearch.index.query.IdsQueryBuilder;
+import org.elasticsearch.index.query.MatchAllQueryBuilder;
+import org.elasticsearch.index.query.MatchNoneQueryBuilder;
+import org.elasticsearch.index.query.MatchQueryBuilder;
+import org.elasticsearch.index.query.PrefixQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.index.query.RangeQueryBuilder;
+import org.elasticsearch.index.query.SimpleQueryStringBuilder;
+import org.elasticsearch.index.query.TermQueryBuilder;
+import org.elasticsearch.index.query.TermsQueryBuilder;
+import org.elasticsearch.index.query.WildcardQueryBuilder;
+import org.elasticsearch.index.search.QueryParserHelper;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.elasticsearch.search.sort.FieldSortBuilder;
 
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
+import java.util.function.Consumer;
 import java.util.function.Function;
 
 import static org.elasticsearch.xpack.security.action.apikey.TransportQueryApiKeyAction.API_KEY_TYPE_RUNTIME_MAPPING_FIELD;
@@ -38,37 +61,243 @@ public class ApiKeyFieldNameTranslators {
         );
     }
 
+    /**
+     * Adds the {@param fieldSortBuilders} to the {@param searchSourceBuilder}, translating the field names,
+     * form query level to index level, see {@link #translate}.
+     * The optional {@param visitor} can be used to collect all the translated field names.
+     */
+    public static void translateFieldSortBuilders(
+        List<FieldSortBuilder> fieldSortBuilders,
+        SearchSourceBuilder searchSourceBuilder,
+        @Nullable Consumer<String> visitor
+    ) {
+        final Consumer<String> fieldNameVisitor = visitor != null ? visitor : ignored -> {};
+        fieldSortBuilders.forEach(fieldSortBuilder -> {
+            if (fieldSortBuilder.getNestedSort() != null) {
+                throw new IllegalArgumentException("nested sorting is not supported for API Key query");
+            }
+            if (FieldSortBuilder.DOC_FIELD_NAME.equals(fieldSortBuilder.getFieldName())) {
+                searchSourceBuilder.sort(fieldSortBuilder);
+            } else {
+                final String translatedFieldName = translate(fieldSortBuilder.getFieldName());
+                fieldNameVisitor.accept(translatedFieldName);
+                if (translatedFieldName.equals(fieldSortBuilder.getFieldName())) {
+                    searchSourceBuilder.sort(fieldSortBuilder);
+                } else {
+                    final FieldSortBuilder translatedFieldSortBuilder = new FieldSortBuilder(translatedFieldName).order(
+                        fieldSortBuilder.order()
+                    )
+                        .missing(fieldSortBuilder.missing())
+                        .unmappedType(fieldSortBuilder.unmappedType())
+                        .setFormat(fieldSortBuilder.getFormat());
+
+                    if (fieldSortBuilder.sortMode() != null) {
+                        translatedFieldSortBuilder.sortMode(fieldSortBuilder.sortMode());
+                    }
+                    if (fieldSortBuilder.getNestedSort() != null) {
+                        translatedFieldSortBuilder.setNestedSort(fieldSortBuilder.getNestedSort());
+                    }
+                    if (fieldSortBuilder.getNumericType() != null) {
+                        translatedFieldSortBuilder.setNumericType(fieldSortBuilder.getNumericType());
+                    }
+                    searchSourceBuilder.sort(translatedFieldSortBuilder);
+                }
+            }
+        });
+    }
+
+    /**
+     * Deep copies the passed-in {@param queryBuilder} translating all the field names, from query level to index level,
+     * see {@link  #translate}. In general, the returned builder should create the same query as if the query were
+     * created by the passed in {@param queryBuilder}, only with the field names translated.
+     * Field name patterns (including "*"), are also replaced with the explicit index level field names whose
+     * associated query level field names match the pattern.
+     * The optional {@param visitor} can be used to collect all the translated field names.
+     */
+    public static QueryBuilder translateQueryBuilderFields(QueryBuilder queryBuilder, @Nullable Consumer<String> visitor) {
+        Objects.requireNonNull(queryBuilder, "unsupported \"null\" query builder for field name translation");
+        final Consumer<String> fieldNameVisitor = visitor != null ? visitor : ignored -> {};
+        if (queryBuilder instanceof final BoolQueryBuilder query) {
+            final BoolQueryBuilder newQuery = QueryBuilders.boolQuery()
+                .minimumShouldMatch(query.minimumShouldMatch())
+                .adjustPureNegative(query.adjustPureNegative())
+                .boost(query.boost())
+                .queryName(query.queryName());
+            query.must().stream().map(q -> translateQueryBuilderFields(q, fieldNameVisitor)).forEach(newQuery::must);
+            query.should().stream().map(q -> translateQueryBuilderFields(q, fieldNameVisitor)).forEach(newQuery::should);
+            query.mustNot().stream().map(q -> translateQueryBuilderFields(q, fieldNameVisitor)).forEach(newQuery::mustNot);
+            query.filter().stream().map(q -> translateQueryBuilderFields(q, fieldNameVisitor)).forEach(newQuery::filter);
+            return newQuery;
+        } else if (queryBuilder instanceof final MatchAllQueryBuilder query) {
+            // just be safe and consistent to always return a new copy instance of the translated query builders
+            return QueryBuilders.matchAllQuery().boost(query.boost()).queryName(query.queryName());
+        } else if (queryBuilder instanceof final IdsQueryBuilder query) {
+            // just be safe and consistent to always return a new copy instance of the translated query builders
+            return QueryBuilders.idsQuery().addIds(query.ids().toArray(new String[0])).boost(query.boost()).queryName(query.queryName());
+        } else if (queryBuilder instanceof final TermQueryBuilder query) {
+            final String translatedFieldName = translate(query.fieldName());
+            fieldNameVisitor.accept(translatedFieldName);
+            return QueryBuilders.termQuery(translatedFieldName, query.value())
+                .caseInsensitive(query.caseInsensitive())
+                .boost(query.boost())
+                .queryName(query.queryName());
+        } else if (queryBuilder instanceof final ExistsQueryBuilder query) {
+            final String translatedFieldName = translate(query.fieldName());
+            fieldNameVisitor.accept(translatedFieldName);
+            return QueryBuilders.existsQuery(translatedFieldName).boost(query.boost()).queryName(query.queryName());
+        } else if (queryBuilder instanceof final TermsQueryBuilder query) {
+            if (query.termsLookup() != null) {
+                throw new IllegalArgumentException("terms query with terms lookup is not supported for API Key query");
+            }
+            final String translatedFieldName = translate(query.fieldName());
+            fieldNameVisitor.accept(translatedFieldName);
+            return QueryBuilders.termsQuery(translatedFieldName, query.getValues()).boost(query.boost()).queryName(query.queryName());
+        } else if (queryBuilder instanceof final PrefixQueryBuilder query) {
+            final String translatedFieldName = translate(query.fieldName());
+            fieldNameVisitor.accept(translatedFieldName);
+            return QueryBuilders.prefixQuery(translatedFieldName, query.value())
+                .caseInsensitive(query.caseInsensitive())
+                .rewrite(query.rewrite())
+                .boost(query.boost())
+                .queryName(query.queryName());
+        } else if (queryBuilder instanceof final WildcardQueryBuilder query) {
+            final String translatedFieldName = translate(query.fieldName());
+            fieldNameVisitor.accept(translatedFieldName);
+            return QueryBuilders.wildcardQuery(translatedFieldName, query.value())
+                .caseInsensitive(query.caseInsensitive())
+                .rewrite(query.rewrite())
+                .boost(query.boost())
+                .queryName(query.queryName());
+        } else if (queryBuilder instanceof final MatchQueryBuilder query) {
+            final String translatedFieldName = translate(query.fieldName());
+            fieldNameVisitor.accept(translatedFieldName);
+            final MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery(translatedFieldName, query.value());
+            if (query.operator() != null) {
+                matchQueryBuilder.operator(query.operator());
+            }
+            if (query.analyzer() != null) {
+                matchQueryBuilder.analyzer(query.analyzer());
+            }
+            if (query.fuzziness() != null) {
+                matchQueryBuilder.fuzziness(query.fuzziness());
+            }
+            if (query.minimumShouldMatch() != null) {
+                matchQueryBuilder.minimumShouldMatch(query.minimumShouldMatch());
+            }
+            if (query.fuzzyRewrite() != null) {
+                matchQueryBuilder.fuzzyRewrite(query.fuzzyRewrite());
+            }
+            if (query.zeroTermsQuery() != null) {
+                matchQueryBuilder.zeroTermsQuery(query.zeroTermsQuery());
+            }
+            matchQueryBuilder.prefixLength(query.prefixLength())
+                .maxExpansions(query.maxExpansions())
+                .fuzzyTranspositions(query.fuzzyTranspositions())
+                .lenient(query.lenient())
+                .autoGenerateSynonymsPhraseQuery(query.autoGenerateSynonymsPhraseQuery())
+                .boost(query.boost())
+                .queryName(query.queryName());
+            return matchQueryBuilder;
+        } else if (queryBuilder instanceof final RangeQueryBuilder query) {
+            if (query.relation() != null) {
+                throw new IllegalArgumentException("range query with relation is not supported for API Key query");
+            }
+            final String translatedFieldName = translate(query.fieldName());
+            fieldNameVisitor.accept(translatedFieldName);
+            final RangeQueryBuilder newQuery = QueryBuilders.rangeQuery(translatedFieldName);
+            if (query.format() != null) {
+                newQuery.format(query.format());
+            }
+            if (query.timeZone() != null) {
+                newQuery.timeZone(query.timeZone());
+            }
+            if (query.from() != null) {
+                newQuery.from(query.from()).includeLower(query.includeLower());
+            }
+            if (query.to() != null) {
+                newQuery.to(query.to()).includeUpper(query.includeUpper());
+            }
+            return newQuery.boost(query.boost()).queryName(query.queryName());
+        } else if (queryBuilder instanceof final SimpleQueryStringBuilder query) {
+            SimpleQueryStringBuilder simpleQueryStringBuilder = QueryBuilders.simpleQueryStringQuery(query.value());
+            Map<String, Float> queryFields = new HashMap<>(query.fields());
+            // be explicit that no field means all fields
+            if (queryFields.isEmpty()) {
+                queryFields.put("*", AbstractQueryBuilder.DEFAULT_BOOST);
+            }
+            // override "lenient" if querying all the fields, because, due to different field mappings,
+            // the query parsing will almost certainly fail otherwise
+            if (QueryParserHelper.hasAllFieldsWildcard(queryFields.keySet())) {
+                simpleQueryStringBuilder.lenient(true);
+            } else {
+                simpleQueryStringBuilder.lenient(query.lenient());
+            }
+            // translate query-level field name patterns to index-level concrete field names
+            for (Map.Entry<String, Float> requestedFieldNameOrPattern : queryFields.entrySet()) {
+                for (String translatedField : translatePattern(requestedFieldNameOrPattern.getKey())) {
+                    simpleQueryStringBuilder.fields()
+                        .compute(
+                            translatedField,
+                            (k, v) -> (v == null) ? requestedFieldNameOrPattern.getValue() : v * requestedFieldNameOrPattern.getValue()
+                        );
+                    fieldNameVisitor.accept(translatedField);
+                }
+            }
+            if (simpleQueryStringBuilder.fields().isEmpty()) {
+                // A SimpleQueryStringBuilder with empty fields() will eventually produce a SimpleQueryString
+                // Lucene query that accesses all the fields, including disallowed ones.
+                // Instead, the behavior we're after here is that a query that accesses only disallowed fields
+                // mustn't match any docs.
+                return new MatchNoneQueryBuilder().boost(simpleQueryStringBuilder.boost()).queryName(simpleQueryStringBuilder.queryName());
+            }
+            return simpleQueryStringBuilder.analyzer(query.analyzer())
+                .defaultOperator(query.defaultOperator())
+                .minimumShouldMatch(query.minimumShouldMatch())
+                .flags(query.flags())
+                .type(query.type())
+                .quoteFieldSuffix(query.quoteFieldSuffix())
+                .analyzeWildcard(query.analyzeWildcard())
+                .autoGenerateSynonymsPhraseQuery(query.autoGenerateSynonymsPhraseQuery())
+                .fuzzyTranspositions(query.fuzzyTranspositions())
+                .fuzzyMaxExpansions(query.fuzzyMaxExpansions())
+                .fuzzyPrefixLength(query.fuzzyPrefixLength())
+                .boost(query.boost())
+                .queryName(query.queryName());
+        } else {
+            throw new IllegalArgumentException("Query type [" + queryBuilder.getName() + "] is not supported for API Key query");
+        }
+    }
+
     /**
      * Translate the query level field name to index level field names.
      * It throws an exception if the field name is not explicitly allowed.
      */
-    public static String translate(String fieldName) {
+    protected static String translate(String fieldName) {
+        // protected for testing
         if (Regex.isSimpleMatchPattern(fieldName)) {
-            throw new IllegalArgumentException("Field name pattern [" + fieldName + "] is not allowed for API Key query");
+            throw new IllegalArgumentException("Field name pattern [" + fieldName + "] is not allowed for API Key query or aggregation");
         }
         for (FieldNameTranslator translator : FIELD_NAME_TRANSLATORS) {
             if (translator.supports(fieldName)) {
                 return translator.translate(fieldName);
             }
         }
-        throw new IllegalArgumentException("Field [" + fieldName + "] is not allowed for API Key query");
+        throw new IllegalArgumentException("Field [" + fieldName + "] is not allowed for API Key query or aggregation");
     }
 
     /**
      * Translates a query level field name pattern to the matching index level field names.
      * The result can be the empty set, if the pattern doesn't match any of the allowed index level field names.
-     * If the pattern is actually a concrete field name rather than a pattern,
-     * it is also translated, but only if the query level field name is allowed, otherwise an exception is thrown.
      */
-    public static Set<String> translatePattern(String fieldNameOrPattern) {
+    private static Set<String> translatePattern(String fieldNameOrPattern) {
         Set<String> indexFieldNames = new HashSet<>();
         for (FieldNameTranslator translator : FIELD_NAME_TRANSLATORS) {
             if (translator.supports(fieldNameOrPattern)) {
                 indexFieldNames.add(translator.translate(fieldNameOrPattern));
             }
         }
-        // It's OK to "translate" to the empty set the concrete disallowed or unknown field names, because
-        // the SimpleQueryString query type is lenient in the sense that it ignores unknown fields and field name patterns,
+        // It's OK to "translate" to the empty set the concrete disallowed or unknown field names.
+        // For eg, the SimpleQueryString query type is lenient in the sense that it ignores unknown fields and field name patterns,
         // so this preprocessing can ignore them too.
         return indexFieldNames;
     }

+ 3 - 2
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyActionTests.java

@@ -13,6 +13,7 @@ import org.elasticsearch.search.sort.NestedSortBuilder;
 import org.elasticsearch.search.sort.SortMode;
 import org.elasticsearch.search.sort.SortOrder;
 import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xpack.security.support.ApiKeyFieldNameTranslators;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -42,7 +43,7 @@ public class TransportQueryApiKeyActionTests extends ESTestCase {
 
         List<String> sortFields = new ArrayList<>();
         final SearchSourceBuilder searchSourceBuilder = SearchSourceBuilder.searchSource();
-        TransportQueryApiKeyAction.translateFieldSortBuilders(originals, searchSourceBuilder, sortFields::add);
+        ApiKeyFieldNameTranslators.translateFieldSortBuilders(originals, searchSourceBuilder, sortFields::add);
 
         IntStream.range(0, originals.size()).forEach(i -> {
             final FieldSortBuilder original = originals.get(i);
@@ -95,7 +96,7 @@ public class TransportQueryApiKeyActionTests extends ESTestCase {
         fieldSortBuilder.setNestedSort(new NestedSortBuilder("name"));
         final IllegalArgumentException e = expectThrows(
             IllegalArgumentException.class,
-            () -> TransportQueryApiKeyAction.translateFieldSortBuilders(
+            () -> ApiKeyFieldNameTranslators.translateFieldSortBuilders(
                 List.of(fieldSortBuilder),
                 SearchSourceBuilder.searchSource(),
                 ignored -> {}

+ 167 - 4
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java

@@ -15,6 +15,7 @@ import org.elasticsearch.index.query.IdsQueryBuilder;
 import org.elasticsearch.index.query.MatchAllQueryBuilder;
 import org.elasticsearch.index.query.MatchNoneQueryBuilder;
 import org.elasticsearch.index.query.MatchQueryBuilder;
+import org.elasticsearch.index.query.MultiMatchQueryBuilder;
 import org.elasticsearch.index.query.MultiTermQueryBuilder;
 import org.elasticsearch.index.query.Operator;
 import org.elasticsearch.index.query.PrefixQueryBuilder;
@@ -23,6 +24,7 @@ import org.elasticsearch.index.query.QueryBuilders;
 import org.elasticsearch.index.query.RangeQueryBuilder;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.index.query.SimpleQueryStringBuilder;
+import org.elasticsearch.index.query.SimpleQueryStringFlag;
 import org.elasticsearch.index.query.SpanQueryBuilder;
 import org.elasticsearch.index.query.TermQueryBuilder;
 import org.elasticsearch.index.query.TermsQueryBuilder;
@@ -44,6 +46,7 @@ import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.function.Predicate;
 
 import static org.elasticsearch.test.LambdaMatchers.falseWith;
@@ -119,6 +122,9 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
         if (randomBoolean()) {
             prefixQueryBuilder.boost(Math.abs(randomFloat()));
         }
+        if (randomBoolean()) {
+            prefixQueryBuilder.queryName(randomAlphaOfLengthBetween(0, 4));
+        }
         if (randomBoolean()) {
             prefixQueryBuilder.caseInsensitive(randomBoolean());
         }
@@ -135,10 +141,156 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
         assertThat(prefixQueryBuilder2.fieldName(), is(ApiKeyFieldNameTranslators.translate(prefixQueryBuilder.fieldName())));
         assertThat(prefixQueryBuilder2.value(), is(prefixQueryBuilder.value()));
         assertThat(prefixQueryBuilder2.boost(), is(prefixQueryBuilder.boost()));
+        assertThat(prefixQueryBuilder2.queryName(), is(prefixQueryBuilder.queryName()));
         assertThat(prefixQueryBuilder2.caseInsensitive(), is(prefixQueryBuilder.caseInsensitive()));
         assertThat(prefixQueryBuilder2.rewrite(), is(prefixQueryBuilder.rewrite()));
     }
 
+    public void testSimpleQueryBuilderWithAllFields() {
+        SimpleQueryStringBuilder simpleQueryStringBuilder = QueryBuilders.simpleQueryStringQuery(randomAlphaOfLength(4));
+        if (randomBoolean()) {
+            if (randomBoolean()) {
+                simpleQueryStringBuilder.field("*");
+            } else {
+                simpleQueryStringBuilder.field("*", Math.abs(randomFloat()));
+            }
+        }
+        if (randomBoolean()) {
+            simpleQueryStringBuilder.lenient(randomBoolean());
+        }
+        List<String> queryFields = new ArrayList<>();
+        ApiKeyBoolQueryBuilder apiKeyMatchQueryBuilder = ApiKeyBoolQueryBuilder.build(simpleQueryStringBuilder, queryFields::add, null);
+        List<QueryBuilder> mustQueries = apiKeyMatchQueryBuilder.must();
+        assertThat(mustQueries, hasSize(1));
+        assertThat(mustQueries.get(0), instanceOf(SimpleQueryStringBuilder.class));
+        SimpleQueryStringBuilder simpleQueryStringBuilder2 = (SimpleQueryStringBuilder) mustQueries.get(0);
+        assertThat(
+            simpleQueryStringBuilder2.fields().keySet(),
+            containsInAnyOrder(
+                "creation_time",
+                "invalidation_time",
+                "expiration_time",
+                "api_key_invalidated",
+                "creator.principal",
+                "creator.realm",
+                "metadata_flattened",
+                "name",
+                "runtime_key_type"
+            )
+        );
+        assertThat(simpleQueryStringBuilder2.lenient(), is(true));
+        assertThat(
+            queryFields,
+            containsInAnyOrder(
+                "doc_type",
+                "creation_time",
+                "invalidation_time",
+                "expiration_time",
+                "api_key_invalidated",
+                "creator.principal",
+                "creator.realm",
+                "metadata_flattened",
+                "name",
+                "runtime_key_type"
+            )
+        );
+    }
+
+    public void testSimpleQueryBuilderPropertiesArePreserved() {
+        SimpleQueryStringBuilder simpleQueryStringBuilder = QueryBuilders.simpleQueryStringQuery(randomAlphaOfLength(4));
+        if (randomBoolean()) {
+            simpleQueryStringBuilder.boost(Math.abs(randomFloat()));
+        }
+        if (randomBoolean()) {
+            simpleQueryStringBuilder.queryName(randomAlphaOfLengthBetween(0, 4));
+        }
+        if (randomBoolean()) {
+            simpleQueryStringBuilder.analyzer(randomAlphaOfLength(4));
+        }
+        if (randomBoolean()) {
+            simpleQueryStringBuilder.defaultOperator(randomFrom(Operator.OR, Operator.AND));
+        }
+        if (randomBoolean()) {
+            simpleQueryStringBuilder.minimumShouldMatch(randomAlphaOfLength(4));
+        }
+        if (randomBoolean()) {
+            simpleQueryStringBuilder.analyzeWildcard(randomBoolean());
+        }
+        if (randomBoolean()) {
+            simpleQueryStringBuilder.autoGenerateSynonymsPhraseQuery(randomBoolean());
+        }
+        if (randomBoolean()) {
+            simpleQueryStringBuilder.lenient(randomBoolean());
+        }
+        if (randomBoolean()) {
+            simpleQueryStringBuilder.type(randomFrom(MultiMatchQueryBuilder.Type.values()));
+        }
+        if (randomBoolean()) {
+            simpleQueryStringBuilder.quoteFieldSuffix(randomAlphaOfLength(4));
+        }
+        if (randomBoolean()) {
+            simpleQueryStringBuilder.fuzzyTranspositions(randomBoolean());
+        }
+        if (randomBoolean()) {
+            simpleQueryStringBuilder.fuzzyMaxExpansions(randomIntBetween(1, 10));
+        }
+        if (randomBoolean()) {
+            simpleQueryStringBuilder.fuzzyPrefixLength(randomIntBetween(1, 10));
+        }
+        if (randomBoolean()) {
+            simpleQueryStringBuilder.flags(
+                randomSubsetOf(randomIntBetween(0, 3), SimpleQueryStringFlag.values()).toArray(new SimpleQueryStringFlag[0])
+            );
+        }
+        // at least one field for this test
+        int nFields = randomIntBetween(1, 4);
+        for (int i = 0; i < nFields; i++) {
+            simpleQueryStringBuilder.field(randomValidFieldName(), Math.abs(randomFloat()));
+        }
+        List<String> queryFields = new ArrayList<>();
+        ApiKeyBoolQueryBuilder apiKeyMatchQueryBuilder = ApiKeyBoolQueryBuilder.build(
+            simpleQueryStringBuilder,
+            queryFields::add,
+            randomFrom(
+                AuthenticationTests.randomApiKeyAuthentication(AuthenticationTests.randomUser(), randomUUID()),
+                AuthenticationTests.randomAuthentication(null, null),
+                null
+            )
+        );
+        List<QueryBuilder> mustQueries = apiKeyMatchQueryBuilder.must();
+        assertThat(mustQueries, hasSize(1));
+        assertThat(mustQueries.get(0), instanceOf(SimpleQueryStringBuilder.class));
+        SimpleQueryStringBuilder simpleQueryStringBuilder2 = (SimpleQueryStringBuilder) mustQueries.get(0);
+        assertThat(simpleQueryStringBuilder2.value(), is(simpleQueryStringBuilder.value()));
+        assertThat(simpleQueryStringBuilder2.boost(), is(simpleQueryStringBuilder.boost()));
+        assertThat(simpleQueryStringBuilder2.queryName(), is(simpleQueryStringBuilder.queryName()));
+        assertThat(simpleQueryStringBuilder2.fields().size(), is(simpleQueryStringBuilder.fields().size()));
+        for (Map.Entry<String, Float> fieldEntry : simpleQueryStringBuilder.fields().entrySet()) {
+            assertThat(
+                simpleQueryStringBuilder2.fields().get(ApiKeyFieldNameTranslators.translate(fieldEntry.getKey())),
+                is(fieldEntry.getValue())
+            );
+        }
+        for (String field : simpleQueryStringBuilder2.fields().keySet()) {
+            assertThat(queryFields, hasItem(field));
+        }
+        assertThat(simpleQueryStringBuilder2.analyzer(), is(simpleQueryStringBuilder.analyzer()));
+        assertThat(simpleQueryStringBuilder2.defaultOperator(), is(simpleQueryStringBuilder.defaultOperator()));
+        assertThat(simpleQueryStringBuilder2.minimumShouldMatch(), is(simpleQueryStringBuilder.minimumShouldMatch()));
+        assertThat(simpleQueryStringBuilder2.analyzeWildcard(), is(simpleQueryStringBuilder.analyzeWildcard()));
+        assertThat(
+            simpleQueryStringBuilder2.autoGenerateSynonymsPhraseQuery(),
+            is(simpleQueryStringBuilder.autoGenerateSynonymsPhraseQuery())
+        );
+        assertThat(simpleQueryStringBuilder2.lenient(), is(simpleQueryStringBuilder.lenient()));
+        assertThat(simpleQueryStringBuilder2.type(), is(simpleQueryStringBuilder.type()));
+        assertThat(simpleQueryStringBuilder2.quoteFieldSuffix(), is(simpleQueryStringBuilder.quoteFieldSuffix()));
+        assertThat(simpleQueryStringBuilder2.fuzzyTranspositions(), is(simpleQueryStringBuilder.fuzzyTranspositions()));
+        assertThat(simpleQueryStringBuilder2.fuzzyMaxExpansions(), is(simpleQueryStringBuilder.fuzzyMaxExpansions()));
+        assertThat(simpleQueryStringBuilder2.fuzzyPrefixLength(), is(simpleQueryStringBuilder.fuzzyPrefixLength()));
+        assertThat(simpleQueryStringBuilder2.flags(), is(simpleQueryStringBuilder.flags()));
+    }
+
     public void testMatchQueryBuilderPropertiesArePreserved() {
         // the match query has many properties, that all must be preserved after limiting for API Key docs only
         Authentication authentication = randomFrom(
@@ -151,6 +303,9 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
         if (randomBoolean()) {
             matchQueryBuilder.boost(Math.abs(randomFloat()));
         }
+        if (randomBoolean()) {
+            matchQueryBuilder.queryName(randomAlphaOfLengthBetween(0, 4));
+        }
         if (randomBoolean()) {
             matchQueryBuilder.operator(randomFrom(Operator.OR, Operator.AND));
         }
@@ -205,6 +360,7 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
         assertThat(matchQueryBuilder2.lenient(), is(matchQueryBuilder.lenient()));
         assertThat(matchQueryBuilder2.autoGenerateSynonymsPhraseQuery(), is(matchQueryBuilder.autoGenerateSynonymsPhraseQuery()));
         assertThat(matchQueryBuilder2.boost(), is(matchQueryBuilder.boost()));
+        assertThat(matchQueryBuilder2.queryName(), is(matchQueryBuilder.queryName()));
     }
 
     public void testQueryForDomainAuthentication() {
@@ -925,20 +1081,27 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
 
     private QueryBuilder randomSimpleQuery(String fieldName) {
         return switch (randomIntBetween(0, 9)) {
-            case 0 -> QueryBuilders.termQuery(fieldName, randomAlphaOfLengthBetween(3, 8));
-            case 1 -> QueryBuilders.termsQuery(fieldName, randomArray(1, 3, String[]::new, () -> randomAlphaOfLengthBetween(3, 8)));
+            case 0 -> QueryBuilders.termQuery(fieldName, randomAlphaOfLengthBetween(3, 8))
+                .boost(Math.abs(randomFloat()))
+                .queryName(randomAlphaOfLength(4));
+            case 1 -> QueryBuilders.termsQuery(fieldName, randomArray(1, 3, String[]::new, () -> randomAlphaOfLengthBetween(3, 8)))
+                .boost(Math.abs(randomFloat()))
+                .queryName(randomAlphaOfLength(4));
             case 2 -> QueryBuilders.idsQuery().addIds(randomArray(1, 3, String[]::new, () -> randomAlphaOfLength(22)));
             case 3 -> QueryBuilders.prefixQuery(fieldName, "prod-");
             case 4 -> QueryBuilders.wildcardQuery(fieldName, "prod-*-east-*");
             case 5 -> QueryBuilders.matchAllQuery();
-            case 6 -> QueryBuilders.existsQuery(fieldName);
+            case 6 -> QueryBuilders.existsQuery(fieldName).boost(Math.abs(randomFloat())).queryName(randomAlphaOfLength(4));
             case 7 -> QueryBuilders.rangeQuery(fieldName)
                 .from(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli(), randomBoolean())
                 .to(Instant.now().toEpochMilli(), randomBoolean());
             case 8 -> QueryBuilders.simpleQueryStringQuery("+rest key*")
                 .field(fieldName)
                 .lenient(randomBoolean())
-                .analyzeWildcard(randomBoolean());
+                .analyzeWildcard(randomBoolean())
+                .fuzzyPrefixLength(randomIntBetween(1, 10))
+                .fuzzyMaxExpansions(randomIntBetween(1, 10))
+                .fuzzyTranspositions(randomBoolean());
             case 9 -> QueryBuilders.matchQuery(fieldName, randomAlphaOfLengthBetween(3, 8))
                 .operator(randomFrom(Operator.OR, Operator.AND))
                 .lenient(randomBoolean())