Query API Keys support for both aggs and aggregations keywords (#107054)

The Query API Key Information endpoint supports aggs since #104895.
But some lang clients actually use the `aggregations` keyword in requests,
as the preferred synonym to `aggs`.
This PR adds support for the `aggregations` request keyword as a synonym
for the existing `aggs` term.

Closes #106839
This commit is contained in:
Albert Zaharovits 2024-04-03 18:33:14 +03:00 committed by GitHub
parent 755226df23
commit 36bcb6b398
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 96 additions and 14 deletions

View file

@ -0,0 +1,6 @@
pr: 107054
summary: Query API Keys support for both `aggs` and `aggregations` keywords
area: Security
type: enhancement
issues:
- 106839

View file

@ -232,7 +232,7 @@ simply mentioning `metadata` (not followed by any dot and sub-field name).
NOTE: You cannot query the role descriptors of an API key. NOTE: You cannot query the role descriptors of an API key.
==== ====
`aggs`:: `aggs` or `aggregations`::
(Optional, object) Any <<search-aggregations,aggregations>> to run over the corpus of returned API keys. (Optional, object) Any <<search-aggregations,aggregations>> to run over the corpus of returned API keys.
Aggregations and queries work together. Aggregations are computed only on the API keys that match the query. Aggregations and queries work together. Aggregations are computed only on the API keys that match the query.
This supports only a subset of aggregation types, namely: <<search-aggregations-bucket-terms-aggregation,terms>>, This supports only a subset of aggregation types, namely: <<search-aggregations-bucket-terms-aggregation,terms>>,

View file

@ -98,7 +98,7 @@ public class ApiKeyAggsIT extends SecurityInBasicRestTestCase {
// other bucket // other bucket
assertAggs(API_KEY_USER_AUTH_HEADER, typedAggs, """ assertAggs(API_KEY_USER_AUTH_HEADER, typedAggs, """
{ {
"aggs": { "aggregations": {
"only_user_keys": { "only_user_keys": {
"filters": { "filters": {
"other_bucket_key": "other_user_keys", "other_bucket_key": "other_user_keys",
@ -267,7 +267,7 @@ public class ApiKeyAggsIT extends SecurityInBasicRestTestCase {
"good-api-key-invalidated": { "term": {"invalidated": false}} "good-api-key-invalidated": { "term": {"invalidated": false}}
} }
}, },
"aggs": { "aggregations": {
"wrong-field": { "wrong-field": {
"filters": { "filters": {
"filters": { "filters": {
@ -487,7 +487,7 @@ public class ApiKeyAggsIT extends SecurityInBasicRestTestCase {
{ "usernames": { "terms": { "field": "username" } } } { "usernames": { "terms": { "field": "username" } } }
] ]
}, },
"aggs": { "aggregations": {
"not_expired": { "not_expired": {
"filter": { "filter": {
"range": { "range": {
@ -564,7 +564,7 @@ public class ApiKeyAggsIT extends SecurityInBasicRestTestCase {
); );
request.setJsonEntity(""" request.setJsonEntity("""
{ {
"aggs": { "aggregations": {
"all_.security_docs": { "all_.security_docs": {
"global": {}, "global": {},
"aggs": { "aggs": {

View file

@ -36,6 +36,8 @@ import static org.elasticsearch.index.query.AbstractQueryBuilder.parseTopLevelQu
import static org.elasticsearch.rest.RestRequest.Method.GET; import static org.elasticsearch.rest.RestRequest.Method.GET;
import static org.elasticsearch.rest.RestRequest.Method.POST; import static org.elasticsearch.rest.RestRequest.Method.POST;
import static org.elasticsearch.search.aggregations.AggregatorFactories.parseAggregators; import static org.elasticsearch.search.aggregations.AggregatorFactories.parseAggregators;
import static org.elasticsearch.search.builder.SearchSourceBuilder.AGGREGATIONS_FIELD;
import static org.elasticsearch.search.builder.SearchSourceBuilder.AGGS_FIELD;
import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg;
/** /**
@ -47,19 +49,27 @@ public final class RestQueryApiKeyAction extends ApiKeyBaseRestHandler {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private static final ConstructingObjectParser<Payload, Void> PARSER = new ConstructingObjectParser<>( private static final ConstructingObjectParser<Payload, Void> PARSER = new ConstructingObjectParser<>(
"query_api_key_request_payload", "query_api_key_request_payload",
a -> new Payload( a -> {
(QueryBuilder) a[0], if (a[1] != null && a[2] != null) {
(AggregatorFactories.Builder) a[1], throw new IllegalArgumentException("Duplicate 'aggs' or 'aggregations' field");
(Integer) a[2], } else {
(Integer) a[3], return new Payload(
(List<FieldSortBuilder>) a[4], (QueryBuilder) a[0],
(SearchAfterBuilder) a[5] (AggregatorFactories.Builder) (a[1] != null ? a[1] : a[2]),
) (Integer) a[3],
(Integer) a[4],
(List<FieldSortBuilder>) a[5],
(SearchAfterBuilder) a[6]
);
}
}
); );
static { static {
PARSER.declareObject(optionalConstructorArg(), (p, c) -> parseTopLevelQuery(p), new ParseField("query")); PARSER.declareObject(optionalConstructorArg(), (p, c) -> parseTopLevelQuery(p), new ParseField("query"));
PARSER.declareObject(optionalConstructorArg(), (p, c) -> parseAggregators(p), new ParseField("aggs")); // only one of aggs or aggregations is allowed
PARSER.declareObject(optionalConstructorArg(), (p, c) -> parseAggregators(p), AGGREGATIONS_FIELD);
PARSER.declareObject(optionalConstructorArg(), (p, c) -> parseAggregators(p), AGGS_FIELD);
PARSER.declareInt(optionalConstructorArg(), new ParseField("from")); PARSER.declareInt(optionalConstructorArg(), new ParseField("from"));
PARSER.declareInt(optionalConstructorArg(), new ParseField("size")); PARSER.declareInt(optionalConstructorArg(), new ParseField("size"));
PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> { PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> {

View file

@ -13,6 +13,7 @@ import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.action.ActionType; import org.elasticsearch.action.ActionType;
import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.client.internal.node.NodeClient;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment; import org.elasticsearch.env.Environment;
@ -35,6 +36,7 @@ import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.rest.FakeRestRequest; import org.elasticsearch.test.rest.FakeRestRequest;
import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.NamedXContentRegistry;
import org.elasticsearch.xcontent.XContentParseException;
import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xcontent.XContentType;
import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; import org.elasticsearch.xpack.core.security.action.apikey.ApiKey;
@ -48,6 +50,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
@ -145,6 +148,69 @@ public class RestQueryApiKeyActionTests extends ESTestCase {
assertNotNull(responseSetOnce.get()); assertNotNull(responseSetOnce.get());
} }
public void testAggsAndAggregationsTogether() {
String agg1;
String agg2;
if (randomBoolean()) {
agg1 = "aggs";
agg2 = "aggregations";
} else {
agg1 = "aggregations";
agg2 = "aggs";
}
final String requestBody = Strings.format("""
{
"%s": {
"all_keys_by_type": {
"composite": {
"sources": [
{ "type": { "terms": { "field": "type" } } }
]
}
}
},
"%s": {
"type_cardinality": {
"cardinality": {
"field": "type"
}
}
}
}""", agg1, agg2);
final FakeRestRequest restRequest = new FakeRestRequest.Builder(xContentRegistry()).withContent(
new BytesArray(requestBody),
XContentType.JSON
).build();
final SetOnce<RestResponse> responseSetOnce = new SetOnce<>();
final RestChannel restChannel = new AbstractRestChannel(restRequest, randomBoolean()) {
@Override
public void sendResponse(RestResponse restResponse) {
responseSetOnce.set(restResponse);
}
};
final var client = new NodeClient(Settings.EMPTY, threadPool) {
@SuppressWarnings("unchecked")
@Override
public <Request extends ActionRequest, Response extends ActionResponse> void doExecute(
ActionType<Response> action,
Request request,
ActionListener<Response> listener
) {
fail("TEST failed, request parsing should've failed");
listener.onResponse((Response) QueryApiKeyResponse.EMPTY);
}
};
RestQueryApiKeyAction restQueryApiKeyAction = new RestQueryApiKeyAction(Settings.EMPTY, mockLicenseState);
XContentParseException ex = expectThrows(
XContentParseException.class,
() -> restQueryApiKeyAction.handleRequest(restRequest, restChannel, client)
);
assertThat(ex.getCause().getMessage(), containsString("Duplicate 'aggs' or 'aggregations' field"));
assertThat(ex.getMessage(), containsString("Failed to build [query_api_key_request_payload]"));
assertNull(responseSetOnce.get());
}
public void testParsingSearchParameters() throws Exception { public void testParsingSearchParameters() throws Exception {
final String requestBody = """ final String requestBody = """
{ {