|
@@ -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;
|
|
|
}
|
|
|
}
|
|
|
}
|