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:
Yang Wang 2022-05-31 15:48:44 +10:00 committed by GitHub
parent 602bb301c3
commit aa25c1dac5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 124 additions and 5 deletions

View file

@ -0,0 +1,5 @@
pr: 87229
summary: Support exists query for API key query
area: Security
type: enhancement
issues: []

View file

@ -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);

View file

@ -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");

View file

@ -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());