Skip to content

Commit 3725cb5

Browse files
authored
Support metadata on API keys (#70292)
This PR adds metadata support for API keys. Metadata are of type Map<String, Object> and can be optionally provided at API key creation time. It is returned as part of GetApiKey response. It is also stored as part of the authentication object to transfer throw the wire. Note that it is not yet searchable and not exposed to any ingest processors. They will be handled by separate PRs.
1 parent 977ecd6 commit 3725cb5

File tree

26 files changed

+732
-229
lines changed

26 files changed

+732
-229
lines changed

client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.io.IOException;
1919
import java.util.List;
20+
import java.util.Map;
2021
import java.util.Objects;
2122

2223
/**
@@ -28,19 +29,28 @@ public final class CreateApiKeyRequest implements Validatable, ToXContentObject
2829
private final TimeValue expiration;
2930
private final List<Role> roles;
3031
private final RefreshPolicy refreshPolicy;
32+
private final Map<String, Object> metadata;
3133

3234
/**
3335
* Create API Key request constructor
3436
* @param name name for the API key
3537
* @param roles list of {@link Role}s
3638
* @param expiration to specify expiration for the API key
39+
* @param metadata Arbitrary metadata for the API key
3740
*/
3841
public CreateApiKeyRequest(String name, List<Role> roles, @Nullable TimeValue expiration,
39-
@Nullable final RefreshPolicy refreshPolicy) {
42+
@Nullable final RefreshPolicy refreshPolicy,
43+
@Nullable Map<String, Object> metadata) {
4044
this.name = name;
4145
this.roles = Objects.requireNonNull(roles, "roles may not be null");
4246
this.expiration = expiration;
4347
this.refreshPolicy = (refreshPolicy == null) ? RefreshPolicy.getDefault() : refreshPolicy;
48+
this.metadata = metadata;
49+
}
50+
51+
public CreateApiKeyRequest(String name, List<Role> roles, @Nullable TimeValue expiration,
52+
@Nullable final RefreshPolicy refreshPolicy) {
53+
this(name, roles, expiration, refreshPolicy, null);
4454
}
4555

4656
public String getName() {
@@ -59,9 +69,13 @@ public RefreshPolicy getRefreshPolicy() {
5969
return refreshPolicy;
6070
}
6171

72+
public Map<String, Object> getMetadata() {
73+
return metadata;
74+
}
75+
6276
@Override
6377
public int hashCode() {
64-
return Objects.hash(name, refreshPolicy, roles, expiration);
78+
return Objects.hash(name, refreshPolicy, roles, expiration, metadata);
6579
}
6680

6781
@Override
@@ -74,7 +88,7 @@ public boolean equals(Object o) {
7488
}
7589
final CreateApiKeyRequest that = (CreateApiKeyRequest) o;
7690
return Objects.equals(name, that.name) && Objects.equals(refreshPolicy, that.refreshPolicy) && Objects.equals(roles, that.roles)
77-
&& Objects.equals(expiration, that.expiration);
91+
&& Objects.equals(expiration, that.expiration) && Objects.equals(metadata, that.metadata);
7892
}
7993

8094
@Override
@@ -107,6 +121,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
107121
builder.endObject();
108122
}
109123
builder.endObject();
124+
if (metadata != null) {
125+
builder.field("metadata", metadata);
126+
}
110127
return builder.endObject();
111128
}
112129

client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import java.io.IOException;
1717
import java.time.Instant;
18+
import java.util.Map;
1819
import java.util.Objects;
1920

2021
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
@@ -32,8 +33,10 @@ public final class ApiKey {
3233
private final boolean invalidated;
3334
private final String username;
3435
private final String realm;
36+
private final Map<String, Object> metadata;
3537

36-
public ApiKey(String name, String id, Instant creation, Instant expiration, boolean invalidated, String username, String realm) {
38+
public ApiKey(String name, String id, Instant creation, Instant expiration, boolean invalidated, String username, String realm,
39+
Map<String, Object> metadata) {
3740
this.name = name;
3841
this.id = id;
3942
// As we do not yet support the nanosecond precision when we serialize to JSON,
@@ -44,6 +47,7 @@ public ApiKey(String name, String id, Instant creation, Instant expiration, bool
4447
this.invalidated = invalidated;
4548
this.username = username;
4649
this.realm = realm;
50+
this.metadata = metadata;
4751
}
4852

4953
public String getId() {
@@ -90,9 +94,13 @@ public String getRealm() {
9094
return realm;
9195
}
9296

97+
public Map<String, Object> getMetadata() {
98+
return metadata;
99+
}
100+
93101
@Override
94102
public int hashCode() {
95-
return Objects.hash(name, id, creation, expiration, invalidated, username, realm);
103+
return Objects.hash(name, id, creation, expiration, invalidated, username, realm, metadata);
96104
}
97105

98106
@Override
@@ -113,12 +121,15 @@ public boolean equals(Object obj) {
113121
&& Objects.equals(expiration, other.expiration)
114122
&& Objects.equals(invalidated, other.invalidated)
115123
&& Objects.equals(username, other.username)
116-
&& Objects.equals(realm, other.realm);
124+
&& Objects.equals(realm, other.realm)
125+
&& Objects.equals(metadata, other.metadata);
117126
}
118127

128+
@SuppressWarnings("unchecked")
119129
static final ConstructingObjectParser<ApiKey, Void> PARSER = new ConstructingObjectParser<>("api_key", args -> {
120130
return new ApiKey((String) args[0], (String) args[1], Instant.ofEpochMilli((Long) args[2]),
121-
(args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3]), (Boolean) args[4], (String) args[5], (String) args[6]);
131+
(args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3]), (Boolean) args[4], (String) args[5], (String) args[6],
132+
(Map<String, Object>) args[7]);
122133
});
123134
static {
124135
PARSER.declareField(optionalConstructorArg(), (p, c) -> p.textOrNull(), new ParseField("name"),
@@ -129,6 +140,7 @@ public boolean equals(Object obj) {
129140
PARSER.declareBoolean(constructorArg(), new ParseField("invalidated"));
130141
PARSER.declareString(constructorArg(), new ParseField("username"));
131142
PARSER.declareString(constructorArg(), new ParseField("realm"));
143+
PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField("metadata"));
132144
}
133145

134146
public static ApiKey fromXContent(XContentParser parser) throws IOException {

client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.apache.http.client.methods.HttpPut;
1515
import org.elasticsearch.client.security.ChangePasswordRequest;
1616
import org.elasticsearch.client.security.CreateApiKeyRequest;
17+
import org.elasticsearch.client.security.CreateApiKeyRequestTests;
1718
import org.elasticsearch.client.security.CreateTokenRequest;
1819
import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest;
1920
import org.elasticsearch.client.security.DeletePrivilegesRequest;
@@ -449,7 +450,8 @@ private CreateApiKeyRequest buildCreateApiKeyRequest() {
449450
.indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build());
450451
final TimeValue expiration = randomBoolean() ? null : TimeValue.timeValueHours(24);
451452
final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
452-
final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy);
453+
final Map<String, Object> metadata = CreateApiKeyRequestTests.randomMetadata();
454+
final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy, metadata);
453455
return createApiKeyRequest;
454456
}
455457

client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.elasticsearch.client.security.ClearRolesCacheResponse;
2929
import org.elasticsearch.client.security.ClearSecurityCacheResponse;
3030
import org.elasticsearch.client.security.CreateApiKeyRequest;
31+
import org.elasticsearch.client.security.CreateApiKeyRequestTests;
3132
import org.elasticsearch.client.security.CreateApiKeyResponse;
3233
import org.elasticsearch.client.security.CreateTokenRequest;
3334
import org.elasticsearch.client.security.CreateTokenResponse;
@@ -1957,10 +1958,11 @@ public void testCreateApiKey() throws Exception {
19571958
.indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build());
19581959
final TimeValue expiration = TimeValue.timeValueHours(24);
19591960
final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
1961+
final Map<String, Object> metadata = CreateApiKeyRequestTests.randomMetadata();
19601962
{
19611963
final String name = randomAlphaOfLength(5);
19621964
// tag::create-api-key-request
1963-
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy);
1965+
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy, metadata);
19641966
// end::create-api-key-request
19651967

19661968
// tag::create-api-key-execute
@@ -1978,7 +1980,7 @@ public void testCreateApiKey() throws Exception {
19781980

19791981
{
19801982
final String name = randomAlphaOfLength(5);
1981-
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy);
1983+
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy, metadata);
19821984

19831985
ActionListener<CreateApiKeyResponse> listener;
19841986
// tag::create-api-key-execute-listener
@@ -2027,6 +2029,7 @@ public void testGrantApiKey() throws Exception {
20272029

20282030

20292031
final Instant start = Instant.now();
2032+
final Map<String, Object> metadata = CreateApiKeyRequestTests.randomMetadata();
20302033
CheckedConsumer<CreateApiKeyResponse, IOException> apiKeyVerifier = (created) -> {
20312034
final GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(created.getId(), false);
20322035
final GetApiKeyResponse getApiKeyResponse = client.security().getApiKey(getApiKeyRequest, RequestOptions.DEFAULT);
@@ -2039,14 +2042,19 @@ public void testGrantApiKey() throws Exception {
20392042
assertThat(apiKeyInfo.isInvalidated(), equalTo(false));
20402043
assertThat(apiKeyInfo.getCreation(), greaterThanOrEqualTo(start));
20412044
assertThat(apiKeyInfo.getCreation(), lessThanOrEqualTo(Instant.now()));
2045+
if (metadata == null) {
2046+
assertThat(apiKeyInfo.getMetadata(), equalTo(Map.of()));
2047+
} else {
2048+
assertThat(apiKeyInfo.getMetadata(), equalTo(metadata));
2049+
}
20422050
};
20432051

20442052
final TimeValue expiration = TimeValue.timeValueHours(24);
20452053
final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
20462054
{
20472055
final String name = randomAlphaOfLength(5);
20482056
// tag::grant-api-key-request
2049-
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy);
2057+
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy, metadata);
20502058
GrantApiKeyRequest.Grant grant = GrantApiKeyRequest.Grant.passwordGrant(username, password);
20512059
GrantApiKeyRequest grantApiKeyRequest = new GrantApiKeyRequest(grant, createApiKeyRequest);
20522060
// end::grant-api-key-request
@@ -2071,7 +2079,7 @@ public void testGrantApiKey() throws Exception {
20712079
final CreateTokenRequest tokenRequest = CreateTokenRequest.passwordGrant(username, password);
20722080
final CreateTokenResponse token = client.security().createToken(tokenRequest, RequestOptions.DEFAULT);
20732081

2074-
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy);
2082+
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy, metadata);
20752083
GrantApiKeyRequest.Grant grant = GrantApiKeyRequest.Grant.accessTokenGrant(token.getAccessToken());
20762084
GrantApiKeyRequest grantApiKeyRequest = new GrantApiKeyRequest(grant, createApiKeyRequest);
20772085

@@ -2117,14 +2125,15 @@ public void testGetApiKey() throws Exception {
21172125
.indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build());
21182126
final TimeValue expiration = TimeValue.timeValueHours(24);
21192127
final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
2128+
final Map<String, Object> metadata = CreateApiKeyRequestTests.randomMetadata();
21202129
// Create API Keys
2121-
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest("k1", roles, expiration, refreshPolicy);
2130+
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest("k1", roles, expiration, refreshPolicy, metadata);
21222131
CreateApiKeyResponse createApiKeyResponse1 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT);
21232132
assertThat(createApiKeyResponse1.getName(), equalTo("k1"));
21242133
assertNotNull(createApiKeyResponse1.getKey());
21252134

21262135
final ApiKey expectedApiKeyInfo = new ApiKey(createApiKeyResponse1.getName(), createApiKeyResponse1.getId(), Instant.now(),
2127-
Instant.now().plusMillis(expiration.getMillis()), false, "test_user", "default_file");
2136+
Instant.now().plusMillis(expiration.getMillis()), false, "test_user", "default_file", metadata);
21282137
{
21292138
// tag::get-api-key-id-request
21302139
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId(), false);
@@ -2258,6 +2267,11 @@ private void verifyApiKey(final ApiKey actual, final ApiKey expected) {
22582267
assertThat(actual.getRealm(), is(expected.getRealm()));
22592268
assertThat(actual.isInvalidated(), is(expected.isInvalidated()));
22602269
assertThat(actual.getExpiration(), is(greaterThan(Instant.now())));
2270+
if (expected.getMetadata() == null) {
2271+
assertThat(actual.getMetadata(), equalTo(Map.of()));
2272+
} else {
2273+
assertThat(actual.getMetadata(), equalTo(expected.getMetadata()));
2274+
}
22612275
}
22622276

22632277
public void testInvalidateApiKey() throws Exception {
@@ -2267,8 +2281,9 @@ public void testInvalidateApiKey() throws Exception {
22672281
.indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build());
22682282
final TimeValue expiration = TimeValue.timeValueHours(24);
22692283
final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
2284+
final Map<String, Object> metadata = CreateApiKeyRequestTests.randomMetadata();
22702285
// Create API Keys
2271-
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest("k1", roles, expiration, refreshPolicy);
2286+
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest("k1", roles, expiration, refreshPolicy, metadata);
22722287
CreateApiKeyResponse createApiKeyResponse1 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT);
22732288
assertThat(createApiKeyResponse1.getName(), equalTo("k1"));
22742289
assertNotNull(createApiKeyResponse1.getKey());
@@ -2312,7 +2327,7 @@ public void testInvalidateApiKey() throws Exception {
23122327
}
23132328

23142329
{
2315-
createApiKeyRequest = new CreateApiKeyRequest("k2", roles, expiration, refreshPolicy);
2330+
createApiKeyRequest = new CreateApiKeyRequest("k2", roles, expiration, refreshPolicy, metadata);
23162331
CreateApiKeyResponse createApiKeyResponse2 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT);
23172332
assertThat(createApiKeyResponse2.getName(), equalTo("k2"));
23182333
assertNotNull(createApiKeyResponse2.getKey());
@@ -2336,7 +2351,7 @@ public void testInvalidateApiKey() throws Exception {
23362351
}
23372352

23382353
{
2339-
createApiKeyRequest = new CreateApiKeyRequest("k3", roles, expiration, refreshPolicy);
2354+
createApiKeyRequest = new CreateApiKeyRequest("k3", roles, expiration, refreshPolicy, metadata);
23402355
CreateApiKeyResponse createApiKeyResponse3 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT);
23412356
assertThat(createApiKeyResponse3.getName(), equalTo("k3"));
23422357
assertNotNull(createApiKeyResponse3.getKey());
@@ -2359,7 +2374,7 @@ public void testInvalidateApiKey() throws Exception {
23592374
}
23602375

23612376
{
2362-
createApiKeyRequest = new CreateApiKeyRequest("k4", roles, expiration, refreshPolicy);
2377+
createApiKeyRequest = new CreateApiKeyRequest("k4", roles, expiration, refreshPolicy, metadata);
23632378
CreateApiKeyResponse createApiKeyResponse4 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT);
23642379
assertThat(createApiKeyResponse4.getName(), equalTo("k4"));
23652380
assertNotNull(createApiKeyResponse4.getKey());
@@ -2382,7 +2397,7 @@ public void testInvalidateApiKey() throws Exception {
23822397
}
23832398

23842399
{
2385-
createApiKeyRequest = new CreateApiKeyRequest("k5", roles, expiration, refreshPolicy);
2400+
createApiKeyRequest = new CreateApiKeyRequest("k5", roles, expiration, refreshPolicy, metadata);
23862401
CreateApiKeyResponse createApiKeyResponse5 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT);
23872402
assertThat(createApiKeyResponse5.getName(), equalTo("k5"));
23882403
assertNotNull(createApiKeyResponse5.getKey());
@@ -2407,7 +2422,7 @@ public void testInvalidateApiKey() throws Exception {
24072422
}
24082423

24092424
{
2410-
createApiKeyRequest = new CreateApiKeyRequest("k6", roles, expiration, refreshPolicy);
2425+
createApiKeyRequest = new CreateApiKeyRequest("k6", roles, expiration, refreshPolicy, metadata);
24112426
CreateApiKeyResponse createApiKeyResponse6 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT);
24122427
assertThat(createApiKeyResponse6.getName(), equalTo("k6"));
24132428
assertNotNull(createApiKeyResponse6.getKey());
@@ -2450,7 +2465,7 @@ public void onFailure(Exception e) {
24502465
}
24512466

24522467
{
2453-
createApiKeyRequest = new CreateApiKeyRequest("k7", roles, expiration, refreshPolicy);
2468+
createApiKeyRequest = new CreateApiKeyRequest("k7", roles, expiration, refreshPolicy, metadata);
24542469
CreateApiKeyResponse createApiKeyResponse7 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT);
24552470
assertThat(createApiKeyResponse7.getName(), equalTo("k7"));
24562471
assertNotNull(createApiKeyResponse7.getKey());

0 commit comments

Comments
 (0)