Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
albertzaharovits committed Jun 7, 2024
1 parent 756b708 commit 5eef6e6
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 125 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<QueryApiKeyRequest, QueryApiKeyResponse> {

Expand All @@ -37,27 +32,27 @@ public TransportQueryRoleAction(ActionFilters actionFilters, NativeRolesStore na
@Override
protected void doExecute(Task task, QueryApiKeyRequest request, ActionListener<QueryApiKeyResponse> 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);
// }
// });
// }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,26 +26,24 @@
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;

/**
* 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(
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"),
Expand All @@ -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(
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<String> translatePattern(String fieldNameOrPattern) {
Set<String> indexFieldNames = new HashSet<>();
for (FieldNameTranslator translator : FIELD_NAME_TRANSLATORS) {
if (translator.supports(fieldNameOrPattern)) {
indexFieldNames.add(translator.translate(fieldNameOrPattern));
}
}
// It's OK to "translate" to the empty set the concrete disallowed or unknown field names.
// 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<String, String> translationFunc;

protected FieldNameTranslator(Function<String, String> 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<String, String> 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<String, String> 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<String> translatePattern(String fieldName) {
return FIELD_NAME_TRANSLATORS.translatePattern(fieldName);
}
}
Original file line number Diff line number Diff line change
@@ -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<FieldNameTranslator> fieldNameTranslators;

public FieldNameTranslators(List<FieldNameTranslator> 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<String> translatePattern(String fieldNameOrPattern) {
Set<String> 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<String, String> translationFunc;

protected FieldNameTranslator(Function<String, String> 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<String, String> 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<String, String> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,4 @@

import org.elasticsearch.index.query.BoolQueryBuilder;

public class RoleBoolQueryBuilder extends BoolQueryBuilder {
}
public class RoleBoolQueryBuilder extends BoolQueryBuilder {}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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"));
}
}

Expand Down

0 comments on commit 5eef6e6

Please sign in to comment.