diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyAction.java index 8abc307ab982d..1454b9e480a39 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyAction.java @@ -28,7 +28,7 @@ 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 { @@ -94,7 +94,7 @@ protected void doExecute(Task task, QueryApiKeyRequest request, ActionListener { + API_KEY_FIELD_NAME_TRANSLATORS.translateFieldSortBuilders(request.getFieldSortBuilders(), searchSourceBuilder, fieldName -> { if (API_KEY_TYPE_RUNTIME_MAPPING_FIELD.equals(fieldName)) { accessesApiKeyTypeField.set(true); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/role/TransportQueryRoleAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/role/TransportQueryRoleAction.java index 9b81be06f9906..33c375f1b4e00 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/role/TransportQueryRoleAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/role/TransportQueryRoleAction.java @@ -18,6 +18,9 @@ import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyResponse; import org.elasticsearch.xpack.core.security.action.role.QueryRoleAction; import org.elasticsearch.xpack.security.authz.store.NativeRolesStore; +import org.elasticsearch.xpack.security.support.RoleBoolQueryBuilder; + +import static org.elasticsearch.xpack.security.support.FieldNameTranslators.ROLE_FIELD_NAME_TRANSLATORS; public class TransportQueryRoleAction extends TransportAction { @@ -41,18 +44,9 @@ protected void doExecute(Task task, QueryApiKeyRequest request, ActionListener { - // if (API_KEY_TYPE_RUNTIME_MAPPING_FIELD.equals(fieldName)) { - // accessesApiKeyTypeField.set(true); - // } - // }, filteringAuthentication)); - // - // if (request.getFieldSortBuilders() != null) { - // translateFieldSortBuilders(request.getFieldSortBuilders(), searchSourceBuilder, fieldName -> { - // if (API_KEY_TYPE_RUNTIME_MAPPING_FIELD.equals(fieldName)) { - // accessesApiKeyTypeField.set(true); - // } - // }); - // } + searchSourceBuilder.query(RoleBoolQueryBuilder.build(request.getQueryBuilder(), null)); + if (request.getFieldSortBuilders() != null) { + ROLE_FIELD_NAME_TRANSLATORS.translateFieldSortBuilders(request.getFieldSortBuilders(), searchSourceBuilder, null); + } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyAggregationsBuilder.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyAggregationsBuilder.java index 495ad1591b6da..3ada85c2129e4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyAggregationsBuilder.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyAggregationsBuilder.java @@ -27,7 +27,7 @@ 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 @@ private static AggregationBuilder translateAggsFields(AggregationBuilder aggsBui 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 @@ private static AggregationBuilder translateAggsFields(AggregationBuilder aggsBui + "]" ); } - 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 @@ private static AggregationBuilder translateAggsFields(AggregationBuilder aggsBui // 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 @@ private static AggregationBuilder translateAggsFields(AggregationBuilder aggsBui // 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 ); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilder.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilder.java index 8d167954b399a..487ae35085a1c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilder.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilder.java @@ -23,7 +23,7 @@ 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 { @@ -69,7 +69,7 @@ public static ApiKeyBoolQueryBuilder build( ) { 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")); @@ -112,5 +112,4 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws static boolean isIndexFieldNameAllowed(String fieldName) { return ALLOWED_EXACT_INDEX_FIELD_NAMES.contains(fieldName) || fieldName.startsWith("metadata_flattened."); } - } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyFieldNameTranslators.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyFieldNameTranslators.java index a3ea533f6d2d6..e78c17317acb7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyFieldNameTranslators.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyFieldNameTranslators.java @@ -7,269 +7,7 @@ package org.elasticsearch.xpack.security.support; -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 org.elasticsearch.xpack.security.support.FieldNameTranslators.ExactFieldNameTranslator; -import org.elasticsearch.xpack.security.support.FieldNameTranslators.PrefixFieldNameTranslator; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Consumer; - -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 FieldNameTranslators FIELD_NAME_TRANSLATORS = new FieldNameTranslators( - 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.") - ) - ); - - /** - * Adds the {@param fieldSortBuilders} to the {@param searchSourceBuilder}, translating the field names, - * form query level to index level, see {@link FieldNameTranslators#translate}. - * The optional {@param visitor} can be used to collect all the translated field names. - */ - public static void translateFieldSortBuilders( - List fieldSortBuilders, - SearchSourceBuilder searchSourceBuilder, - @Nullable Consumer visitor - ) { - final Consumer 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 FieldNameTranslators#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 visitor) { - Objects.requireNonNull(queryBuilder, "unsupported \"null\" query builder for field name translation"); - final Consumer 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 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 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"); - } - } - - static String translate(String fieldName) { - return FIELD_NAME_TRANSLATORS.translate(fieldName); - } - - static Set translatePattern(String fieldName) { - return FIELD_NAME_TRANSLATORS.translatePattern(fieldName); - } -} +public class ApiKeyFieldNameTranslators {} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FieldNameTranslators.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FieldNameTranslators.java index 449af85e6c232..6062eb3a142d9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FieldNameTranslators.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FieldNameTranslators.java @@ -8,20 +8,283 @@ 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; + public final class FieldNameTranslators { + public static final FieldNameTranslators API_KEY_FIELD_NAME_TRANSLATORS = new FieldNameTranslators( + 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"), + // allows querying on any concrete (i.e. non-wildcard) fields under the "metadata." prefix + new PrefixFieldNameTranslator(s -> "metadata_flattened." + s.substring("metadata.".length()), "metadata.") + ) + ); + + public static final FieldNameTranslators ROLE_FIELD_NAME_TRANSLATORS = new FieldNameTranslators( + List.of( + new ExactFieldNameTranslator(s -> "name", "name"), + new ExactFieldNameTranslator(s -> "description", "description"), + new ExactFieldNameTranslator(s -> "applications.application", "applications.application"), + new ExactFieldNameTranslator(s -> "applications.resources", "applications.resource"), + new ExactFieldNameTranslator(s -> "applications.privileges", "applications.privilege"), + // allows querying on all metadata values as keywords because "metadata_flattened" is a flattened field type + new ExactFieldNameTranslator(s -> "metadata_flattened", "metadata"), + // allows querying on any concrete (i.e. non-wildcard) fields under the "metadata." prefix + new PrefixFieldNameTranslator(s -> "metadata_flattened." + s.substring("metadata.".length()), "metadata.") + ) + ); + final List fieldNameTranslators; - public FieldNameTranslators(List fieldNameTranslators) { + private FieldNameTranslators(List fieldNameTranslators) { this.fieldNameTranslators = fieldNameTranslators; } + /** + * 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 QueryBuilder translateQueryBuilderFields(QueryBuilder queryBuilder, @Nullable Consumer visitor) { + Objects.requireNonNull(queryBuilder, "unsupported \"null\" query builder for field name translation"); + final Consumer 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 currently supported in this context"); + } + 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 currently supported in this context"); + } + 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 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 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 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 fieldSortBuilders, + SearchSourceBuilder searchSourceBuilder, + @Nullable Consumer visitor + ) { + final Consumer fieldNameVisitor = visitor != null ? visitor : ignored -> {}; + fieldSortBuilders.forEach(fieldSortBuilder -> { + if (fieldSortBuilder.getNestedSort() != null) { + throw new IllegalArgumentException("nested sorting is not supported for querying"); + } + 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); + } + } + }); + } + /** * Translate the query level field name to index level field names. * It throws an exception if the field name is not explicitly allowed. diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/RoleBoolQueryBuilder.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/RoleBoolQueryBuilder.java index aa2165670189c..7a1d0211345ea 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/RoleBoolQueryBuilder.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/RoleBoolQueryBuilder.java @@ -7,6 +7,78 @@ 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.QueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.index.query.QueryRewriteContext; +import org.elasticsearch.index.query.SearchExecutionContext; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; -public class RoleBoolQueryBuilder extends BoolQueryBuilder {} +import java.io.IOException; +import java.util.Set; +import java.util.function.Consumer; + +import static org.elasticsearch.xpack.security.support.FieldNameTranslators.ROLE_FIELD_NAME_TRANSLATORS; + +public class RoleBoolQueryBuilder extends BoolQueryBuilder { + + // Field names allowed at the index level + private static final Set ALLOWED_EXACT_INDEX_FIELD_NAMES = Set.of( + "_id", + "doc_type", + "name", + "description", + "applications.application", + "applications.resources", + "applications.privileges", + "metadata_flattened" + ); + + private RoleBoolQueryBuilder() {} + + /** + * Build a bool query that is specialised for querying roles from the security index. + * The method processes the given QueryBuilder to ensure: + * * Only fields from an allowlist are queried + * * Only query types from an allowlist are used + * * Field names used in the Query DSL get translated into corresponding names used at the index level. + * * Not exposing any other types of documents stored in the same security index + * + * @param queryBuilder This represents the query parsed directly from the user input. It is validated + * and transformed (see above). + * @param fieldNameVisitor This {@code Consumer} is invoked with all the (index-level) field names referred to in the passed-in query. + * @return A specialised query builder for roles that is safe to run on the security index. + */ + public static RoleBoolQueryBuilder build(QueryBuilder queryBuilder, @Nullable Consumer fieldNameVisitor) { + final RoleBoolQueryBuilder finalQuery = new RoleBoolQueryBuilder(); + if (queryBuilder != null) { + QueryBuilder processedQuery = ROLE_FIELD_NAME_TRANSLATORS.translateQueryBuilderFields(queryBuilder, fieldNameVisitor); + finalQuery.must(processedQuery); + } + finalQuery.filter(QueryBuilders.termQuery("doc_type", RoleDescriptor.ROLE_TYPE)); + if (fieldNameVisitor != null) { + fieldNameVisitor.accept("doc_type"); + } + return finalQuery; + } + + @Override + protected Query doToQuery(SearchExecutionContext context) throws IOException { + context.setAllowedFields(RoleBoolQueryBuilder::isIndexFieldNameAllowed); + return super.doToQuery(context); + } + + @Override + protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException { + if (queryRewriteContext instanceof SearchExecutionContext) { + ((SearchExecutionContext) queryRewriteContext).setAllowedFields(RoleBoolQueryBuilder::isIndexFieldNameAllowed); + } + return super.doRewrite(queryRewriteContext); + } + + static boolean isIndexFieldNameAllowed(String fieldName) { + return ALLOWED_EXACT_INDEX_FIELD_NAMES.contains(fieldName) || fieldName.startsWith("metadata_flattened."); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyActionTests.java index 1593fadf1802d..a51c45cfbd9c9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyActionTests.java @@ -13,13 +13,13 @@ 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 void testTranslateFieldSortBuilders() { List 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,7 +96,7 @@ public void testNestedSortingIsNotAllowed() { 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 -> {} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java index d4f21fe878984..6674ea7a9d935 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java @@ -51,7 +51,7 @@ 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 void testPrefixQueryBuilderPropertiesArePreserved() { } List 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 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 void testSimpleQueryBuilderPropertiesArePreserved() { assertThat(simpleQueryStringBuilder2.fields().size(), is(simpleQueryStringBuilder.fields().size())); for (Map.Entry 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 void testMatchQueryBuilderPropertiesArePreserved() { } List 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 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 void testAllowListOfFieldNames() { final Authentication authentication = randomBoolean() ? AuthenticationTests.randomAuthentication(null, null) : null; final String randomFieldName = randomValueOtherThanMany( - s -> FIELD_NAME_TRANSLATORS.fieldNameTranslators.stream().anyMatch(t -> t.supports(s)), + s -> API_KEY_FIELD_NAME_TRANSLATORS.fieldNameTranslators.stream().anyMatch(t -> t.supports(s)), () -> randomAlphaOfLengthBetween(3, 20) ); final String fieldName = randomFrom(