From 5eef6e6dba2aa74c7976be746588c36c4a6c27e6 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Fri, 7 Jun 2024 18:43:09 +0300 Subject: [PATCH] WIP --- .../core/security/authz/RoleDescriptor.java | 3 +- .../action/role/TransportQueryRoleAction.java | 37 +++--- .../support/ApiKeyFieldNameTranslators.java | 109 ++--------------- .../support/FieldNameTranslators.java | 114 ++++++++++++++++++ .../support/RoleBoolQueryBuilder.java | 3 +- .../support/ApiKeyBoolQueryBuilderTests.java | 6 +- 6 files changed, 147 insertions(+), 125 deletions(-) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FieldNameTranslators.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java index 565587573ea65..3f35b31e82a00 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java @@ -441,7 +441,6 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params, boolea */ public XContentBuilder innerToXContent(XContentBuilder builder, Params params, boolean docCreation, boolean includeMetadataFlattened) throws IOException { - builder.startObject(); builder.array(Fields.CLUSTER.getPreferredName(), clusterPrivileges); if (configurableClusterPrivileges.length != 0) { builder.field(Fields.GLOBAL.getPreferredName()); @@ -473,7 +472,7 @@ public XContentBuilder innerToXContent(XContentBuilder builder, Params params, b if (hasDescription()) { builder.field(Fields.DESCRIPTION.getPreferredName(), description); } - return builder.endObject(); + return builder; } @Override 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 362591acad9da..9b81be06f9906 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,11 +18,6 @@ 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.ApiKeyBoolQueryBuilder; - -import java.util.concurrent.atomic.AtomicBoolean; - -import static org.elasticsearch.xpack.security.support.ApiKeyFieldNameTranslators.translateFieldSortBuilders; public class TransportQueryRoleAction extends TransportAction { @@ -37,27 +32,27 @@ public TransportQueryRoleAction(ActionFilters actionFilters, NativeRolesStore na @Override protected void doExecute(Task task, QueryApiKeyRequest request, ActionListener listener) { final SearchSourceBuilder searchSourceBuilder = SearchSourceBuilder.searchSource() - .version(false) - .fetchSource(true) - .trackTotalHits(true); + .version(false) + .fetchSource(true) + .trackTotalHits(true); if (request.getFrom() != null) { searchSourceBuilder.from(request.getFrom()); } if (request.getSize() != null) { searchSourceBuilder.size(request.getSize()); } - searchSourceBuilder.query(ApiKeyBoolQueryBuilder.build(request.getQueryBuilder(), fieldName -> { - 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(ApiKeyBoolQueryBuilder.build(request.getQueryBuilder(), fieldName -> { + // 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); + // } + // }); + // } } } 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 23fee852f8b83..a3ea533f6d2d6 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,7 +7,6 @@ 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; @@ -27,15 +26,15 @@ 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.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; @@ -43,10 +42,8 @@ * A class to translate query level field names to index level field names. */ public class ApiKeyFieldNameTranslators { - static final List FIELD_NAME_TRANSLATORS; - - static { - FIELD_NAME_TRANSLATORS = List.of( + 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"), @@ -58,12 +55,12 @@ public class ApiKeyFieldNameTranslators { // 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 #translate}. + * 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( @@ -108,7 +105,7 @@ public static void translateFieldSortBuilders( /** * 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 + * 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. @@ -268,93 +265,11 @@ public static QueryBuilder translateQueryBuilderFields(QueryBuilder queryBuilder } } - /** - * 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) { - // protected for testing - if (Regex.isSimpleMatchPattern(fieldName)) { - throw new IllegalArgumentException("Field name pattern [" + fieldName + "] is not allowed for API Key query or aggregation"); - } - for (FieldNameTranslator translator : FIELD_NAME_TRANSLATORS) { - if (translator.supports(fieldName)) { - return translator.translate(fieldName); - } - } - throw new IllegalArgumentException("Field [" + fieldName + "] is not allowed for API Key query 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 translatePattern(String fieldNameOrPattern) { - Set indexFieldNames = new HashSet<>(); - for (FieldNameTranslator translator : FIELD_NAME_TRANSLATORS) { - if (translator.supports(fieldNameOrPattern)) { - indexFieldNames.add(translator.translate(fieldNameOrPattern)); - } - } - // It's OK to "translate" to the empty set the concrete disallowed or unknown field names. - // For eg, the SimpleQueryString query type is lenient in the sense that it ignores unknown fields and field name patterns, - // so this preprocessing can ignore them too. - return indexFieldNames; - } - - abstract static class FieldNameTranslator { - - private final Function translationFunc; - - protected FieldNameTranslator(Function translationFunc) { - this.translationFunc = translationFunc; - } - - String translate(String fieldName) { - return translationFunc.apply(fieldName); - } - - abstract boolean supports(String fieldName); - } - - static class ExactFieldNameTranslator extends FieldNameTranslator { - private final String name; - - ExactFieldNameTranslator(Function translationFunc, String name) { - super(translationFunc); - this.name = name; - } - - @Override - public boolean supports(String fieldNameOrPattern) { - if (Regex.isSimpleMatchPattern(fieldNameOrPattern)) { - return Regex.simpleMatch(fieldNameOrPattern, name); - } else { - return name.equals(fieldNameOrPattern); - } - } + static String translate(String fieldName) { + return FIELD_NAME_TRANSLATORS.translate(fieldName); } - static class PrefixFieldNameTranslator extends FieldNameTranslator { - private final String prefix; - - PrefixFieldNameTranslator(Function translationFunc, String prefix) { - super(translationFunc); - this.prefix = prefix; - } - - @Override - boolean supports(String fieldNameOrPattern) { - if (Regex.isSimpleMatchPattern(fieldNameOrPattern)) { - // It is not possible to translate a pattern into concrete field names, - // because we do not store the list of concrete field names sharing the prefix. - // 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 eventually ignored, - // because of the way that the flattened field type works in ES) - return false; - } - return fieldNameOrPattern.startsWith(prefix); - } + static Set translatePattern(String fieldName) { + return FIELD_NAME_TRANSLATORS.translatePattern(fieldName); } } 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 new file mode 100644 index 0000000000000..449af85e6c232 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FieldNameTranslators.java @@ -0,0 +1,114 @@ +/* + * 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 org.elasticsearch.common.regex.Regex; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +public final class FieldNameTranslators { + + final List fieldNameTranslators; + + public FieldNameTranslators(List fieldNameTranslators) { + this.fieldNameTranslators = fieldNameTranslators; + } + + /** + * Translate the query level field name to index level field names. + * It throws an exception if the field name is not explicitly allowed. + */ + public String translate(String fieldName) { + // protected for testing + if (Regex.isSimpleMatchPattern(fieldName)) { + throw new IllegalArgumentException("Field name pattern [" + fieldName + "] is not allowed for querying or aggregation"); + } + for (FieldNameTranslator translator : fieldNameTranslators) { + if (translator.supports(fieldName)) { + return translator.translate(fieldName); + } + } + throw new IllegalArgumentException("Field [" + fieldName + "] 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. + */ + public Set translatePattern(String fieldNameOrPattern) { + Set indexFieldNames = new HashSet<>(); + for (FieldNameTranslator translator : fieldNameTranslators) { + if (translator.supports(fieldNameOrPattern)) { + indexFieldNames.add(translator.translate(fieldNameOrPattern)); + } + } + // It's OK to "translate" to the empty set the concrete disallowed or unknown field names. + // For eg, the SimpleQueryString query type is lenient in the sense that it ignores unknown fields and field name patterns, + // so this preprocessing can ignore them too. + return indexFieldNames; + } + + static abstract class FieldNameTranslator { + + private final Function translationFunc; + + protected FieldNameTranslator(Function translationFunc) { + this.translationFunc = translationFunc; + } + + String translate(String fieldName) { + return translationFunc.apply(fieldName); + } + + abstract boolean supports(String fieldName); + } + + static class ExactFieldNameTranslator extends FieldNameTranslator { + private final String name; + + ExactFieldNameTranslator(Function translationFunc, String name) { + super(translationFunc); + this.name = name; + } + + @Override + public boolean supports(String fieldNameOrPattern) { + if (Regex.isSimpleMatchPattern(fieldNameOrPattern)) { + return Regex.simpleMatch(fieldNameOrPattern, name); + } else { + return name.equals(fieldNameOrPattern); + } + } + } + + static class PrefixFieldNameTranslator extends FieldNameTranslator { + private final String prefix; + + PrefixFieldNameTranslator(Function translationFunc, String prefix) { + super(translationFunc); + this.prefix = prefix; + } + + @Override + boolean supports(String fieldNameOrPattern) { + if (Regex.isSimpleMatchPattern(fieldNameOrPattern)) { + // It is not possible to translate a pattern into concrete field names, + // because we do not store the list of concrete field names sharing this same prefix. + // That 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 eventually ignored, + // because of the way that the flattened field type works in ES) + return false; + } + return fieldNameOrPattern.startsWith(prefix); + } + } +} 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 4aad338ebdc42..aa2165670189c 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 @@ -9,5 +9,4 @@ import org.elasticsearch.index.query.BoolQueryBuilder; -public class RoleBoolQueryBuilder extends BoolQueryBuilder { -} +public class RoleBoolQueryBuilder extends BoolQueryBuilder {} 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 fdc7b59528153..d4f21fe878984 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 @@ -612,7 +612,7 @@ public void testAllowListOfFieldNames() { final Authentication authentication = randomBoolean() ? AuthenticationTests.randomAuthentication(null, null) : null; final String randomFieldName = randomValueOtherThanMany( - s -> FIELD_NAME_TRANSLATORS.stream().anyMatch(t -> t.supports(s)), + s -> FIELD_NAME_TRANSLATORS.fieldNameTranslators.stream().anyMatch(t -> t.supports(s)), () -> randomAlphaOfLengthBetween(3, 20) ); final String fieldName = randomFrom( @@ -638,7 +638,7 @@ public void testAllowListOfFieldNames() { 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 void testAllowListOfFieldNames() { 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")); } }