mirror of
https://github.com/elastic/elasticsearch.git
synced 2025-06-29 01:44:36 -04:00
Support exists query for API key query (#87229)
Exists query is useful for testing whether expiration date is configured for an API key or whether a metadata field exists. This PR adds support for it.
This commit is contained in:
parent
602bb301c3
commit
aa25c1dac5
4 changed files with 124 additions and 5 deletions
5
docs/changelog/87229.yaml
Normal file
5
docs/changelog/87229.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
pr: 87229
|
||||
summary: Support exists query for API key query
|
||||
area: Security
|
||||
type: enhancement
|
||||
issues: []
|
|
@ -17,6 +17,7 @@ import org.elasticsearch.xcontent.XContentType;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
@ -367,6 +368,105 @@ public class QueryApiKeyIT extends SecurityInBasicRestTestCase {
|
|||
assertQueryError(authHeader, 400, "{\"sort\":[\"" + invalidFieldName + "\"]}");
|
||||
}
|
||||
|
||||
public void testExistsQuery() throws IOException, InterruptedException {
|
||||
final String authHeader = randomFrom(API_KEY_ADMIN_AUTH_HEADER, API_KEY_USER_AUTH_HEADER);
|
||||
|
||||
// No expiration
|
||||
createApiKey("test-exists-1", null, null, Map.of("value", 42), authHeader);
|
||||
// A short-lived key
|
||||
createApiKey("test-exists-2", "1ms", null, Map.of("label", "prod"), authHeader);
|
||||
createApiKey("test-exists-3", "1d", null, Map.of("value", 42, "label", "prod"), authHeader);
|
||||
|
||||
final long startTime = Instant.now().toEpochMilli();
|
||||
|
||||
assertQuery(
|
||||
authHeader,
|
||||
"""
|
||||
{"query": {"exists": {"field": "expiration" }}}""",
|
||||
apiKeys -> {
|
||||
assertThat(
|
||||
apiKeys.stream().map(k -> (String) k.get("name")).toList(),
|
||||
containsInAnyOrder("test-exists-2", "test-exists-3")
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
assertQuery(
|
||||
authHeader,
|
||||
"""
|
||||
{"query": {"exists": {"field": "metadata.value" }}}""",
|
||||
apiKeys -> {
|
||||
assertThat(
|
||||
apiKeys.stream().map(k -> (String) k.get("name")).toList(),
|
||||
containsInAnyOrder("test-exists-1", "test-exists-3")
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
assertQuery(
|
||||
authHeader,
|
||||
"""
|
||||
{"query": {"exists": {"field": "metadata.label" }}}""",
|
||||
apiKeys -> {
|
||||
assertThat(
|
||||
apiKeys.stream().map(k -> (String) k.get("name")).toList(),
|
||||
containsInAnyOrder("test-exists-2", "test-exists-3")
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Create an invalidated API key
|
||||
createAndInvalidateApiKey("test-exists-4", authHeader);
|
||||
|
||||
// Ensure the short-lived key is expired
|
||||
final long elapsed = Instant.now().toEpochMilli() - startTime;
|
||||
if (elapsed < 10) {
|
||||
Thread.sleep(10 - elapsed);
|
||||
}
|
||||
|
||||
// Find valid API keys (not invalidated nor expired)
|
||||
assertQuery(
|
||||
authHeader,
|
||||
"""
|
||||
{
|
||||
"query": {
|
||||
"bool": {
|
||||
"must": {
|
||||
"term": {
|
||||
"invalidated": false
|
||||
}
|
||||
},
|
||||
"should": [
|
||||
{
|
||||
"range": {
|
||||
"expiration": {
|
||||
"gte": "now"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"bool": {
|
||||
"must_not": {
|
||||
"exists": {
|
||||
"field": "expiration"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"minimum_should_match": 1
|
||||
}
|
||||
}
|
||||
}""",
|
||||
apiKeys -> {
|
||||
assertThat(
|
||||
apiKeys.stream().map(k -> (String) k.get("name")).toList(),
|
||||
containsInAnyOrder("test-exists-1", "test-exists-3")
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Object> extractSortValues(Map<String, Object> apiKeyInfo) {
|
||||
return (List<Object>) apiKeyInfo.get("_sort");
|
||||
|
@ -481,6 +581,16 @@ public class QueryApiKeyIT extends SecurityInBasicRestTestCase {
|
|||
Map<String, Object> roleDescriptors,
|
||||
Map<String, Object> metadata,
|
||||
String authHeader
|
||||
) throws IOException {
|
||||
return createApiKey(name, randomFrom("10d", null), roleDescriptors, metadata, authHeader);
|
||||
}
|
||||
|
||||
private Tuple<String, String> createApiKey(
|
||||
String name,
|
||||
String expiration,
|
||||
Map<String, Object> roleDescriptors,
|
||||
Map<String, Object> metadata,
|
||||
String authHeader
|
||||
) throws IOException {
|
||||
final Request request = new Request("POST", "/_security/api_key");
|
||||
final String roleDescriptorsString = XContentTestUtils.convertToXContent(
|
||||
|
@ -489,13 +599,13 @@ public class QueryApiKeyIT extends SecurityInBasicRestTestCase {
|
|||
).utf8ToString();
|
||||
final String metadataString = XContentTestUtils.convertToXContent(metadata == null ? Map.of() : metadata, XContentType.JSON)
|
||||
.utf8ToString();
|
||||
if (randomBoolean()) {
|
||||
if (expiration == null) {
|
||||
request.setJsonEntity("""
|
||||
{"name":"%s", "role_descriptors":%s, "metadata":%s}""".formatted(name, roleDescriptorsString, metadataString));
|
||||
} else {
|
||||
request.setJsonEntity("""
|
||||
{"name":"%s", "expiration": "10d", "role_descriptors":%s,\
|
||||
"metadata":%s}""".formatted(name, roleDescriptorsString, metadataString));
|
||||
{"name":"%s", "expiration": "%s", "role_descriptors":%s,\
|
||||
"metadata":%s}""".formatted(name, expiration, roleDescriptorsString, metadataString));
|
||||
}
|
||||
request.setOptions(request.getOptions().toBuilder().addHeader(HttpHeaders.AUTHORIZATION, authHeader));
|
||||
final Response response = client().performRequest(request);
|
||||
|
|
|
@ -10,6 +10,7 @@ package org.elasticsearch.xpack.security.support;
|
|||
import org.apache.lucene.search.Query;
|
||||
import org.elasticsearch.core.Nullable;
|
||||
import org.elasticsearch.index.query.BoolQueryBuilder;
|
||||
import org.elasticsearch.index.query.ExistsQueryBuilder;
|
||||
import org.elasticsearch.index.query.IdsQueryBuilder;
|
||||
import org.elasticsearch.index.query.MatchAllQueryBuilder;
|
||||
import org.elasticsearch.index.query.PrefixQueryBuilder;
|
||||
|
@ -99,6 +100,9 @@ public class ApiKeyBoolQueryBuilder extends BoolQueryBuilder {
|
|||
} 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 ExistsQueryBuilder query) {
|
||||
final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName());
|
||||
return QueryBuilders.existsQuery(translatedFieldName);
|
||||
} else if (qb instanceof final TermsQueryBuilder query) {
|
||||
if (query.termsLookup() != null) {
|
||||
throw new IllegalArgumentException("terms query with terms lookup is not supported for API Key query");
|
||||
|
|
|
@ -219,7 +219,6 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
|
|||
final AbstractQueryBuilder<? extends AbstractQueryBuilder<?>> q1 = randomFrom(
|
||||
QueryBuilders.matchQuery(randomAlphaOfLength(5), randomAlphaOfLength(5)),
|
||||
QueryBuilders.constantScoreQuery(mock(QueryBuilder.class)),
|
||||
QueryBuilders.existsQuery(randomAlphaOfLength(5)),
|
||||
QueryBuilders.boostingQuery(mock(QueryBuilder.class), mock(QueryBuilder.class)),
|
||||
QueryBuilders.queryStringQuery("q=a:42"),
|
||||
QueryBuilders.simpleQueryStringQuery(randomAlphaOfLength(5)),
|
||||
|
@ -337,13 +336,14 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
|
|||
}
|
||||
|
||||
private QueryBuilder randomSimpleQuery(String name) {
|
||||
return switch (randomIntBetween(0, 6)) {
|
||||
return switch (randomIntBetween(0, 7)) {
|
||||
case 0 -> QueryBuilders.termQuery(name, randomAlphaOfLengthBetween(3, 8));
|
||||
case 1 -> QueryBuilders.termsQuery(name, randomArray(1, 3, String[]::new, () -> randomAlphaOfLengthBetween(3, 8)));
|
||||
case 2 -> QueryBuilders.idsQuery().addIds(randomArray(1, 3, String[]::new, () -> randomAlphaOfLength(22)));
|
||||
case 3 -> QueryBuilders.prefixQuery(name, "prod-");
|
||||
case 4 -> QueryBuilders.wildcardQuery(name, "prod-*-east-*");
|
||||
case 5 -> QueryBuilders.matchAllQuery();
|
||||
case 6 -> QueryBuilders.existsQuery(name);
|
||||
default -> QueryBuilders.rangeQuery(name)
|
||||
.from(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli(), randomBoolean())
|
||||
.to(Instant.now().toEpochMilli(), randomBoolean());
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue