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.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -367,6 +368,105 @@ public class QueryApiKeyIT extends SecurityInBasicRestTestCase {
|
||||||
assertQueryError(authHeader, 400, "{\"sort\":[\"" + invalidFieldName + "\"]}");
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
private List<Object> extractSortValues(Map<String, Object> apiKeyInfo) {
|
private List<Object> extractSortValues(Map<String, Object> apiKeyInfo) {
|
||||||
return (List<Object>) apiKeyInfo.get("_sort");
|
return (List<Object>) apiKeyInfo.get("_sort");
|
||||||
|
@ -481,6 +581,16 @@ public class QueryApiKeyIT extends SecurityInBasicRestTestCase {
|
||||||
Map<String, Object> roleDescriptors,
|
Map<String, Object> roleDescriptors,
|
||||||
Map<String, Object> metadata,
|
Map<String, Object> metadata,
|
||||||
String authHeader
|
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 {
|
) throws IOException {
|
||||||
final Request request = new Request("POST", "/_security/api_key");
|
final Request request = new Request("POST", "/_security/api_key");
|
||||||
final String roleDescriptorsString = XContentTestUtils.convertToXContent(
|
final String roleDescriptorsString = XContentTestUtils.convertToXContent(
|
||||||
|
@ -489,13 +599,13 @@ public class QueryApiKeyIT extends SecurityInBasicRestTestCase {
|
||||||
).utf8ToString();
|
).utf8ToString();
|
||||||
final String metadataString = XContentTestUtils.convertToXContent(metadata == null ? Map.of() : metadata, XContentType.JSON)
|
final String metadataString = XContentTestUtils.convertToXContent(metadata == null ? Map.of() : metadata, XContentType.JSON)
|
||||||
.utf8ToString();
|
.utf8ToString();
|
||||||
if (randomBoolean()) {
|
if (expiration == null) {
|
||||||
request.setJsonEntity("""
|
request.setJsonEntity("""
|
||||||
{"name":"%s", "role_descriptors":%s, "metadata":%s}""".formatted(name, roleDescriptorsString, metadataString));
|
{"name":"%s", "role_descriptors":%s, "metadata":%s}""".formatted(name, roleDescriptorsString, metadataString));
|
||||||
} else {
|
} else {
|
||||||
request.setJsonEntity("""
|
request.setJsonEntity("""
|
||||||
{"name":"%s", "expiration": "10d", "role_descriptors":%s,\
|
{"name":"%s", "expiration": "%s", "role_descriptors":%s,\
|
||||||
"metadata":%s}""".formatted(name, roleDescriptorsString, metadataString));
|
"metadata":%s}""".formatted(name, expiration, roleDescriptorsString, metadataString));
|
||||||
}
|
}
|
||||||
request.setOptions(request.getOptions().toBuilder().addHeader(HttpHeaders.AUTHORIZATION, authHeader));
|
request.setOptions(request.getOptions().toBuilder().addHeader(HttpHeaders.AUTHORIZATION, authHeader));
|
||||||
final Response response = client().performRequest(request);
|
final Response response = client().performRequest(request);
|
||||||
|
|
|
@ -10,6 +10,7 @@ package org.elasticsearch.xpack.security.support;
|
||||||
import org.apache.lucene.search.Query;
|
import org.apache.lucene.search.Query;
|
||||||
import org.elasticsearch.core.Nullable;
|
import org.elasticsearch.core.Nullable;
|
||||||
import org.elasticsearch.index.query.BoolQueryBuilder;
|
import org.elasticsearch.index.query.BoolQueryBuilder;
|
||||||
|
import org.elasticsearch.index.query.ExistsQueryBuilder;
|
||||||
import org.elasticsearch.index.query.IdsQueryBuilder;
|
import org.elasticsearch.index.query.IdsQueryBuilder;
|
||||||
import org.elasticsearch.index.query.MatchAllQueryBuilder;
|
import org.elasticsearch.index.query.MatchAllQueryBuilder;
|
||||||
import org.elasticsearch.index.query.PrefixQueryBuilder;
|
import org.elasticsearch.index.query.PrefixQueryBuilder;
|
||||||
|
@ -99,6 +100,9 @@ public class ApiKeyBoolQueryBuilder extends BoolQueryBuilder {
|
||||||
} else if (qb instanceof final TermQueryBuilder query) {
|
} else if (qb instanceof final TermQueryBuilder query) {
|
||||||
final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName());
|
final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName());
|
||||||
return QueryBuilders.termQuery(translatedFieldName, query.value()).caseInsensitive(query.caseInsensitive());
|
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) {
|
} else if (qb instanceof final TermsQueryBuilder query) {
|
||||||
if (query.termsLookup() != null) {
|
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 supported for API Key query");
|
||||||
|
|
|
@ -219,7 +219,6 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
|
||||||
final AbstractQueryBuilder<? extends AbstractQueryBuilder<?>> q1 = randomFrom(
|
final AbstractQueryBuilder<? extends AbstractQueryBuilder<?>> q1 = randomFrom(
|
||||||
QueryBuilders.matchQuery(randomAlphaOfLength(5), randomAlphaOfLength(5)),
|
QueryBuilders.matchQuery(randomAlphaOfLength(5), randomAlphaOfLength(5)),
|
||||||
QueryBuilders.constantScoreQuery(mock(QueryBuilder.class)),
|
QueryBuilders.constantScoreQuery(mock(QueryBuilder.class)),
|
||||||
QueryBuilders.existsQuery(randomAlphaOfLength(5)),
|
|
||||||
QueryBuilders.boostingQuery(mock(QueryBuilder.class), mock(QueryBuilder.class)),
|
QueryBuilders.boostingQuery(mock(QueryBuilder.class), mock(QueryBuilder.class)),
|
||||||
QueryBuilders.queryStringQuery("q=a:42"),
|
QueryBuilders.queryStringQuery("q=a:42"),
|
||||||
QueryBuilders.simpleQueryStringQuery(randomAlphaOfLength(5)),
|
QueryBuilders.simpleQueryStringQuery(randomAlphaOfLength(5)),
|
||||||
|
@ -337,13 +336,14 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
private QueryBuilder randomSimpleQuery(String name) {
|
private QueryBuilder randomSimpleQuery(String name) {
|
||||||
return switch (randomIntBetween(0, 6)) {
|
return switch (randomIntBetween(0, 7)) {
|
||||||
case 0 -> QueryBuilders.termQuery(name, randomAlphaOfLengthBetween(3, 8));
|
case 0 -> QueryBuilders.termQuery(name, randomAlphaOfLengthBetween(3, 8));
|
||||||
case 1 -> QueryBuilders.termsQuery(name, randomArray(1, 3, String[]::new, () -> 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 2 -> QueryBuilders.idsQuery().addIds(randomArray(1, 3, String[]::new, () -> randomAlphaOfLength(22)));
|
||||||
case 3 -> QueryBuilders.prefixQuery(name, "prod-");
|
case 3 -> QueryBuilders.prefixQuery(name, "prod-");
|
||||||
case 4 -> QueryBuilders.wildcardQuery(name, "prod-*-east-*");
|
case 4 -> QueryBuilders.wildcardQuery(name, "prod-*-east-*");
|
||||||
case 5 -> QueryBuilders.matchAllQuery();
|
case 5 -> QueryBuilders.matchAllQuery();
|
||||||
|
case 6 -> QueryBuilders.existsQuery(name);
|
||||||
default -> QueryBuilders.rangeQuery(name)
|
default -> QueryBuilders.rangeQuery(name)
|
||||||
.from(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli(), randomBoolean())
|
.from(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli(), randomBoolean())
|
||||||
.to(Instant.now().toEpochMilli(), randomBoolean());
|
.to(Instant.now().toEpochMilli(), randomBoolean());
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue