浏览代码

Refactor field name translator of query endpoints for security entities (#109559)

This is a refactoring of the internal logic that's used to translate
query-level into index-level field names for query APIs for
security entities (i.e. users, API Keys, and soon, roles).
The objective here is to have and reuse a single class to handle
all the translations for different security query APIs.
Albert Zaharovits 1 年之前
父节点
当前提交
0e4888bdec
共有 19 个文件被更改,包括 263 次插入357 次删除
  1. 1 1
      docs/reference/rest-api/security/query-api-key.asciidoc
  2. 4 1
      docs/reference/rest-api/security/query-user.asciidoc
  3. 4 7
      x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/ApiKeyAggsIT.java
  4. 1 1
      x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryApiKeyIT.java
  5. 6 6
      x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryUserIT.java
  6. 2 2
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyAction.java
  7. 2 44
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportQueryUserAction.java
  8. 1 1
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java
  9. 1 0
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestQueryApiKeyAction.java
  10. 0 1
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/NativeRoleBaseRestHandler.java
  11. 5 5
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyAggregationsBuilder.java
  12. 5 21
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilder.java
  13. 187 109
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FieldNameTranslators.java
  14. 0 72
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexFieldNameTranslator.java
  15. 14 57
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/UserBoolQueryBuilder.java
  16. 4 4
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyActionTests.java
  17. 11 6
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportQueryUserActionTests.java
  18. 13 13
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java
  19. 2 6
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/UserBoolQueryBuilderTests.java

+ 1 - 1
docs/reference/rest-api/security/query-api-key.asciidoc

@@ -177,7 +177,7 @@ The query supports a subset of query types, including
 <<query-dsl-match-query,`match`>>, <<query-dsl-ids-query,`ids`>>,
 <<query-dsl-prefix-query,`prefix`>>, <<query-dsl-wildcard-query,`wildcard`>>,
 <<query-dsl-exists-query,`exists`>>, <<query-dsl-range-query,`range`>>,
-and <<query-dsl-simple-query-string-query,`simple query string`>>
+and <<query-dsl-simple-query-string-query,`simple query string`>>.
 +
 You can query the following public values associated with an API key.
 +

+ 4 - 1
docs/reference/rest-api/security/query-user.asciidoc

@@ -40,7 +40,10 @@ You can specify the following parameters in the request body:
 The query supports a subset of query types, including
 <<query-dsl-match-all-query,`match_all`>>, <<query-dsl-bool-query,`bool`>>,
 <<query-dsl-term-query,`term`>>, <<query-dsl-terms-query,`terms`>>,
-<<query-dsl-prefix-query,`prefix`>>, <<query-dsl-wildcard-query,`wildcard`>> and <<query-dsl-exists-query,`exists`>>.
+<<query-dsl-match-query,`match`>>, <<query-dsl-ids-query,`ids`>>,
+<<query-dsl-prefix-query,`prefix`>>, <<query-dsl-wildcard-query,`wildcard`>>,
+<<query-dsl-exists-query,`exists`>>, <<query-dsl-range-query,`range`>>,
+and <<query-dsl-simple-query-string-query,`simple query string`>>.
 +
 You can query the following public values associated with a user.
 +

+ 4 - 7
x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/ApiKeyAggsIT.java

@@ -246,10 +246,7 @@ public class ApiKeyAggsIT extends SecurityInBasicRestTestCase {
                 """);
             ResponseException exception = expectThrows(ResponseException.class, () -> client().performRequest(request));
             assertThat(exception.getResponse().toString(), exception.getResponse().getStatusLine().getStatusCode(), is(400));
-            assertThat(
-                exception.getMessage(),
-                containsString("Field [api_key_invalidated] is not allowed for API Key query or aggregation")
-            );
+            assertThat(exception.getMessage(), containsString("Field [api_key_invalidated] is not allowed for querying or aggregation"));
         }
         {
             Request request = new Request("GET", "/_security/_query/api_key" + (randomBoolean() ? "?typed_keys" : ""));
@@ -282,7 +279,7 @@ public class ApiKeyAggsIT extends SecurityInBasicRestTestCase {
                 """);
             ResponseException exception = expectThrows(ResponseException.class, () -> client().performRequest(request));
             assertThat(exception.getResponse().toString(), exception.getResponse().getStatusLine().getStatusCode(), is(400));
-            assertThat(exception.getMessage(), containsString("Field [creator.realm] is not allowed for API Key query or aggregation"));
+            assertThat(exception.getMessage(), containsString("Field [creator.realm] is not allowed for querying or aggregation"));
         }
     }
 
@@ -418,7 +415,7 @@ public class ApiKeyAggsIT extends SecurityInBasicRestTestCase {
                 """);
             ResponseException exception = expectThrows(ResponseException.class, () -> client().performRequest(request));
             assertThat(exception.getResponse().toString(), exception.getResponse().getStatusLine().getStatusCode(), is(400));
-            assertThat(exception.getMessage(), containsString("Field [runtime_key_type] is not allowed for API Key query or aggregation"));
+            assertThat(exception.getMessage(), containsString("Field [runtime_key_type] is not allowed for querying or aggregation"));
         }
     }
 
@@ -549,7 +546,7 @@ public class ApiKeyAggsIT extends SecurityInBasicRestTestCase {
                 """);
             ResponseException exception = expectThrows(ResponseException.class, () -> client().performRequest(request));
             assertThat(exception.getResponse().toString(), exception.getResponse().getStatusLine().getStatusCode(), is(400));
-            assertThat(exception.getMessage(), containsString("Field [creator] is not allowed for API Key query or aggregation"));
+            assertThat(exception.getMessage(), containsString("Field [creator] is not allowed for querying or aggregation"));
         }
     }
 

+ 1 - 1
x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryApiKeyIT.java

@@ -138,7 +138,7 @@ public class QueryApiKeyIT extends SecurityInBasicRestTestCase {
         // Search for fields outside of the allowlist fails
         ResponseException responseException = assertQueryError(API_KEY_ADMIN_AUTH_HEADER, 400, """
             { "query": { "prefix": {"api_key_hash": "{PBKDF2}10000$"} } }""");
-        assertThat(responseException.getMessage(), containsString("Field [api_key_hash] is not allowed for API Key query"));
+        assertThat(responseException.getMessage(), containsString("Field [api_key_hash] is not allowed for querying"));
 
         // Search for fields that are not allowed in Query DSL but used internally by the service itself
         final String fieldName = randomFrom("doc_type", "api_key_invalidated", "invalidation_time");

+ 6 - 6
x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryUserIT.java

@@ -195,13 +195,13 @@ public class QueryUserIT extends SecurityInBasicRestTestCase {
         assertQueryError(TEST_USER_NO_READ_USERS_AUTH_HEADER, 403, """
             { "query": { "wildcard": {"name": "*prefix*"} } }""");
 
-        // Range query not supported
+        // Span term query not supported
         assertQueryError(400, """
-            {"query":{"range":{"username":{"lt":"now"}}}}""");
+            {"query":{"span_term":{"username": "X"} } }""");
 
-        // IDs query not supported
+        // Fuzzy query not supported
         assertQueryError(400, """
-            { "query": { "ids": { "values": "abc" } } }""");
+            { "query": { "fuzzy": { "username": "X" } } }""");
 
         // Make sure we can't query reserved users
         String reservedUsername = getReservedUsernameAndAssertExists();
@@ -323,8 +323,8 @@ public class QueryUserIT extends SecurityInBasicRestTestCase {
         assertQueryError(
             READ_USERS_USER_AUTH_HEADER,
             400,
-            String.format("{\"sort\":[\"%s\"]}", invalidSortName),
-            String.format("sorting is not supported for field [%s] in User query", invalidSortName)
+            Strings.format("{\"sort\":[\"%s\"]}", invalidSortName),
+            Strings.format("sorting is not supported for field [%s]", invalidSortName)
         );
     }
 

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

@@ -28,7 +28,7 @@ import org.elasticsearch.xpack.security.support.ApiKeyBoolQueryBuilder;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
 
-import static org.elasticsearch.xpack.security.support.ApiKeyFieldNameTranslators.translateFieldSortBuilders;
+import static org.elasticsearch.xpack.security.support.FieldNameTranslators.API_KEY_FIELD_NAME_TRANSLATORS;
 import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_MAIN_ALIAS;
 
 public final class TransportQueryApiKeyAction extends TransportAction<QueryApiKeyRequest, QueryApiKeyResponse> {
@@ -94,7 +94,7 @@ public final class TransportQueryApiKeyAction extends TransportAction<QueryApiKe
         }, filteringAuthentication));
 
         if (request.getFieldSortBuilders() != null) {
-            translateFieldSortBuilders(request.getFieldSortBuilders(), searchSourceBuilder, fieldName -> {
+            API_KEY_FIELD_NAME_TRANSLATORS.translateFieldSortBuilders(request.getFieldSortBuilders(), searchSourceBuilder, fieldName -> {
                 if (API_KEY_TYPE_RUNTIME_MAPPING_FIELD.equals(fieldName)) {
                     accessesApiKeyTypeField.set(true);
                 }

+ 2 - 44
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportQueryUserAction.java

@@ -16,7 +16,6 @@ import org.elasticsearch.common.inject.Inject;
 import org.elasticsearch.core.Tuple;
 import org.elasticsearch.rest.RestStatus;
 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.action.ActionTypes;
@@ -30,20 +29,17 @@ import org.elasticsearch.xpack.security.profile.ProfileService;
 import org.elasticsearch.xpack.security.support.UserBoolQueryBuilder;
 
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 import java.util.stream.Collectors;
 
+import static org.elasticsearch.xpack.security.support.FieldNameTranslators.USER_FIELD_NAME_TRANSLATORS;
 import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_MAIN_ALIAS;
-import static org.elasticsearch.xpack.security.support.UserBoolQueryBuilder.USER_FIELD_NAME_TRANSLATOR;
 
 public final class TransportQueryUserAction extends TransportAction<QueryUserRequest, QueryUserResponse> {
     private final NativeUsersStore usersStore;
     private final ProfileService profileService;
     private final Authentication.RealmRef nativeRealmRef;
-    private static final Set<String> FIELD_NAMES_WITH_SORT_SUPPORT = Set.of("username", "roles", "enabled");
 
     @Inject
     public TransportQueryUserAction(
@@ -76,7 +72,7 @@ public final class TransportQueryUserAction extends TransportAction<QueryUserReq
         searchSourceBuilder.query(UserBoolQueryBuilder.build(request.getQueryBuilder()));
 
         if (request.getFieldSortBuilders() != null) {
-            translateFieldSortBuilders(request.getFieldSortBuilders(), searchSourceBuilder);
+            USER_FIELD_NAME_TRANSLATORS.translateFieldSortBuilders(request.getFieldSortBuilders(), searchSourceBuilder, null);
         }
 
         if (request.getSearchAfterBuilder() != null) {
@@ -135,42 +131,4 @@ public final class TransportQueryUserAction extends TransportAction<QueryUserReq
             }
         }, listener::onFailure));
     }
-
-    // package private for testing
-    static void translateFieldSortBuilders(List<FieldSortBuilder> fieldSortBuilders, SearchSourceBuilder searchSourceBuilder) {
-        fieldSortBuilders.forEach(fieldSortBuilder -> {
-            if (fieldSortBuilder.getNestedSort() != null) {
-                throw new IllegalArgumentException("nested sorting is not supported for User query");
-            }
-            if (FieldSortBuilder.DOC_FIELD_NAME.equals(fieldSortBuilder.getFieldName())) {
-                searchSourceBuilder.sort(fieldSortBuilder);
-            } else {
-                final String translatedFieldName = USER_FIELD_NAME_TRANSLATOR.translate(fieldSortBuilder.getFieldName());
-                if (FIELD_NAMES_WITH_SORT_SUPPORT.contains(translatedFieldName) == false) {
-                    throw new IllegalArgumentException(
-                        String.format(Locale.ROOT, "sorting is not supported for field [%s] in User query", fieldSortBuilder.getFieldName())
-                    );
-                }
-
-                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.getNumericType() != null) {
-                        translatedFieldSortBuilder.setNumericType(fieldSortBuilder.getNumericType());
-                    }
-                    searchSourceBuilder.sort(translatedFieldSortBuilder);
-                }
-            }
-        });
-    }
 }

+ 1 - 1
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java

@@ -79,7 +79,7 @@ import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SEC
  */
 public class NativeUsersStore {
 
-    static final String USER_DOC_TYPE = "user";
+    public static final String USER_DOC_TYPE = "user";
     public static final String RESERVED_USER_TYPE = "reserved-user";
     private static final Logger logger = LogManager.getLogger(NativeUsersStore.class);
 

+ 1 - 0
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestQueryApiKeyAction.java

@@ -113,6 +113,7 @@ public final class RestQueryApiKeyAction extends ApiKeyBaseRestHandler {
 
     @Override
     protected Set<String> responseParams() {
+        // this is a parameter that's consumed by the response formatter for aggregations
         return Set.of(RestSearchAction.TYPED_KEYS_PARAM);
     }
 

+ 0 - 1
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/NativeRoleBaseRestHandler.java

@@ -43,6 +43,5 @@ abstract class NativeRoleBaseRestHandler extends SecurityBaseRestHandler {
         } else {
             return null;
         }
-
     }
 }

+ 5 - 5
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyAggregationsBuilder.java

@@ -27,7 +27,7 @@ import org.elasticsearch.search.aggregations.support.ValuesSourceAggregationBuil
 
 import java.util.function.Consumer;
 
-import static org.elasticsearch.xpack.security.support.ApiKeyFieldNameTranslators.translateQueryBuilderFields;
+import static org.elasticsearch.xpack.security.support.FieldNameTranslators.API_KEY_FIELD_NAME_TRANSLATORS;
 
 public class ApiKeyAggregationsBuilder {
 
@@ -73,7 +73,7 @@ public class ApiKeyAggregationsBuilder {
                     throw new IllegalArgumentException("Unsupported script value source for [" + copiedAggsBuilder.getName() + "] agg");
                 }
                 // the user-facing field names are different from the index mapping field names of API Key docs
-                String translatedFieldName = ApiKeyFieldNameTranslators.translate(valuesSourceAggregationBuilder.field());
+                String translatedFieldName = API_KEY_FIELD_NAME_TRANSLATORS.translate(valuesSourceAggregationBuilder.field());
                 valuesSourceAggregationBuilder.field(translatedFieldName);
                 fieldNameVisitor.accept(translatedFieldName);
                 return valuesSourceAggregationBuilder;
@@ -88,7 +88,7 @@ public class ApiKeyAggregationsBuilder {
                                 + "]"
                         );
                     }
-                    String translatedFieldName = ApiKeyFieldNameTranslators.translate(valueSource.field());
+                    String translatedFieldName = API_KEY_FIELD_NAME_TRANSLATORS.translate(valueSource.field());
                     valueSource.field(translatedFieldName);
                     fieldNameVisitor.accept(translatedFieldName);
                 }
@@ -97,7 +97,7 @@ public class ApiKeyAggregationsBuilder {
                 // filters the aggregation query to user's allowed API Keys only
                 FilterAggregationBuilder newFilterAggregationBuilder = new FilterAggregationBuilder(
                     filterAggregationBuilder.getName(),
-                    translateQueryBuilderFields(filterAggregationBuilder.getFilter(), fieldNameVisitor)
+                    API_KEY_FIELD_NAME_TRANSLATORS.translateQueryBuilderFields(filterAggregationBuilder.getFilter(), fieldNameVisitor)
                 );
                 if (filterAggregationBuilder.getMetadata() != null) {
                     newFilterAggregationBuilder.setMetadata(filterAggregationBuilder.getMetadata());
@@ -110,7 +110,7 @@ public class ApiKeyAggregationsBuilder {
                 // filters the aggregation's bucket queries to user's allowed API Keys only
                 QueryBuilder[] filterQueryBuilders = new QueryBuilder[filtersAggregationBuilder.filters().size()];
                 for (int i = 0; i < filtersAggregationBuilder.filters().size(); i++) {
-                    filterQueryBuilders[i] = translateQueryBuilderFields(
+                    filterQueryBuilders[i] = API_KEY_FIELD_NAME_TRANSLATORS.translateQueryBuilderFields(
                         filtersAggregationBuilder.filters().get(i).filter(),
                         fieldNameVisitor
                     );

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

@@ -22,26 +22,11 @@ import java.io.IOException;
 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;
+import static org.elasticsearch.xpack.security.support.FieldNameTranslators.API_KEY_FIELD_NAME_TRANSLATORS;
 
-public class ApiKeyBoolQueryBuilder extends BoolQueryBuilder {
+public final class ApiKeyBoolQueryBuilder extends BoolQueryBuilder {
 
-    // Field names allowed at the index level
-    private static final Set<String> ALLOWED_EXACT_INDEX_FIELD_NAMES = Set.of(
-        "_id",
-        "doc_type",
-        "name",
-        "type",
-        API_KEY_TYPE_RUNTIME_MAPPING_FIELD,
-        "api_key_invalidated",
-        "invalidation_time",
-        "creation_time",
-        "expiration_time",
-        "metadata_flattened",
-        "creator.principal",
-        "creator.realm"
-    );
+    private static final Set<String> FIELDS_ALLOWED_TO_QUERY = Set.of("_id", "doc_type", "type");
 
     private ApiKeyBoolQueryBuilder() {}
 
@@ -69,7 +54,7 @@ public class ApiKeyBoolQueryBuilder extends BoolQueryBuilder {
     ) {
         final ApiKeyBoolQueryBuilder finalQuery = new ApiKeyBoolQueryBuilder();
         if (queryBuilder != null) {
-            QueryBuilder processedQuery = translateQueryBuilderFields(queryBuilder, fieldNameVisitor);
+            QueryBuilder processedQuery = API_KEY_FIELD_NAME_TRANSLATORS.translateQueryBuilderFields(queryBuilder, fieldNameVisitor);
             finalQuery.must(processedQuery);
         }
         finalQuery.filter(QueryBuilders.termQuery("doc_type", "api_key"));
@@ -110,7 +95,6 @@ public class ApiKeyBoolQueryBuilder extends BoolQueryBuilder {
     }
 
     static boolean isIndexFieldNameAllowed(String fieldName) {
-        return ALLOWED_EXACT_INDEX_FIELD_NAMES.contains(fieldName) || fieldName.startsWith("metadata_flattened.");
+        return FIELDS_ALLOWED_TO_QUERY.contains(fieldName) || API_KEY_FIELD_NAME_TRANSLATORS.isIndexFieldSupported(fieldName);
     }
-
 }

+ 187 - 109
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyFieldNameTranslators.java → x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FieldNameTranslators.java

@@ -9,6 +9,7 @@ package org.elasticsearch.xpack.security.support;
 
 import org.elasticsearch.common.regex.Regex;
 import org.elasticsearch.core.Nullable;
+import org.elasticsearch.core.Strings;
 import org.elasticsearch.index.query.AbstractQueryBuilder;
 import org.elasticsearch.index.query.BoolQueryBuilder;
 import org.elasticsearch.index.query.ExistsQueryBuilder;
@@ -35,75 +36,42 @@ 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;
 
-/**
- * A class to translate query level field names to index level field names.
- */
-public class ApiKeyFieldNameTranslators {
-    static final List<FieldNameTranslator> FIELD_NAME_TRANSLATORS;
-
-    static {
-        FIELD_NAME_TRANSLATORS = List.of(
-            new ExactFieldNameTranslator(s -> "creator.principal", "username"),
-            new ExactFieldNameTranslator(s -> "creator.realm", "realm_name"),
-            new ExactFieldNameTranslator(s -> "name", "name"),
-            new ExactFieldNameTranslator(s -> API_KEY_TYPE_RUNTIME_MAPPING_FIELD, "type"),
-            new ExactFieldNameTranslator(s -> "creation_time", "creation"),
-            new ExactFieldNameTranslator(s -> "expiration_time", "expiration"),
-            new ExactFieldNameTranslator(s -> "api_key_invalidated", "invalidated"),
-            new ExactFieldNameTranslator(s -> "invalidation_time", "invalidation"),
-            // allows querying on all metadata values as keywords because "metadata_flattened" is a flattened field type
-            new ExactFieldNameTranslator(s -> "metadata_flattened", "metadata"),
-            new PrefixFieldNameTranslator(s -> "metadata_flattened." + s.substring("metadata.".length()), "metadata.")
-        );
-    }
+public final class FieldNameTranslators {
 
-    /**
-     * 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());
+    public static final FieldNameTranslators API_KEY_FIELD_NAME_TRANSLATORS = new FieldNameTranslators(
+        List.of(
+            new SimpleFieldNameTranslator("creator.principal", "username"),
+            new SimpleFieldNameTranslator("creator.realm", "realm_name"),
+            new SimpleFieldNameTranslator("name", "name"),
+            new SimpleFieldNameTranslator(API_KEY_TYPE_RUNTIME_MAPPING_FIELD, "type"),
+            new SimpleFieldNameTranslator("creation_time", "creation"),
+            new SimpleFieldNameTranslator("expiration_time", "expiration"),
+            new SimpleFieldNameTranslator("api_key_invalidated", "invalidated"),
+            new SimpleFieldNameTranslator("invalidation_time", "invalidation"),
+            // allows querying on any non-wildcard sub-fields under the "metadata." prefix
+            // also allows querying on the "metadata" field itself (including by specifying patterns)
+            new FlattenedFieldNameTranslator("metadata_flattened", "metadata")
+        )
+    );
 
-                    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);
-                }
-            }
-        });
+    public static final FieldNameTranslators USER_FIELD_NAME_TRANSLATORS = new FieldNameTranslators(
+        List.of(
+            idemFieldNameTranslator("username"),
+            idemFieldNameTranslator("roles"),
+            idemFieldNameTranslator("enabled"),
+            // the mapping for these fields does not support sorting (because their mapping does not store "fielddata" in the index)
+            idemFieldNameTranslator("full_name", false),
+            idemFieldNameTranslator("email", false)
+        )
+    );
+
+    private final List<FieldNameTranslator> fieldNameTranslators;
+
+    private FieldNameTranslators(List<FieldNameTranslator> fieldNameTranslators) {
+        this.fieldNameTranslators = fieldNameTranslators;
     }
 
     /**
@@ -114,7 +82,7 @@ public class ApiKeyFieldNameTranslators {
      * 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) {
+    public 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) {
@@ -147,7 +115,7 @@ public class ApiKeyFieldNameTranslators {
             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");
+                throw new IllegalArgumentException("terms query with terms lookup is not currently supported in this context");
             }
             final String translatedFieldName = translate(query.fieldName());
             fieldNameVisitor.accept(translatedFieldName);
@@ -200,7 +168,7 @@ public class ApiKeyFieldNameTranslators {
             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");
+                throw new IllegalArgumentException("range query with relation is not currently supported in this context");
             }
             final String translatedFieldName = translate(query.fieldName());
             fieldNameVisitor.accept(translatedFieldName);
@@ -264,35 +232,91 @@ public class ApiKeyFieldNameTranslators {
                 .boost(query.boost())
                 .queryName(query.queryName());
         } else {
-            throw new IllegalArgumentException("Query type [" + queryBuilder.getName() + "] is not supported for API Key query");
+            throw new IllegalArgumentException("Query type [" + queryBuilder.getName() + "] is not currently supported in this context");
         }
     }
 
+    /**
+     * 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 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 currently supported in this context");
+            }
+            if (FieldSortBuilder.DOC_FIELD_NAME.equals(fieldSortBuilder.getFieldName())) {
+                searchSourceBuilder.sort(fieldSortBuilder);
+            } else {
+                final String translatedFieldName = translate(fieldSortBuilder.getFieldName(), true);
+                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);
+                }
+            }
+        });
+    }
+
     /**
      * Translate the query level field name to index level field names.
      * It throws an exception if the field name is not explicitly allowed.
      */
-    protected static String translate(String fieldName) {
+    public String translate(String queryFieldName) {
+        return translate(queryFieldName, false);
+    }
+
+    /**
+     * Translate the query level field name to index level field names.
+     * It throws an exception if the field name is not explicitly allowed.
+     */
+    private String translate(String queryFieldName, boolean inSortContext) {
         // protected for testing
-        if (Regex.isSimpleMatchPattern(fieldName)) {
-            throw new IllegalArgumentException("Field name pattern [" + fieldName + "] is not allowed for API Key query or aggregation");
+        if (Regex.isSimpleMatchPattern(queryFieldName)) {
+            throw new IllegalArgumentException("Field name pattern [" + queryFieldName + "] is not allowed for querying or aggregation");
         }
-        for (FieldNameTranslator translator : FIELD_NAME_TRANSLATORS) {
-            if (translator.supports(fieldName)) {
-                return translator.translate(fieldName);
+        for (FieldNameTranslator translator : fieldNameTranslators) {
+            if (translator.isQueryFieldSupported(queryFieldName)) {
+                if (inSortContext && translator.isSortSupported() == false) {
+                    throw new IllegalArgumentException(Strings.format("sorting is not supported for field [%s]", queryFieldName));
+                }
+                return translator.translate(queryFieldName);
             }
         }
-        throw new IllegalArgumentException("Field [" + fieldName + "] is not allowed for API Key query or aggregation");
+        throw new IllegalArgumentException("Field [" + queryFieldName + "] is not allowed for querying 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.
      */
-    private static Set<String> translatePattern(String fieldNameOrPattern) {
+    public Set<String> translatePattern(String fieldNameOrPattern) {
         Set<String> indexFieldNames = new HashSet<>();
-        for (FieldNameTranslator translator : FIELD_NAME_TRANSLATORS) {
-            if (translator.supports(fieldNameOrPattern)) {
+        for (FieldNameTranslator translator : fieldNameTranslators) {
+            if (translator.isQueryFieldSupported(fieldNameOrPattern)) {
                 indexFieldNames.add(translator.translate(fieldNameOrPattern));
             }
         }
@@ -302,58 +326,112 @@ public class ApiKeyFieldNameTranslators {
         return indexFieldNames;
     }
 
-    abstract static class FieldNameTranslator {
+    public boolean isQueryFieldSupported(String fieldName) {
+        return fieldNameTranslators.stream().anyMatch(t -> t.isQueryFieldSupported(fieldName));
+    }
+
+    public boolean isIndexFieldSupported(String fieldName) {
+        return fieldNameTranslators.stream().anyMatch(t -> t.isIndexFieldSupported(fieldName));
+    }
 
-        private final Function<String, String> translationFunc;
+    private interface FieldNameTranslator {
+        String translate(String fieldName);
 
-        protected FieldNameTranslator(Function<String, String> translationFunc) {
-            this.translationFunc = translationFunc;
-        }
+        boolean isQueryFieldSupported(String fieldName);
 
-        String translate(String fieldName) {
-            return translationFunc.apply(fieldName);
-        }
+        boolean isIndexFieldSupported(String fieldName);
+
+        boolean isSortSupported();
+    }
+
+    private static SimpleFieldNameTranslator idemFieldNameTranslator(String fieldName) {
+        return new SimpleFieldNameTranslator(fieldName, fieldName);
+    }
 
-        abstract boolean supports(String fieldName);
+    private static SimpleFieldNameTranslator idemFieldNameTranslator(String fieldName, boolean isSortSupported) {
+        return new SimpleFieldNameTranslator(fieldName, fieldName, isSortSupported);
     }
 
-    static class ExactFieldNameTranslator extends FieldNameTranslator {
-        private final String name;
+    private static class SimpleFieldNameTranslator implements FieldNameTranslator {
+        private final String indexFieldName;
+        private final String queryFieldName;
+        private final boolean isSortSupported;
+
+        SimpleFieldNameTranslator(String indexFieldName, String queryFieldName, boolean isSortSupported) {
+            this.indexFieldName = indexFieldName;
+            this.queryFieldName = queryFieldName;
+            this.isSortSupported = isSortSupported;
+        }
 
-        ExactFieldNameTranslator(Function<String, String> translationFunc, String name) {
-            super(translationFunc);
-            this.name = name;
+        SimpleFieldNameTranslator(String indexFieldName, String queryFieldName) {
+            this(indexFieldName, queryFieldName, true);
         }
 
         @Override
-        public boolean supports(String fieldNameOrPattern) {
+        public boolean isQueryFieldSupported(String fieldNameOrPattern) {
             if (Regex.isSimpleMatchPattern(fieldNameOrPattern)) {
-                return Regex.simpleMatch(fieldNameOrPattern, name);
+                return Regex.simpleMatch(fieldNameOrPattern, queryFieldName);
             } else {
-                return name.equals(fieldNameOrPattern);
+                return queryFieldName.equals(fieldNameOrPattern);
             }
         }
+
+        @Override
+        public boolean isIndexFieldSupported(String fieldName) {
+            return fieldName.equals(indexFieldName);
+        }
+
+        @Override
+        public String translate(String fieldNameOrPattern) {
+            return indexFieldName;
+        }
+
+        @Override
+        public boolean isSortSupported() {
+            return isSortSupported;
+        }
     }
 
-    static class PrefixFieldNameTranslator extends FieldNameTranslator {
-        private final String prefix;
+    private static class FlattenedFieldNameTranslator implements FieldNameTranslator {
+        private final String indexFieldName;
+        private final String queryFieldName;
 
-        PrefixFieldNameTranslator(Function<String, String> translationFunc, String prefix) {
-            super(translationFunc);
-            this.prefix = prefix;
+        FlattenedFieldNameTranslator(String indexFieldName, String queryFieldName) {
+            this.indexFieldName = indexFieldName;
+            this.queryFieldName = queryFieldName;
         }
 
         @Override
-        boolean supports(String fieldNamePrefix) {
-            // a pattern can generally match a prefix in multiple ways
-            // moreover, it's not possible to iterate the concrete fields matching the prefix
-            if (Regex.isSimpleMatchPattern(fieldNamePrefix)) {
-                // this means that e.g. `metadata.*` and `metadata.x*` are expanded to the empty list,
-                // rather than be replaced with `metadata_flattened.*` and `metadata_flattened.x*`
-                // (but, in any case, `metadata_flattened.*` and `metadata.x*` are going to be ignored)
-                return false;
+        public boolean isQueryFieldSupported(String fieldNameOrPattern) {
+            if (Regex.isSimpleMatchPattern(fieldNameOrPattern)) {
+                // It is not possible to translate a pattern for subfields of a flattened field
+                // (because there's no list of subfields of the flattened field).
+                // But the pattern can still match the flattened field itself.
+                return Regex.simpleMatch(fieldNameOrPattern, queryFieldName);
+            } else {
+                return fieldNameOrPattern.equals(queryFieldName) || fieldNameOrPattern.startsWith(queryFieldName + ".");
             }
-            return fieldNamePrefix.startsWith(prefix);
+        }
+
+        @Override
+        public boolean isIndexFieldSupported(String fieldName) {
+            return fieldName.equals(indexFieldName) || fieldName.startsWith(indexFieldName + ".");
+        }
+
+        @Override
+        public String translate(String fieldNameOrPattern) {
+            if (Regex.isSimpleMatchPattern(fieldNameOrPattern) || fieldNameOrPattern.equals(queryFieldName)) {
+                // the pattern can only refer to the flattened field itself, not to its subfields
+                return indexFieldName;
+            } else {
+                assert fieldNameOrPattern.startsWith(queryFieldName + ".");
+                return indexFieldName + fieldNameOrPattern.substring(queryFieldName.length());
+            }
+        }
+
+        @Override
+        public boolean isSortSupported() {
+            return true;
         }
     }
 }

+ 0 - 72
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexFieldNameTranslator.java

@@ -1,72 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-package org.elasticsearch.xpack.security.support;
-
-import java.util.List;
-import java.util.function.Function;
-import java.util.function.Predicate;
-
-public class SecurityIndexFieldNameTranslator {
-
-    private final List<SecurityIndexFieldNameTranslator.FieldName> fieldNameTranslators;
-
-    public SecurityIndexFieldNameTranslator(List<SecurityIndexFieldNameTranslator.FieldName> fieldNameTranslators) {
-        this.fieldNameTranslators = fieldNameTranslators;
-    }
-
-    public String translate(String queryFieldName) {
-        for (FieldName fieldName : this.fieldNameTranslators) {
-            if (fieldName.supportsQueryName(queryFieldName)) {
-                return fieldName.indexFieldName(queryFieldName);
-            }
-        }
-        throw new IllegalArgumentException("Field [" + queryFieldName + "] is not allowed");
-    }
-
-    public boolean supportedIndexFieldName(String indexFieldName) {
-        for (FieldName fieldName : this.fieldNameTranslators) {
-            if (fieldName.supportsIndexName(indexFieldName)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    public static FieldName exact(String name) {
-        return exact(name, Function.identity());
-    }
-
-    public static FieldName exact(String name, Function<String, String> translation) {
-        return new SecurityIndexFieldNameTranslator.FieldName(name, translation);
-    }
-
-    public static class FieldName {
-        private final String name;
-        private final Function<String, String> toIndexFieldName;
-        protected final Predicate<String> validIndexNamePredicate;
-
-        private FieldName(String name, Function<String, String> toIndexFieldName) {
-            this.name = name;
-            this.toIndexFieldName = toIndexFieldName;
-            this.validIndexNamePredicate = fieldName -> toIndexFieldName.apply(name).equals(fieldName);
-
-        }
-
-        public boolean supportsQueryName(String queryFieldName) {
-            return queryFieldName.equals(name);
-        }
-
-        public boolean supportsIndexName(String indexFieldName) {
-            return validIndexNamePredicate.test(indexFieldName);
-        }
-
-        public String indexFieldName(String queryFieldName) {
-            return toIndexFieldName.apply(queryFieldName);
-        }
-    }
-}

+ 14 - 57
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/UserBoolQueryBuilder.java

@@ -9,75 +9,33 @@ package org.elasticsearch.xpack.security.support;
 
 import org.apache.lucene.search.Query;
 import org.elasticsearch.index.query.BoolQueryBuilder;
-import org.elasticsearch.index.query.ExistsQueryBuilder;
-import org.elasticsearch.index.query.MatchAllQueryBuilder;
-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.SearchExecutionContext;
-import org.elasticsearch.index.query.TermQueryBuilder;
-import org.elasticsearch.index.query.TermsQueryBuilder;
-import org.elasticsearch.index.query.WildcardQueryBuilder;
+import org.elasticsearch.xpack.core.security.user.User;
+import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore;
 
 import java.io.IOException;
-import java.util.List;
+import java.util.Set;
 
-import static org.elasticsearch.xpack.security.support.SecurityIndexFieldNameTranslator.exact;
+import static org.elasticsearch.xpack.security.support.FieldNameTranslators.USER_FIELD_NAME_TRANSLATORS;
 
-public class UserBoolQueryBuilder extends BoolQueryBuilder {
-    public static final SecurityIndexFieldNameTranslator USER_FIELD_NAME_TRANSLATOR = new SecurityIndexFieldNameTranslator(
-        List.of(exact("username"), exact("roles"), exact("full_name"), exact("email"), exact("enabled"))
-    );
+public final class UserBoolQueryBuilder extends BoolQueryBuilder {
+
+    private static final Set<String> FIELDS_ALLOWED_TO_QUERY = Set.of("_id", User.Fields.TYPE.getPreferredName());
 
     private UserBoolQueryBuilder() {}
 
     public static UserBoolQueryBuilder build(QueryBuilder queryBuilder) {
-        UserBoolQueryBuilder userQueryBuilder = new UserBoolQueryBuilder();
+        final UserBoolQueryBuilder finalQuery = new UserBoolQueryBuilder();
         if (queryBuilder != null) {
-            QueryBuilder translaterdQueryBuilder = translateToUserQueryBuilder(queryBuilder);
-            userQueryBuilder.must(translaterdQueryBuilder);
+            QueryBuilder processedQuery = USER_FIELD_NAME_TRANSLATORS.translateQueryBuilderFields(queryBuilder, null);
+            finalQuery.must(processedQuery);
         }
-        userQueryBuilder.filter(QueryBuilders.termQuery("type", "user"));
-
-        return userQueryBuilder;
-    }
+        finalQuery.filter(QueryBuilders.termQuery(User.Fields.TYPE.getPreferredName(), NativeUsersStore.USER_DOC_TYPE));
 
-    private static QueryBuilder translateToUserQueryBuilder(QueryBuilder qb) {
-        if (qb instanceof final BoolQueryBuilder query) {
-            final BoolQueryBuilder newQuery = QueryBuilders.boolQuery()
-                .minimumShouldMatch(query.minimumShouldMatch())
-                .adjustPureNegative(query.adjustPureNegative());
-            query.must().stream().map(UserBoolQueryBuilder::translateToUserQueryBuilder).forEach(newQuery::must);
-            query.should().stream().map(UserBoolQueryBuilder::translateToUserQueryBuilder).forEach(newQuery::should);
-            query.mustNot().stream().map(UserBoolQueryBuilder::translateToUserQueryBuilder).forEach(newQuery::mustNot);
-            query.filter().stream().map(UserBoolQueryBuilder::translateToUserQueryBuilder).forEach(newQuery::filter);
-            return newQuery;
-        } else if (qb instanceof MatchAllQueryBuilder) {
-            return qb;
-        } else if (qb instanceof final TermQueryBuilder query) {
-            final String translatedFieldName = USER_FIELD_NAME_TRANSLATOR.translate(query.fieldName());
-            return QueryBuilders.termQuery(translatedFieldName, query.value()).caseInsensitive(query.caseInsensitive());
-        } else if (qb instanceof final ExistsQueryBuilder query) {
-            final String translatedFieldName = USER_FIELD_NAME_TRANSLATOR.translate(query.fieldName());
-            return QueryBuilders.existsQuery(translatedFieldName);
-        } else if (qb instanceof final TermsQueryBuilder query) {
-            if (query.termsLookup() != null) {
-                throw new IllegalArgumentException("Terms query with terms lookup is not supported for User query");
-            }
-            final String translatedFieldName = USER_FIELD_NAME_TRANSLATOR.translate(query.fieldName());
-            return QueryBuilders.termsQuery(translatedFieldName, query.getValues());
-        } else if (qb instanceof final PrefixQueryBuilder query) {
-            final String translatedFieldName = USER_FIELD_NAME_TRANSLATOR.translate(query.fieldName());
-            return QueryBuilders.prefixQuery(translatedFieldName, query.value()).caseInsensitive(query.caseInsensitive());
-        } else if (qb instanceof final WildcardQueryBuilder query) {
-            final String translatedFieldName = USER_FIELD_NAME_TRANSLATOR.translate(query.fieldName());
-            return QueryBuilders.wildcardQuery(translatedFieldName, query.value())
-                .caseInsensitive(query.caseInsensitive())
-                .rewrite(query.rewrite());
-        } else {
-            throw new IllegalArgumentException("Query type [" + qb.getName() + "] is not supported for User query");
-        }
+        return finalQuery;
     }
 
     @Override
@@ -94,8 +52,7 @@ public class UserBoolQueryBuilder extends BoolQueryBuilder {
         return super.doRewrite(queryRewriteContext);
     }
 
-    boolean isIndexFieldNameAllowed(String queryFieldName) {
-        // Type is needed to filter on user doc type
-        return queryFieldName.equals("type") || USER_FIELD_NAME_TRANSLATOR.supportedIndexFieldName(queryFieldName);
+    boolean isIndexFieldNameAllowed(String fieldName) {
+        return FIELDS_ALLOWED_TO_QUERY.contains(fieldName) || USER_FIELD_NAME_TRANSLATORS.isIndexFieldSupported(fieldName);
     }
 }

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

@@ -13,13 +13,13 @@ 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;
 import java.util.Set;
 import java.util.stream.IntStream;
 
+import static org.elasticsearch.xpack.security.support.FieldNameTranslators.API_KEY_FIELD_NAME_TRANSLATORS;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.equalTo;
 
@@ -43,7 +43,7 @@ public class TransportQueryApiKeyActionTests extends ESTestCase {
 
         List<String> sortFields = new ArrayList<>();
         final SearchSourceBuilder searchSourceBuilder = SearchSourceBuilder.searchSource();
-        ApiKeyFieldNameTranslators.translateFieldSortBuilders(originals, searchSourceBuilder, sortFields::add);
+        API_KEY_FIELD_NAME_TRANSLATORS.translateFieldSortBuilders(originals, searchSourceBuilder, sortFields::add);
 
         IntStream.range(0, originals.size()).forEach(i -> {
             final FieldSortBuilder original = originals.get(i);
@@ -96,13 +96,13 @@ public class TransportQueryApiKeyActionTests extends ESTestCase {
         fieldSortBuilder.setNestedSort(new NestedSortBuilder("name"));
         final IllegalArgumentException e = expectThrows(
             IllegalArgumentException.class,
-            () -> ApiKeyFieldNameTranslators.translateFieldSortBuilders(
+            () -> API_KEY_FIELD_NAME_TRANSLATORS.translateFieldSortBuilders(
                 List.of(fieldSortBuilder),
                 SearchSourceBuilder.searchSource(),
                 ignored -> {}
             )
         );
-        assertThat(e.getMessage(), equalTo("nested sorting is not supported for API Key query"));
+        assertThat(e.getMessage(), equalTo("nested sorting is not currently supported in this context"));
     }
 
     private FieldSortBuilder randomFieldSortBuilderWithName(String name) {

+ 11 - 6
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportQueryUserActionTests.java

@@ -13,6 +13,7 @@ import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.search.SearchRequest;
 import org.elasticsearch.action.support.ActionFilters;
 import org.elasticsearch.action.support.PlainActionFuture;
+import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.Tuple;
 import org.elasticsearch.rest.RestStatus;
@@ -43,13 +44,13 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 
 import static org.elasticsearch.test.ActionListenerUtils.anyActionListener;
+import static org.elasticsearch.xpack.security.support.FieldNameTranslators.USER_FIELD_NAME_TRANSLATORS;
 import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.greaterThan;
@@ -72,7 +73,7 @@ public class TransportQueryUserActionTests extends ESTestCase {
         final List<FieldSortBuilder> originals = fieldNames.stream().map(this::randomFieldSortBuilderWithName).toList();
 
         final SearchSourceBuilder searchSourceBuilder = SearchSourceBuilder.searchSource();
-        TransportQueryUserAction.translateFieldSortBuilders(originals, searchSourceBuilder);
+        USER_FIELD_NAME_TRANSLATORS.translateFieldSortBuilders(originals, searchSourceBuilder, null);
 
         IntStream.range(0, originals.size()).forEach(i -> {
             final FieldSortBuilder original = originals.get(i);
@@ -93,9 +94,13 @@ public class TransportQueryUserActionTests extends ESTestCase {
         fieldSortBuilder.setNestedSort(new NestedSortBuilder("something"));
         final IllegalArgumentException e = expectThrows(
             IllegalArgumentException.class,
-            () -> TransportQueryUserAction.translateFieldSortBuilders(List.of(fieldSortBuilder), SearchSourceBuilder.searchSource())
+            () -> USER_FIELD_NAME_TRANSLATORS.translateFieldSortBuilders(
+                List.of(fieldSortBuilder),
+                SearchSourceBuilder.searchSource(),
+                null
+            )
         );
-        assertThat(e.getMessage(), equalTo("nested sorting is not supported for User query"));
+        assertThat(e.getMessage(), equalTo("nested sorting is not currently supported in this context"));
     }
 
     public void testNestedSortingOnTextFieldsNotAllowed() {
@@ -106,9 +111,9 @@ public class TransportQueryUserActionTests extends ESTestCase {
 
         final IllegalArgumentException e = expectThrows(
             IllegalArgumentException.class,
-            () -> TransportQueryUserAction.translateFieldSortBuilders(originals, searchSourceBuilder)
+            () -> USER_FIELD_NAME_TRANSLATORS.translateFieldSortBuilders(originals, searchSourceBuilder, null)
         );
-        assertThat(e.getMessage(), equalTo(String.format(Locale.ROOT, "sorting is not supported for field [%s] in User query", fieldName)));
+        assertThat(e.getMessage(), equalTo(Strings.format("sorting is not supported for field [%s]", fieldName)));
     }
 
     public void testQueryUsers() {

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

@@ -51,7 +51,7 @@ import java.util.function.Predicate;
 
 import static org.elasticsearch.test.LambdaMatchers.falseWith;
 import static org.elasticsearch.test.LambdaMatchers.trueWith;
-import static org.elasticsearch.xpack.security.support.ApiKeyFieldNameTranslators.FIELD_NAME_TRANSLATORS;
+import static org.elasticsearch.xpack.security.support.FieldNameTranslators.API_KEY_FIELD_NAME_TRANSLATORS;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.empty;
@@ -133,12 +133,12 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
         }
         List<String> queryFields = new ArrayList<>();
         ApiKeyBoolQueryBuilder apiKeyMatchQueryBuilder = ApiKeyBoolQueryBuilder.build(prefixQueryBuilder, queryFields::add, authentication);
-        assertThat(queryFields, hasItem(ApiKeyFieldNameTranslators.translate(fieldName)));
+        assertThat(queryFields, hasItem(API_KEY_FIELD_NAME_TRANSLATORS.translate(fieldName)));
         List<QueryBuilder> mustQueries = apiKeyMatchQueryBuilder.must();
         assertThat(mustQueries, hasSize(1));
         assertThat(mustQueries.get(0), instanceOf(PrefixQueryBuilder.class));
         PrefixQueryBuilder prefixQueryBuilder2 = (PrefixQueryBuilder) mustQueries.get(0);
-        assertThat(prefixQueryBuilder2.fieldName(), is(ApiKeyFieldNameTranslators.translate(prefixQueryBuilder.fieldName())));
+        assertThat(prefixQueryBuilder2.fieldName(), is(API_KEY_FIELD_NAME_TRANSLATORS.translate(prefixQueryBuilder.fieldName())));
         assertThat(prefixQueryBuilder2.value(), is(prefixQueryBuilder.value()));
         assertThat(prefixQueryBuilder2.boost(), is(prefixQueryBuilder.boost()));
         assertThat(prefixQueryBuilder2.queryName(), is(prefixQueryBuilder.queryName()));
@@ -267,7 +267,7 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
         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())),
+                simpleQueryStringBuilder2.fields().get(API_KEY_FIELD_NAME_TRANSLATORS.translate(fieldEntry.getKey())),
                 is(fieldEntry.getValue())
             );
         }
@@ -341,12 +341,12 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
         }
         List<String> queryFields = new ArrayList<>();
         ApiKeyBoolQueryBuilder apiKeyMatchQueryBuilder = ApiKeyBoolQueryBuilder.build(matchQueryBuilder, queryFields::add, authentication);
-        assertThat(queryFields, hasItem(ApiKeyFieldNameTranslators.translate(fieldName)));
+        assertThat(queryFields, hasItem(API_KEY_FIELD_NAME_TRANSLATORS.translate(fieldName)));
         List<QueryBuilder> mustQueries = apiKeyMatchQueryBuilder.must();
         assertThat(mustQueries, hasSize(1));
         assertThat(mustQueries.get(0), instanceOf(MatchQueryBuilder.class));
         MatchQueryBuilder matchQueryBuilder2 = (MatchQueryBuilder) mustQueries.get(0);
-        assertThat(matchQueryBuilder2.fieldName(), is(ApiKeyFieldNameTranslators.translate(matchQueryBuilder.fieldName())));
+        assertThat(matchQueryBuilder2.fieldName(), is(API_KEY_FIELD_NAME_TRANSLATORS.translate(matchQueryBuilder.fieldName())));
         assertThat(matchQueryBuilder2.value(), is(matchQueryBuilder.value()));
         assertThat(matchQueryBuilder2.operator(), is(matchQueryBuilder.operator()));
         assertThat(matchQueryBuilder2.analyzer(), is(matchQueryBuilder.analyzer()));
@@ -612,7 +612,7 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
         final Authentication authentication = randomBoolean() ? AuthenticationTests.randomAuthentication(null, null) : null;
 
         final String randomFieldName = randomValueOtherThanMany(
-            s -> FIELD_NAME_TRANSLATORS.stream().anyMatch(t -> t.supports(s)),
+            API_KEY_FIELD_NAME_TRANSLATORS::isQueryFieldSupported,
             () -> randomAlphaOfLengthBetween(3, 20)
         );
         final String fieldName = randomFrom(
@@ -638,7 +638,7 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
                 IllegalArgumentException.class,
                 () -> ApiKeyBoolQueryBuilder.build(q1, ignored -> {}, authentication)
             );
-            assertThat(e1.getMessage(), containsString("Field [" + fieldName + "] is not allowed for API Key query"));
+            assertThat(e1.getMessage(), containsString("Field [" + fieldName + "] is not allowed for querying"));
         }
 
         // also wrapped in a boolean query
@@ -667,7 +667,7 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
                 IllegalArgumentException.class,
                 () -> ApiKeyBoolQueryBuilder.build(q2, ignored -> {}, authentication)
             );
-            assertThat(e2.getMessage(), containsString("Field [" + fieldName + "] is not allowed for API Key query"));
+            assertThat(e2.getMessage(), containsString("Field [" + fieldName + "] is not allowed for querying"));
         }
     }
 
@@ -678,7 +678,7 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
             IllegalArgumentException.class,
             () -> ApiKeyBoolQueryBuilder.build(q1, ignored -> {}, authentication)
         );
-        assertThat(e1.getMessage(), containsString("terms query with terms lookup is not supported for API Key query"));
+        assertThat(e1.getMessage(), containsString("terms query with terms lookup is not currently supported in this context"));
     }
 
     public void testRangeQueryWithRelationIsNotAllowed() {
@@ -688,7 +688,7 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
             IllegalArgumentException.class,
             () -> ApiKeyBoolQueryBuilder.build(q1, ignored -> {}, authentication)
         );
-        assertThat(e1.getMessage(), containsString("range query with relation is not supported for API Key query"));
+        assertThat(e1.getMessage(), containsString("range query with relation is not currently supported in this context"));
     }
 
     public void testDisallowedQueryTypes() {
@@ -734,7 +734,7 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
             IllegalArgumentException.class,
             () -> ApiKeyBoolQueryBuilder.build(q1, ignored -> {}, authentication)
         );
-        assertThat(e1.getMessage(), containsString("Query type [" + q1.getName() + "] is not supported for API Key query"));
+        assertThat(e1.getMessage(), containsString("Query type [" + q1.getName() + "] is not currently supported in this context"));
 
         // also wrapped in a boolean query
         {
@@ -756,7 +756,7 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
                 IllegalArgumentException.class,
                 () -> ApiKeyBoolQueryBuilder.build(q2, ignored -> {}, authentication)
             );
-            assertThat(e2.getMessage(), containsString("Query type [" + q1.getName() + "] is not supported for API Key query"));
+            assertThat(e2.getMessage(), containsString("Query type [" + q1.getName() + "] is not currently supported in this context"));
         }
     }
 

+ 2 - 6
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/UserBoolQueryBuilderTests.java

@@ -110,18 +110,14 @@ public class UserBoolQueryBuilderTests extends ESTestCase {
     public void testTermsLookupIsNotAllowed() {
         final TermsQueryBuilder q1 = QueryBuilders.termsLookupQuery("roles", new TermsLookup("lookup", "1", "id"));
         final IllegalArgumentException e1 = expectThrows(IllegalArgumentException.class, () -> UserBoolQueryBuilder.build(q1));
-        assertThat(e1.getMessage(), containsString("Terms query with terms lookup is not supported for User query"));
+        assertThat(e1.getMessage(), containsString("terms query with terms lookup is not currently supported in this context"));
     }
 
     public void testDisallowedQueryTypes() {
         final AbstractQueryBuilder<? extends AbstractQueryBuilder<?>> q1 = randomFrom(
-            QueryBuilders.idsQuery(),
-            QueryBuilders.rangeQuery(randomAlphaOfLength(5)),
-            QueryBuilders.matchQuery(randomAlphaOfLength(5), randomAlphaOfLength(5)),
             QueryBuilders.constantScoreQuery(mock(QueryBuilder.class)),
             QueryBuilders.boostingQuery(mock(QueryBuilder.class), mock(QueryBuilder.class)),
             QueryBuilders.queryStringQuery("q=a:42"),
-            QueryBuilders.simpleQueryStringQuery(randomAlphaOfLength(5)),
             QueryBuilders.combinedFieldsQuery(randomAlphaOfLength(5)),
             QueryBuilders.disMaxQuery(),
             QueryBuilders.distanceFeatureQuery(
@@ -155,7 +151,7 @@ public class UserBoolQueryBuilderTests extends ESTestCase {
         );
 
         final IllegalArgumentException e1 = expectThrows(IllegalArgumentException.class, () -> UserBoolQueryBuilder.build(q1));
-        assertThat(e1.getMessage(), containsString("Query type [" + q1.getName() + "] is not supported for User query"));
+        assertThat(e1.getMessage(), containsString("Query type [" + q1.getName() + "] is not currently supported in this context"));
     }
 
     public void testWillSetAllowedFields() {