diff --git a/docs/changelog/103192.yaml b/docs/changelog/103192.yaml new file mode 100644 index 0000000000000..ed80b4529485c --- /dev/null +++ b/docs/changelog/103192.yaml @@ -0,0 +1,5 @@ +pr: 103192 +summary: Query api key api improvements +area: Security +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index bc95caa2d0bf0..f0c8563a1004a 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -173,6 +173,7 @@ static TransportVersion def(int id) { public static final TransportVersion NODE_STATS_REQUEST_SIMPLIFIED = def(8_561_00_0); public static final TransportVersion TEXT_EXPANSION_TOKEN_PRUNING_CONFIG_ADDED = def(8_562_00_0); public static final TransportVersion ESQL_ASYNC_QUERY = def(8_563_00_0); + public static final TransportVersion QUERY_API_KEY_AGGS_ADDED = def(8_564_00_0); /* * STOP! READ THIS FIRST! No, really, diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregationBuilder.java index dc11658437670..f3c13117f50aa 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregationBuilder.java @@ -41,7 +41,7 @@ public class FilterAggregationBuilder extends AbstractAggregationBuilder { public static final String NAME = "filter"; - private final QueryBuilder filter; + private QueryBuilder filter; /** * @param name @@ -59,6 +59,14 @@ public FilterAggregationBuilder(String name, QueryBuilder filter) { this.filter = filter; } + public FilterAggregationBuilder filter(QueryBuilder filter) { + if (filter == null) { + throw new IllegalArgumentException("[filter] must not be null: [" + name + "]"); + } + this.filter = filter; + return this; + } + protected FilterAggregationBuilder( FilterAggregationBuilder clone, AggregatorFactories.Builder factoriesBuilder, diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregator.java index 69dcc8d3da117..c3e2249ce7f0e 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregator.java @@ -61,7 +61,7 @@ public abstract class FiltersAggregator extends BucketsAggregator { public static class KeyedFilter implements Writeable, ToXContentFragment { private final String key; - private final QueryBuilder filter; + private QueryBuilder filter; public KeyedFilter(String key, QueryBuilder filter) { if (key == null) { @@ -92,6 +92,10 @@ public String key() { return key; } + public void filter(QueryBuilder filter) { + this.filter = filter; + } + public QueryBuilder filter() { return filter; } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceAggregationBuilder.java index 2b7e27eb97c7d..48afb79b95e90 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceAggregationBuilder.java @@ -192,7 +192,6 @@ public Set metricNames() { private String format = null; private Object missing = null; private ZoneId timeZone = null; - protected ValuesSourceConfig config; protected ValuesSourceAggregationBuilder(String name) { super(name); @@ -209,7 +208,6 @@ protected ValuesSourceAggregationBuilder( this.format = clone.format; this.missing = clone.missing; this.timeZone = clone.timeZone; - this.config = clone.config; this.script = clone.script; } diff --git a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java index c7077e4c867b0..b0bc09c77ce10 100644 --- a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java @@ -728,6 +728,14 @@ public SearchSourceBuilder collapse(CollapseBuilder collapse) { return this; } + public SearchSourceBuilder aggregationsBuilder(AggregatorFactories.Builder aggregations) { + if (this.aggregations != null) { + throw new IllegalStateException("cannot override aggregation build ["); + } + this.aggregations = aggregations; + return this; + } + /** * Add an aggregation to perform as part of the search. */ diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyRequest.java index e7eefaeb3a525..e2dc07075a632 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyRequest.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.core.Nullable; import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.searchafter.SearchAfterBuilder; import org.elasticsearch.search.sort.FieldSortBuilder; @@ -27,6 +28,8 @@ public final class QueryApiKeyRequest extends ActionRequest { @Nullable private final QueryBuilder queryBuilder; @Nullable + private final AggregatorFactories.Builder aggsBuilder; + @Nullable private final Integer from; @Nullable private final Integer size; @@ -42,11 +45,12 @@ public QueryApiKeyRequest() { } public QueryApiKeyRequest(QueryBuilder queryBuilder) { - this(queryBuilder, null, null, null, null, false); + this(queryBuilder, null, null, null, null, null, false); } public QueryApiKeyRequest( @Nullable QueryBuilder queryBuilder, + @Nullable AggregatorFactories.Builder aggsBuilder, @Nullable Integer from, @Nullable Integer size, @Nullable List fieldSortBuilders, @@ -54,6 +58,7 @@ public QueryApiKeyRequest( boolean withLimitedBy ) { this.queryBuilder = queryBuilder; + this.aggsBuilder = aggsBuilder; this.from = from; this.size = size; this.fieldSortBuilders = fieldSortBuilders; @@ -77,12 +82,21 @@ public QueryApiKeyRequest(StreamInput in) throws IOException { } else { this.withLimitedBy = false; } + if (in.getTransportVersion().onOrAfter(TransportVersions.QUERY_API_KEY_AGGS_ADDED)) { + this.aggsBuilder = in.readOptionalWriteable(AggregatorFactories.Builder::new); + } else { + this.aggsBuilder = null; + } } public QueryBuilder getQueryBuilder() { return queryBuilder; } + public AggregatorFactories.Builder getAggsBuilder() { + return aggsBuilder; + } + public Integer getFrom() { return from; } @@ -139,5 +153,8 @@ public void writeTo(StreamOutput out) throws IOException { if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_5_0)) { out.writeBoolean(withLimitedBy); } + if (out.getTransportVersion().onOrAfter(TransportVersions.QUERY_API_KEY_AGGS_ADDED)) { + out.writeOptionalWriteable(aggsBuilder); + } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyResponse.java index c8771a1604b03..c7f5f2ba81e7c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyResponse.java @@ -7,19 +7,22 @@ package org.elasticsearch.xpack.core.security.action.apikey; +import org.elasticsearch.TransportVersions; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.core.Nullable; +import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; +import java.util.List; import java.util.Objects; /** @@ -30,21 +33,28 @@ public final class QueryApiKeyResponse extends ActionResponse implements ToXCont private final long total; private final Item[] items; + private final @Nullable Aggregations aggregations; public QueryApiKeyResponse(StreamInput in) throws IOException { super(in); this.total = in.readLong(); this.items = in.readArray(Item::new, Item[]::new); + if (in.getTransportVersion().onOrAfter(TransportVersions.QUERY_API_KEY_AGGS_ADDED)) { + this.aggregations = in.readOptionalWriteable(InternalAggregations::readFrom); + } else { + this.aggregations = null; + } } - public QueryApiKeyResponse(long total, Collection items) { + public QueryApiKeyResponse(long total, Collection items, @Nullable Aggregations aggregations) { this.total = total; Objects.requireNonNull(items, "items must be provided"); this.items = items.toArray(new Item[0]); + this.aggregations = aggregations; } public static QueryApiKeyResponse emptyResponse() { - return new QueryApiKeyResponse(0, Collections.emptyList()); + return new QueryApiKeyResponse(0, List.of(), null); } public long getTotal() { @@ -59,9 +69,16 @@ public int getCount() { return items.length; } + public Aggregations getAggregations() { + return aggregations; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject().field("total", total).field("count", items.length).array("api_keys", (Object[]) items); + if (aggregations != null) { + aggregations.toXContent(builder, params); + } return builder.endObject(); } @@ -69,6 +86,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public void writeTo(StreamOutput out) throws IOException { out.writeLong(total); out.writeArray(items); + if (out.getTransportVersion().onOrAfter(TransportVersions.QUERY_API_KEY_AGGS_ADDED)) { + out.writeOptionalWriteable((InternalAggregations) aggregations); + } } @Override @@ -76,19 +96,20 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; QueryApiKeyResponse that = (QueryApiKeyResponse) o; - return total == that.total && Arrays.equals(items, that.items); + return total == that.total && Arrays.equals(items, that.items) && Objects.equals(aggregations, that.aggregations); } @Override public int hashCode() { int result = Objects.hash(total); result = 31 * result + Arrays.hashCode(items); + result = 31 * result + Objects.hash(aggregations); return result; } @Override public String toString() { - return "QueryApiKeyResponse{" + "total=" + total + ", items=" + Arrays.toString(items) + '}'; + return "QueryApiKeyResponse{" + "total=" + total + ", items=" + Arrays.toString(items) + ", aggs=" + aggregations + '}'; } public static class Item implements ToXContentObject, Writeable { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyRequestTests.java index 6c7df3d4db80c..f26595c89a565 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyRequestTests.java @@ -65,6 +65,7 @@ public void testReadWrite() throws IOException { final QueryApiKeyRequest request3 = new QueryApiKeyRequest( QueryBuilders.matchAllQuery(), + null, 42, 20, List.of( @@ -91,6 +92,7 @@ public void testReadWrite() throws IOException { public void testValidate() { final QueryApiKeyRequest request1 = new QueryApiKeyRequest( + null, null, randomIntBetween(0, Integer.MAX_VALUE), randomIntBetween(0, Integer.MAX_VALUE), @@ -101,6 +103,7 @@ public void testValidate() { assertThat(request1.validate(), nullValue()); final QueryApiKeyRequest request2 = new QueryApiKeyRequest( + null, null, randomIntBetween(Integer.MIN_VALUE, -1), randomIntBetween(0, Integer.MAX_VALUE), @@ -111,6 +114,7 @@ public void testValidate() { assertThat(request2.validate().getMessage(), containsString("[from] parameter cannot be negative")); final QueryApiKeyRequest request3 = new QueryApiKeyRequest( + null, null, randomIntBetween(0, Integer.MAX_VALUE), randomIntBetween(Integer.MIN_VALUE, -1), diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyResponseTests.java index 677d2201fe1e1..73c90ee84e03c 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyResponseTests.java @@ -34,7 +34,7 @@ protected Writeable.Reader instanceReader() { @Override protected QueryApiKeyResponse createTestInstance() { final List items = randomList(0, 3, this::randomItem); - return new QueryApiKeyResponse(randomIntBetween(items.size(), 100), items); + return new QueryApiKeyResponse(randomIntBetween(items.size(), 100), items, null); } @Override @@ -43,13 +43,13 @@ protected QueryApiKeyResponse mutateInstance(QueryApiKeyResponse instance) { switch (randomIntBetween(0, 3)) { case 0: items.add(randomItem()); - return new QueryApiKeyResponse(instance.getTotal(), items); + return new QueryApiKeyResponse(instance.getTotal(), items, null); case 1: if (false == items.isEmpty()) { - return new QueryApiKeyResponse(instance.getTotal(), items.subList(1, items.size())); + return new QueryApiKeyResponse(instance.getTotal(), items.subList(1, items.size()), null); } else { items.add(randomItem()); - return new QueryApiKeyResponse(instance.getTotal(), items); + return new QueryApiKeyResponse(instance.getTotal(), items, null); } case 2: if (false == items.isEmpty()) { @@ -58,9 +58,9 @@ protected QueryApiKeyResponse mutateInstance(QueryApiKeyResponse instance) { } else { items.add(randomItem()); } - return new QueryApiKeyResponse(instance.getTotal(), items); + return new QueryApiKeyResponse(instance.getTotal(), items, null); default: - return new QueryApiKeyResponse(instance.getTotal() + 1, items); + return new QueryApiKeyResponse(instance.getTotal() + 1, items, null); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilegeTests.java index 22e6a6f005919..ec6f3c15add3c 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilegeTests.java @@ -259,7 +259,7 @@ public void testCheckQueryApiKeyRequest() { final ClusterPermission clusterPermission = ManageOwnApiKeyClusterPrivilege.INSTANCE.buildPermission(ClusterPermission.builder()) .build(); - final QueryApiKeyRequest queryApiKeyRequest = new QueryApiKeyRequest(null, null, null, null, null, randomBoolean()); + final QueryApiKeyRequest queryApiKeyRequest = new QueryApiKeyRequest(null, null, null, null, null, null, randomBoolean()); if (randomBoolean()) { queryApiKeyRequest.setFilterForCurrentUser(); } @@ -278,7 +278,7 @@ public void testAuthenticationWithApiKeyAllowsDeniesQueryApiKeyWithLimitedBy() { .build(); final boolean withLimitedBy = randomBoolean(); - final QueryApiKeyRequest queryApiKeyRequest = new QueryApiKeyRequest(null, null, null, null, null, withLimitedBy); + final QueryApiKeyRequest queryApiKeyRequest = new QueryApiKeyRequest(null, null, null, null, null, null, withLimitedBy); queryApiKeyRequest.setFilterForCurrentUser(); assertThat( clusterPermission.check(QueryApiKeyAction.NAME, queryApiKeyRequest, AuthenticationTestHelper.builder().apiKey().build(false)), diff --git a/x-pack/plugin/security/build.gradle b/x-pack/plugin/security/build.gradle index acb802743586c..e3d7ed8f2c4af 100644 --- a/x-pack/plugin/security/build.gradle +++ b/x-pack/plugin/security/build.gradle @@ -27,6 +27,9 @@ dependencies { testImplementation project(path: xpackModule('wildcard')) testImplementation project(path: ':modules:legacy-geo') testImplementation project(path: ':modules:percolator') +// Conflict found for the following module: +// - org.ow2.asm:asm between versions 8.0.1 and 7.2 +// testImplementation project(path: ':modules:lang-painless') testImplementation project(path: xpackModule('sql:sql-action')) testImplementation project(path: ':modules:analysis-common') testImplementation project(path: ':modules:reindex') diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java index 72a6b6049932c..a9ef97869dd85 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java @@ -2852,7 +2852,7 @@ private ApiKey getApiKeyInfo(Client client, String apiKeyId, boolean withLimited final PlainActionFuture future = new PlainActionFuture<>(); client.execute( QueryApiKeyAction.INSTANCE, - new QueryApiKeyRequest(QueryBuilders.idsQuery().addIds(apiKeyId), null, null, null, null, withLimitedBy), + new QueryApiKeyRequest(QueryBuilders.idsQuery().addIds(apiKeyId), null, null, null, null, null, withLimitedBy), future ); final QueryApiKeyResponse queryApiKeyResponse = future.actionGet(); @@ -2871,7 +2871,7 @@ private ApiKey[] getAllApiKeyInfo(Client client, boolean withLimitedBy) { final PlainActionFuture future = new PlainActionFuture<>(); client.execute( QueryApiKeyAction.INSTANCE, - new QueryApiKeyRequest(QueryBuilders.matchAllQuery(), null, 1000, null, null, withLimitedBy), + new QueryApiKeyRequest(QueryBuilders.matchAllQuery(), null, null, 1000, null, null, withLimitedBy), future ); final QueryApiKeyResponse queryApiKeyResponse = future.actionGet(); diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java index 91884086af959..b3892a0a2c0ff 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java @@ -625,6 +625,7 @@ public void testCreateCrossClusterApiKey() throws IOException { null, null, null, + null, randomBoolean() ); final QueryApiKeyResponse queryApiKeyResponse = client().execute(QueryApiKeyAction.INSTANCE, queryApiKeyRequest).actionGet(); @@ -722,6 +723,7 @@ public void testUpdateCrossClusterApiKey() throws IOException { null, null, null, + null, randomBoolean() ); final QueryApiKeyResponse queryApiKeyResponse = client().execute(QueryApiKeyAction.INSTANCE, queryApiKeyRequest).actionGet(); 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 4077597a7ef16..b870a0c23ecfd 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 @@ -13,6 +13,7 @@ import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.sort.FieldSortBuilder; import org.elasticsearch.tasks.Task; @@ -23,15 +24,23 @@ import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.security.authc.ApiKeyService; +import org.elasticsearch.xpack.security.support.ApiKeyAggregationsBuilder; import org.elasticsearch.xpack.security.support.ApiKeyBoolQueryBuilder; import org.elasticsearch.xpack.security.support.ApiKeyFieldNameTranslators; import java.util.List; +import java.util.Map; import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_MAIN_ALIAS; public final class TransportQueryApiKeyAction extends HandledTransportAction { + public static final String API_KEY_TYPE_RUNTIME_MAPPING_FIELD = "runtime_key_type"; + private static final Map API_KEY_TYPE_RUNTIME_MAPPING = Map.of( + API_KEY_TYPE_RUNTIME_MAPPING_FIELD, + Map.of("type", "keyword", "script", Map.of("source", "emit(doc['type'].value ?: \"rest\");")) + ); + private final ApiKeyService apiKeyService; private final SecurityContext securityContext; @@ -49,10 +58,13 @@ public TransportQueryApiKeyAction( @Override protected void doExecute(Task task, QueryApiKeyRequest request, ActionListener listener) { - final Authentication authentication = securityContext.getAuthentication(); - if (authentication == null) { + Authentication filteringAuthentication = securityContext.getAuthentication(); + if (filteringAuthentication == null) { listener.onFailure(new IllegalStateException("authentication is required")); } + if (request.isFilterForCurrentUser() == false) { + filteringAuthentication = null; + } final SearchSourceBuilder searchSourceBuilder = SearchSourceBuilder.searchSource() .version(false) @@ -68,10 +80,18 @@ protected void doExecute(Task task, QueryApiKeyRequest request, ActionListener apiKeyItem = Arrays.stream(searchResponse.getHits().getHits()) .map(hit -> convertSearchHitToQueryItem(hit, withLimitedBy)) .toList(); - listener.onResponse(new QueryApiKeyResponse(total, apiKeyItem)); + listener.onResponse(new QueryApiKeyResponse(total, apiKeyItem, searchResponse.getAggregations())); }, listener::onFailure) ) ); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestQueryApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestQueryApiKeyAction.java index c410052db70bf..c0b85f7beaf50 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestQueryApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestQueryApiKeyAction.java @@ -17,6 +17,7 @@ import org.elasticsearch.rest.Scope; import org.elasticsearch.rest.ServerlessScope; import org.elasticsearch.rest.action.RestToXContentListener; +import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.searchafter.SearchAfterBuilder; import org.elasticsearch.search.sort.FieldSortBuilder; import org.elasticsearch.xcontent.ConstructingObjectParser; @@ -32,6 +33,7 @@ import static org.elasticsearch.index.query.AbstractQueryBuilder.parseTopLevelQuery; import static org.elasticsearch.rest.RestRequest.Method.GET; import static org.elasticsearch.rest.RestRequest.Method.POST; +import static org.elasticsearch.search.aggregations.AggregatorFactories.parseAggregators; import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; /** @@ -43,11 +45,19 @@ public final class RestQueryApiKeyAction extends ApiKeyBaseRestHandler { @SuppressWarnings("unchecked") private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "query_api_key_request_payload", - a -> new Payload((QueryBuilder) a[0], (Integer) a[1], (Integer) a[2], (List) a[3], (SearchAfterBuilder) a[4]) + a -> new Payload( + (QueryBuilder) a[0], + (AggregatorFactories.Builder) a[1], + (Integer) a[2], + (Integer) a[3], + (List) a[4], + (SearchAfterBuilder) a[5] + ) ); static { PARSER.declareObject(optionalConstructorArg(), (p, c) -> parseTopLevelQuery(p), new ParseField("query")); + PARSER.declareObject(optionalConstructorArg(), (p, c) -> parseAggregators(p), new ParseField("aggs")); PARSER.declareInt(optionalConstructorArg(), new ParseField("from")); PARSER.declareInt(optionalConstructorArg(), new ParseField("size")); PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> { @@ -98,6 +108,7 @@ protected RestChannelConsumer innerPrepareRequest(final RestRequest request, fin final Payload payload = PARSER.parse(request.contentOrSourceParamParser(), null); queryApiKeyRequest = new QueryApiKeyRequest( payload.queryBuilder, + payload.aggsBuilder, payload.from, payload.size, payload.fieldSortBuilders, @@ -105,13 +116,14 @@ protected RestChannelConsumer innerPrepareRequest(final RestRequest request, fin withLimitedBy ); } else { - queryApiKeyRequest = new QueryApiKeyRequest(null, null, null, null, null, withLimitedBy); + queryApiKeyRequest = new QueryApiKeyRequest(null, null, null, null, null, null, withLimitedBy); } return channel -> client.execute(QueryApiKeyAction.INSTANCE, queryApiKeyRequest, new RestToXContentListener<>(channel)); } private record Payload( @Nullable QueryBuilder queryBuilder, + @Nullable AggregatorFactories.Builder aggsBuilder, @Nullable Integer from, @Nullable Integer size, @Nullable List fieldSortBuilders, 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 new file mode 100644 index 0000000000000..e158a735ead5c --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyAggregationsBuilder.java @@ -0,0 +1,113 @@ +/* + * 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.core.Nullable; +import org.elasticsearch.search.aggregations.AggregationBuilder; +import org.elasticsearch.search.aggregations.AggregatorFactories; +import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.composite.CompositeValuesSourceBuilder; +import org.elasticsearch.search.aggregations.bucket.composite.TermsValuesSourceBuilder; +import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregator; +import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.missing.MissingAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.range.DateRangeAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.range.RangeAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.CardinalityAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.ValueCountAggregationBuilder; +import org.elasticsearch.search.aggregations.support.ValuesSourceAggregationBuilder; +import org.elasticsearch.xpack.core.security.authc.Authentication; + +public class ApiKeyAggregationsBuilder { + + public static void verifyRequested( + @Nullable AggregatorFactories.Builder aggsBuilder, + @Nullable Authentication filteringAuthentication + ) { + if (aggsBuilder == null) { + return; + } + // Most of these can be supported without much hassle, but they're not useful for the identified use cases so far + for (PipelineAggregationBuilder pipelineAggregator : aggsBuilder.getPipelineAggregatorFactories()) { + throw new IllegalArgumentException("Unsupported pipeline aggregation of type [" + pipelineAggregator.getType() + "]"); + } + for (AggregationBuilder aggregator : aggsBuilder.getAggregatorFactories()) { + doVerify(aggregator, filteringAuthentication); + } + } + + private static void doVerify(AggregationBuilder aggregationBuilder, @Nullable Authentication filteringAuthentication) { + // Most of these can be supported without much hassle, but they're not useful for the identified use cases so far + for (PipelineAggregationBuilder pipelineAggregator : aggregationBuilder.getPipelineAggregations()) { + throw new IllegalArgumentException("Unsupported pipeline aggregation of type [" + pipelineAggregator.getType() + "]"); + } + if (aggregationBuilder instanceof ValuesSourceAggregationBuilder valuesSourceAggregationBuilder) { + if (valuesSourceAggregationBuilder instanceof TermsAggregationBuilder == false + && valuesSourceAggregationBuilder instanceof RangeAggregationBuilder == false + && valuesSourceAggregationBuilder instanceof DateRangeAggregationBuilder == false + && valuesSourceAggregationBuilder instanceof MissingAggregationBuilder == false + && valuesSourceAggregationBuilder instanceof CardinalityAggregationBuilder == false + && valuesSourceAggregationBuilder instanceof ValueCountAggregationBuilder == false) { + throw new IllegalArgumentException( + "Unsupported API Keys agg [" + aggregationBuilder.getName() + "] of type [" + aggregationBuilder.getType() + "]" + ); + } + // scripts are not currently supported because it's harder to restrict and rename the doc fields the script has access to + if (valuesSourceAggregationBuilder.script() != null) { + throw new IllegalArgumentException("Unsupported script value source for [" + aggregationBuilder.getName() + "] agg"); + } + // the user-facing field names are different from the index mapping field names of API Key docs + valuesSourceAggregationBuilder.field(ApiKeyFieldNameTranslators.translate(valuesSourceAggregationBuilder.field())); + } else if (aggregationBuilder instanceof CompositeAggregationBuilder compositeAggregationBuilder) { + for (CompositeValuesSourceBuilder valueSource : compositeAggregationBuilder.sources()) { + if (valueSource.script() != null) { + throw new IllegalArgumentException( + "Unsupported script value source for [" + + valueSource.name() + + "] of composite agg [" + + compositeAggregationBuilder.getName() + + "]" + ); + } + if (valueSource instanceof TermsValuesSourceBuilder == false) { + throw new IllegalArgumentException( + "Unsupported value source type for [" + + valueSource.name() + + "] of composite agg [" + + compositeAggregationBuilder.getName() + + "]." + + "Only [terms] value sources are allowed." + ); + } + valueSource.field(ApiKeyFieldNameTranslators.translate(valueSource.field())); + } + } else if (aggregationBuilder instanceof GlobalAggregationBuilder) { + // nothing to verify here + } else if (aggregationBuilder instanceof FilterAggregationBuilder filterAggregationBuilder) { + // filters the aggregation query to user's allowed API Keys only + filterAggregationBuilder.filter(ApiKeyBoolQueryBuilder.build(filterAggregationBuilder.getFilter(), filteringAuthentication)); + } else if (aggregationBuilder instanceof FiltersAggregationBuilder filtersAggregationBuilder) { + // filters the aggregation queries to user's allowed API Keys only + for (FiltersAggregator.KeyedFilter keyedFilter : filtersAggregationBuilder.filters()) { + keyedFilter.filter(ApiKeyBoolQueryBuilder.build(keyedFilter.filter(), filteringAuthentication)); + } + } else { + throw new IllegalArgumentException( + "Unsupported API Keys agg [" + aggregationBuilder.getName() + "] of type [" + aggregationBuilder.getType() + "]" + ); + } + // check sub-aggs recursively + for (AggregationBuilder subAggregation : aggregationBuilder.getSubAggregations()) { + doVerify(subAggregation, filteringAuthentication); + } + } +} 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 28ecd5ffe5b57..bcecf2e1ed198 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 @@ -13,12 +13,15 @@ import org.elasticsearch.index.query.ExistsQueryBuilder; import org.elasticsearch.index.query.IdsQueryBuilder; import org.elasticsearch.index.query.MatchAllQueryBuilder; +import org.elasticsearch.index.query.MultiMatchQueryBuilder; import org.elasticsearch.index.query.PrefixQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.query.QueryRewriteContext; +import org.elasticsearch.index.query.QueryStringQueryBuilder; import org.elasticsearch.index.query.RangeQueryBuilder; import org.elasticsearch.index.query.SearchExecutionContext; +import org.elasticsearch.index.query.SimpleQueryStringBuilder; import org.elasticsearch.index.query.TermQueryBuilder; import org.elasticsearch.index.query.TermsQueryBuilder; import org.elasticsearch.index.query.WildcardQueryBuilder; @@ -27,6 +30,8 @@ import org.elasticsearch.xpack.security.authc.ApiKeyService; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import java.util.Set; public class ApiKeyBoolQueryBuilder extends BoolQueryBuilder { @@ -36,6 +41,7 @@ public class ApiKeyBoolQueryBuilder extends BoolQueryBuilder { "_id", "doc_type", "name", + "type", "api_key_invalidated", "invalidation_time", "creation_time", @@ -103,6 +109,33 @@ private static QueryBuilder doProcess(QueryBuilder qb) { } else if (qb instanceof final TermQueryBuilder query) { final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName()); return QueryBuilders.termQuery(translatedFieldName, query.value()).caseInsensitive(query.caseInsensitive()); + } else if (qb instanceof final MultiMatchQueryBuilder query) { + // this relies on the query fields map to be mutable + Map originalFields = new HashMap<>(query.fields()); + query.fields().clear(); + for (Map.Entry originalField : originalFields.entrySet()) { + query.fields().put(ApiKeyFieldNameTranslators.translate(originalField.getKey()), originalField.getValue()); + } + assert query.fields().size() == originalFields.size(); + return query; + } else if (qb instanceof final SimpleQueryStringBuilder query) { + // this relies on the query fields map to be mutable + Map originalFields = new HashMap<>(query.fields()); + query.fields().clear(); + for (Map.Entry originalField : originalFields.entrySet()) { + query.fields().put(ApiKeyFieldNameTranslators.translate(originalField.getKey()), originalField.getValue()); + } + assert query.fields().size() == originalFields.size(); + return query; + } else if (qb instanceof final QueryStringQueryBuilder query) { + // this relies on the query fields map to be mutable + Map originalFields = new HashMap<>(query.fields()); + query.fields().clear(); + for (Map.Entry originalField : originalFields.entrySet()) { + query.fields().put(ApiKeyFieldNameTranslators.translate(originalField.getKey()), originalField.getValue()); + } + assert query.fields().size() == originalFields.size(); + return query; } else if (qb instanceof final ExistsQueryBuilder query) { final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName()); return QueryBuilders.existsQuery(translatedFieldName); 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 4d7cc9d978cd4..75d163fd0e6e4 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 @@ -21,6 +21,8 @@ public class ApiKeyFieldNameTranslators { new ExactFieldNameTranslator(s -> "creator.principal", "username"), new ExactFieldNameTranslator(s -> "creator.realm", "realm_name"), new ExactFieldNameTranslator(Function.identity(), "name"), + new ExactFieldNameTranslator(Function.identity(), "type"), + // new ExactFieldNameTranslator(s -> TransportQueryApiKeyAction.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"), @@ -39,7 +41,7 @@ public static String translate(String fieldName) { return translator.translate(fieldName); } } - throw new IllegalArgumentException("Field [" + fieldName + "] is not allowed for API Key query"); + throw new IllegalArgumentException("Field [" + fieldName + "] is not allowed for API Key query or agg"); } abstract static class FieldNameTranslator { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/LocalStateSecurity.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/LocalStateSecurity.java index a2aa04e0f56c3..fbded3f38651b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/LocalStateSecurity.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/LocalStateSecurity.java @@ -85,6 +85,8 @@ protected XPackLicenseState getLicenseState() { return thisVar.getLicenseState(); } }); + // testImplementation project(path: ':modules:lang-painless') + // plugins.add(new PainlessPlugin()); plugins.add(new Monitoring(settings) { @Override protected SSLService getSslService() { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestQueryApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestQueryApiKeyActionTests.java index 67d2ab006eb22..b743361b75e1e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestQueryApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestQueryApiKeyActionTests.java @@ -125,7 +125,7 @@ public void doE final QueryBuilder shouldQueryBuilder = boolQueryBuilder.should().get(0); assertThat(shouldQueryBuilder.getClass(), is(PrefixQueryBuilder.class)); assertThat(((PrefixQueryBuilder) shouldQueryBuilder).fieldName(), equalTo("metadata.environ")); - listener.onResponse((Response) new QueryApiKeyResponse(0, List.of())); + listener.onResponse((Response) QueryApiKeyResponse.emptyResponse()); } }; final RestQueryApiKeyAction restQueryApiKeyAction = new RestQueryApiKeyAction(Settings.EMPTY, mockLicenseState); @@ -189,7 +189,7 @@ public void doE equalTo(new SearchAfterBuilder().setSortValues(new String[] { "key-2048", "2021-07-01T00:00:59.000Z" })) ); - listener.onResponse((Response) new QueryApiKeyResponse(0, List.of())); + listener.onResponse((Response) QueryApiKeyResponse.emptyResponse()); } };