-
Notifications
You must be signed in to change notification settings - Fork 24.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
A new search API for API keys - core search function (#75335)
This PR adds a new API for searching API keys. The API supports searching API keys with a controlled list of field names and a subset of Query DSL. It also provides a translation layer between the field names used in the REST layer and those in the index layer. This is to prevent tight coupling between the user facing request and index mappings so that they can evolve separately. Compared to the Get API key API, this new search API automatically applies calling user's security context similar to regular searches, e.g. if the user has only manage_own_api_key privilege, only keys owned by the user are returned in the search response. Relates: #71023
- Loading branch information
Showing
22 changed files
with
1,436 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
.../src/main/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyAction.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/* | ||
* 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.core.security.action.apikey; | ||
|
||
import org.elasticsearch.action.ActionType; | ||
|
||
public final class QueryApiKeyAction extends ActionType<QueryApiKeyResponse> { | ||
|
||
public static final String NAME = "cluster:admin/xpack/security/api_key/query"; | ||
public static final QueryApiKeyAction INSTANCE = new QueryApiKeyAction(); | ||
|
||
private QueryApiKeyAction() { | ||
super(NAME, QueryApiKeyResponse::new); | ||
} | ||
|
||
} |
60 changes: 60 additions & 0 deletions
60
...src/main/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
/* | ||
* 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.core.security.action.apikey; | ||
|
||
import org.elasticsearch.action.ActionRequest; | ||
import org.elasticsearch.action.ActionRequestValidationException; | ||
import org.elasticsearch.common.io.stream.StreamInput; | ||
import org.elasticsearch.common.io.stream.StreamOutput; | ||
import org.elasticsearch.core.Nullable; | ||
import org.elasticsearch.index.query.QueryBuilder; | ||
|
||
import java.io.IOException; | ||
|
||
public final class QueryApiKeyRequest extends ActionRequest { | ||
|
||
@Nullable | ||
private final QueryBuilder queryBuilder; | ||
private boolean filterForCurrentUser; | ||
|
||
public QueryApiKeyRequest() { | ||
this((QueryBuilder) null); | ||
} | ||
|
||
public QueryApiKeyRequest(QueryBuilder queryBuilder) { | ||
this.queryBuilder = queryBuilder; | ||
} | ||
|
||
public QueryApiKeyRequest(StreamInput in) throws IOException { | ||
super(in); | ||
queryBuilder = in.readOptionalNamedWriteable(QueryBuilder.class); | ||
} | ||
|
||
public QueryBuilder getQueryBuilder() { | ||
return queryBuilder; | ||
} | ||
|
||
public boolean isFilterForCurrentUser() { | ||
return filterForCurrentUser; | ||
} | ||
|
||
public void setFilterForCurrentUser() { | ||
filterForCurrentUser = true; | ||
} | ||
|
||
@Override | ||
public ActionRequestValidationException validate() { | ||
return null; | ||
} | ||
|
||
@Override | ||
public void writeTo(StreamOutput out) throws IOException { | ||
super.writeTo(out); | ||
out.writeOptionalNamedWriteable(queryBuilder); | ||
} | ||
} |
82 changes: 82 additions & 0 deletions
82
...rc/main/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/* | ||
* 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.core.security.action.apikey; | ||
|
||
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.xcontent.ToXContentObject; | ||
import org.elasticsearch.common.xcontent.XContentBuilder; | ||
import org.elasticsearch.xpack.core.security.action.ApiKey; | ||
|
||
import java.io.IOException; | ||
import java.util.Arrays; | ||
import java.util.Collection; | ||
import java.util.Collections; | ||
import java.util.Objects; | ||
|
||
/** | ||
* Response for search API keys.<br> | ||
* The result contains information about the API keys that were found. | ||
*/ | ||
public final class QueryApiKeyResponse extends ActionResponse implements ToXContentObject, Writeable { | ||
|
||
private final ApiKey[] foundApiKeysInfo; | ||
|
||
public QueryApiKeyResponse(StreamInput in) throws IOException { | ||
super(in); | ||
this.foundApiKeysInfo = in.readArray(ApiKey::new, ApiKey[]::new); | ||
} | ||
|
||
public QueryApiKeyResponse(Collection<ApiKey> foundApiKeysInfo) { | ||
Objects.requireNonNull(foundApiKeysInfo, "found_api_keys_info must be provided"); | ||
this.foundApiKeysInfo = foundApiKeysInfo.toArray(new ApiKey[0]); | ||
} | ||
|
||
public static QueryApiKeyResponse emptyResponse() { | ||
return new QueryApiKeyResponse(Collections.emptyList()); | ||
} | ||
|
||
public ApiKey[] getApiKeyInfos() { | ||
return foundApiKeysInfo; | ||
} | ||
|
||
@Override | ||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { | ||
builder.startObject() | ||
.array("api_keys", (Object[]) foundApiKeysInfo); | ||
return builder.endObject(); | ||
} | ||
|
||
@Override | ||
public void writeTo(StreamOutput out) throws IOException { | ||
out.writeArray(foundApiKeysInfo); | ||
} | ||
|
||
@Override | ||
public boolean equals(Object o) { | ||
if (this == o) | ||
return true; | ||
if (o == null || getClass() != o.getClass()) | ||
return false; | ||
QueryApiKeyResponse that = (QueryApiKeyResponse) o; | ||
return Arrays.equals(foundApiKeysInfo, that.foundApiKeysInfo); | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
return Arrays.hashCode(foundApiKeysInfo); | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return "QueryApiKeyResponse [foundApiKeysInfo=" + foundApiKeysInfo + "]"; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
61 changes: 61 additions & 0 deletions
61
...est/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyRequestTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
/* | ||
* 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.core.security.action.apikey; | ||
|
||
import org.elasticsearch.common.io.stream.BytesStreamOutput; | ||
import org.elasticsearch.common.io.stream.InputStreamStreamInput; | ||
import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; | ||
import org.elasticsearch.common.io.stream.NamedWriteableRegistry; | ||
import org.elasticsearch.common.io.stream.StreamInput; | ||
import org.elasticsearch.common.settings.Settings; | ||
import org.elasticsearch.index.query.BoolQueryBuilder; | ||
import org.elasticsearch.index.query.QueryBuilders; | ||
import org.elasticsearch.search.SearchModule; | ||
import org.elasticsearch.test.ESTestCase; | ||
|
||
import java.io.ByteArrayInputStream; | ||
import java.io.IOException; | ||
import java.util.List; | ||
|
||
import static org.hamcrest.Matchers.equalTo; | ||
import static org.hamcrest.Matchers.is; | ||
import static org.hamcrest.Matchers.nullValue; | ||
|
||
public class QueryApiKeyRequestTests extends ESTestCase { | ||
|
||
@Override | ||
protected NamedWriteableRegistry writableRegistry() { | ||
final SearchModule searchModule = new SearchModule(Settings.EMPTY, List.of()); | ||
return new NamedWriteableRegistry(searchModule.getNamedWriteables()); | ||
} | ||
|
||
public void testReadWrite() throws IOException { | ||
final QueryApiKeyRequest request1 = new QueryApiKeyRequest(); | ||
try (BytesStreamOutput out = new BytesStreamOutput()) { | ||
request1.writeTo(out); | ||
try (StreamInput in = new InputStreamStreamInput(new ByteArrayInputStream(out.bytes().array()))) { | ||
assertThat(new QueryApiKeyRequest(in).getQueryBuilder(), nullValue()); | ||
} | ||
} | ||
|
||
final BoolQueryBuilder boolQueryBuilder2 = QueryBuilders.boolQuery() | ||
.filter(QueryBuilders.termQuery("foo", "bar")) | ||
.should(QueryBuilders.idsQuery().addIds("id1", "id2")) | ||
.must(QueryBuilders.wildcardQuery("a.b", "t*y")) | ||
.mustNot(QueryBuilders.prefixQuery("value", "prod")); | ||
final QueryApiKeyRequest request2 = new QueryApiKeyRequest(boolQueryBuilder2); | ||
try (BytesStreamOutput out = new BytesStreamOutput()) { | ||
request2.writeTo(out); | ||
try (StreamInput in = new NamedWriteableAwareStreamInput(out.bytes().streamInput(), writableRegistry())) { | ||
final QueryApiKeyRequest deserialized = new QueryApiKeyRequest(in); | ||
assertThat(deserialized.getQueryBuilder().getClass(), is(BoolQueryBuilder.class)); | ||
assertThat((BoolQueryBuilder) deserialized.getQueryBuilder(), equalTo(boolQueryBuilder2)); | ||
} | ||
} | ||
} | ||
} |
72 changes: 72 additions & 0 deletions
72
...st/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyResponseTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
/* | ||
* 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.core.security.action.apikey; | ||
|
||
import org.elasticsearch.common.io.stream.Writeable; | ||
import org.elasticsearch.test.AbstractWireSerializingTestCase; | ||
import org.elasticsearch.xpack.core.security.action.ApiKey; | ||
import org.elasticsearch.xpack.core.security.action.ApiKeyTests; | ||
|
||
import java.io.IOException; | ||
import java.time.Instant; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.stream.Collectors; | ||
|
||
public class QueryApiKeyResponseTests extends AbstractWireSerializingTestCase<QueryApiKeyResponse> { | ||
|
||
@Override | ||
protected Writeable.Reader<QueryApiKeyResponse> instanceReader() { | ||
return QueryApiKeyResponse::new; | ||
} | ||
|
||
@Override | ||
protected QueryApiKeyResponse createTestInstance() { | ||
final List<ApiKey> apiKeys = randomList(0, 3, this::randomApiKeyInfo); | ||
return new QueryApiKeyResponse(apiKeys); | ||
} | ||
|
||
@Override | ||
protected QueryApiKeyResponse mutateInstance(QueryApiKeyResponse instance) throws IOException { | ||
final ArrayList<ApiKey> apiKeyInfos = | ||
Arrays.stream(instance.getApiKeyInfos()).collect(Collectors.toCollection(ArrayList::new)); | ||
switch (randomIntBetween(0, 2)) { | ||
case 0: | ||
apiKeyInfos.add(randomApiKeyInfo()); | ||
return new QueryApiKeyResponse(apiKeyInfos); | ||
case 1: | ||
if (false == apiKeyInfos.isEmpty()) { | ||
return new QueryApiKeyResponse(apiKeyInfos.subList(1, apiKeyInfos.size())); | ||
} else { | ||
apiKeyInfos.add(randomApiKeyInfo()); | ||
return new QueryApiKeyResponse(apiKeyInfos); | ||
} | ||
default: | ||
if (false == apiKeyInfos.isEmpty()) { | ||
final int index = randomIntBetween(0, apiKeyInfos.size() - 1); | ||
apiKeyInfos.set(index, randomApiKeyInfo()); | ||
} else { | ||
apiKeyInfos.add(randomApiKeyInfo()); | ||
} | ||
return new QueryApiKeyResponse(apiKeyInfos); | ||
} | ||
} | ||
|
||
private ApiKey randomApiKeyInfo() { | ||
final String name = randomAlphaOfLengthBetween(3, 8); | ||
final String id = randomAlphaOfLength(22); | ||
final String username = randomAlphaOfLengthBetween(3, 8); | ||
final String realm_name = randomAlphaOfLengthBetween(3, 8); | ||
final Instant creation = Instant.ofEpochMilli(randomMillisUpToYear9999()); | ||
final Instant expiration = randomBoolean() ? Instant.ofEpochMilli(randomMillisUpToYear9999()) : null; | ||
final Map<String, Object> metadata = ApiKeyTests.randomMetadata(); | ||
return new ApiKey(name, id, creation, expiration, false, username, realm_name, metadata); | ||
} | ||
} |
Oops, something went wrong.