Skip to content

Commit

Permalink
Support metadata on API keys (#70292)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ywangd authored Mar 28, 2021
1 parent 977ecd6 commit 3725cb5
Show file tree
Hide file tree
Showing 26 changed files with 732 additions and 229 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
Expand All @@ -28,19 +29,28 @@ public final class CreateApiKeyRequest implements Validatable, ToXContentObject
private final TimeValue expiration;
private final List<Role> roles;
private final RefreshPolicy refreshPolicy;
private final Map<String, Object> metadata;

/**
* Create API Key request constructor
* @param name name for the API key
* @param roles list of {@link Role}s
* @param expiration to specify expiration for the API key
* @param metadata Arbitrary metadata for the API key
*/
public CreateApiKeyRequest(String name, List<Role> roles, @Nullable TimeValue expiration,
@Nullable final RefreshPolicy refreshPolicy) {
@Nullable final RefreshPolicy refreshPolicy,
@Nullable Map<String, Object> metadata) {
this.name = name;
this.roles = Objects.requireNonNull(roles, "roles may not be null");
this.expiration = expiration;
this.refreshPolicy = (refreshPolicy == null) ? RefreshPolicy.getDefault() : refreshPolicy;
this.metadata = metadata;
}

public CreateApiKeyRequest(String name, List<Role> roles, @Nullable TimeValue expiration,
@Nullable final RefreshPolicy refreshPolicy) {
this(name, roles, expiration, refreshPolicy, null);
}

public String getName() {
Expand All @@ -59,9 +69,13 @@ public RefreshPolicy getRefreshPolicy() {
return refreshPolicy;
}

public Map<String, Object> getMetadata() {
return metadata;
}

@Override
public int hashCode() {
return Objects.hash(name, refreshPolicy, roles, expiration);
return Objects.hash(name, refreshPolicy, roles, expiration, metadata);
}

@Override
Expand All @@ -74,7 +88,7 @@ public boolean equals(Object o) {
}
final CreateApiKeyRequest that = (CreateApiKeyRequest) o;
return Objects.equals(name, that.name) && Objects.equals(refreshPolicy, that.refreshPolicy) && Objects.equals(roles, that.roles)
&& Objects.equals(expiration, that.expiration);
&& Objects.equals(expiration, that.expiration) && Objects.equals(metadata, that.metadata);
}

@Override
Expand Down Expand Up @@ -107,6 +121,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
builder.endObject();
}
builder.endObject();
if (metadata != null) {
builder.field("metadata", metadata);
}
return builder.endObject();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import java.io.IOException;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;

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

public ApiKey(String name, String id, Instant creation, Instant expiration, boolean invalidated, String username, String realm) {
public ApiKey(String name, String id, Instant creation, Instant expiration, boolean invalidated, String username, String realm,
Map<String, Object> metadata) {
this.name = name;
this.id = id;
// As we do not yet support the nanosecond precision when we serialize to JSON,
Expand All @@ -44,6 +47,7 @@ public ApiKey(String name, String id, Instant creation, Instant expiration, bool
this.invalidated = invalidated;
this.username = username;
this.realm = realm;
this.metadata = metadata;
}

public String getId() {
Expand Down Expand Up @@ -90,9 +94,13 @@ public String getRealm() {
return realm;
}

public Map<String, Object> getMetadata() {
return metadata;
}

@Override
public int hashCode() {
return Objects.hash(name, id, creation, expiration, invalidated, username, realm);
return Objects.hash(name, id, creation, expiration, invalidated, username, realm, metadata);
}

@Override
Expand All @@ -113,12 +121,15 @@ public boolean equals(Object obj) {
&& Objects.equals(expiration, other.expiration)
&& Objects.equals(invalidated, other.invalidated)
&& Objects.equals(username, other.username)
&& Objects.equals(realm, other.realm);
&& Objects.equals(realm, other.realm)
&& Objects.equals(metadata, other.metadata);
}

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

public static ApiKey fromXContent(XContentParser parser) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.apache.http.client.methods.HttpPut;
import org.elasticsearch.client.security.ChangePasswordRequest;
import org.elasticsearch.client.security.CreateApiKeyRequest;
import org.elasticsearch.client.security.CreateApiKeyRequestTests;
import org.elasticsearch.client.security.CreateTokenRequest;
import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest;
import org.elasticsearch.client.security.DeletePrivilegesRequest;
Expand Down Expand Up @@ -449,7 +450,8 @@ private CreateApiKeyRequest buildCreateApiKeyRequest() {
.indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build());
final TimeValue expiration = randomBoolean() ? null : TimeValue.timeValueHours(24);
final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy);
final Map<String, Object> metadata = CreateApiKeyRequestTests.randomMetadata();
final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy, metadata);
return createApiKeyRequest;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.elasticsearch.client.security.ClearRolesCacheResponse;
import org.elasticsearch.client.security.ClearSecurityCacheResponse;
import org.elasticsearch.client.security.CreateApiKeyRequest;
import org.elasticsearch.client.security.CreateApiKeyRequestTests;
import org.elasticsearch.client.security.CreateApiKeyResponse;
import org.elasticsearch.client.security.CreateTokenRequest;
import org.elasticsearch.client.security.CreateTokenResponse;
Expand Down Expand Up @@ -1957,10 +1958,11 @@ public void testCreateApiKey() throws Exception {
.indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build());
final TimeValue expiration = TimeValue.timeValueHours(24);
final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
final Map<String, Object> metadata = CreateApiKeyRequestTests.randomMetadata();
{
final String name = randomAlphaOfLength(5);
// tag::create-api-key-request
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy);
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy, metadata);
// end::create-api-key-request

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

{
final String name = randomAlphaOfLength(5);
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy);
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy, metadata);

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


final Instant start = Instant.now();
final Map<String, Object> metadata = CreateApiKeyRequestTests.randomMetadata();
CheckedConsumer<CreateApiKeyResponse, IOException> apiKeyVerifier = (created) -> {
final GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(created.getId(), false);
final GetApiKeyResponse getApiKeyResponse = client.security().getApiKey(getApiKeyRequest, RequestOptions.DEFAULT);
Expand All @@ -2039,14 +2042,19 @@ public void testGrantApiKey() throws Exception {
assertThat(apiKeyInfo.isInvalidated(), equalTo(false));
assertThat(apiKeyInfo.getCreation(), greaterThanOrEqualTo(start));
assertThat(apiKeyInfo.getCreation(), lessThanOrEqualTo(Instant.now()));
if (metadata == null) {
assertThat(apiKeyInfo.getMetadata(), equalTo(Map.of()));
} else {
assertThat(apiKeyInfo.getMetadata(), equalTo(metadata));
}
};

final TimeValue expiration = TimeValue.timeValueHours(24);
final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
{
final String name = randomAlphaOfLength(5);
// tag::grant-api-key-request
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy);
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy, metadata);
GrantApiKeyRequest.Grant grant = GrantApiKeyRequest.Grant.passwordGrant(username, password);
GrantApiKeyRequest grantApiKeyRequest = new GrantApiKeyRequest(grant, createApiKeyRequest);
// end::grant-api-key-request
Expand All @@ -2071,7 +2079,7 @@ public void testGrantApiKey() throws Exception {
final CreateTokenRequest tokenRequest = CreateTokenRequest.passwordGrant(username, password);
final CreateTokenResponse token = client.security().createToken(tokenRequest, RequestOptions.DEFAULT);

CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy);
CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy, metadata);
GrantApiKeyRequest.Grant grant = GrantApiKeyRequest.Grant.accessTokenGrant(token.getAccessToken());
GrantApiKeyRequest grantApiKeyRequest = new GrantApiKeyRequest(grant, createApiKeyRequest);

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

final ApiKey expectedApiKeyInfo = new ApiKey(createApiKeyResponse1.getName(), createApiKeyResponse1.getId(), Instant.now(),
Instant.now().plusMillis(expiration.getMillis()), false, "test_user", "default_file");
Instant.now().plusMillis(expiration.getMillis()), false, "test_user", "default_file", metadata);
{
// tag::get-api-key-id-request
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId(), false);
Expand Down Expand Up @@ -2258,6 +2267,11 @@ private void verifyApiKey(final ApiKey actual, final ApiKey expected) {
assertThat(actual.getRealm(), is(expected.getRealm()));
assertThat(actual.isInvalidated(), is(expected.isInvalidated()));
assertThat(actual.getExpiration(), is(greaterThan(Instant.now())));
if (expected.getMetadata() == null) {
assertThat(actual.getMetadata(), equalTo(Map.of()));
} else {
assertThat(actual.getMetadata(), equalTo(expected.getMetadata()));
}
}

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

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

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

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

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

{
createApiKeyRequest = new CreateApiKeyRequest("k6", roles, expiration, refreshPolicy);
createApiKeyRequest = new CreateApiKeyRequest("k6", roles, expiration, refreshPolicy, metadata);
CreateApiKeyResponse createApiKeyResponse6 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT);
assertThat(createApiKeyResponse6.getName(), equalTo("k6"));
assertNotNull(createApiKeyResponse6.getKey());
Expand Down Expand Up @@ -2450,7 +2465,7 @@ public void onFailure(Exception e) {
}

{
createApiKeyRequest = new CreateApiKeyRequest("k7", roles, expiration, refreshPolicy);
createApiKeyRequest = new CreateApiKeyRequest("k7", roles, expiration, refreshPolicy, metadata);
CreateApiKeyResponse createApiKeyResponse7 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT);
assertThat(createApiKeyResponse7.getName(), equalTo("k7"));
assertNotNull(createApiKeyResponse7.getKey());
Expand Down
Loading

0 comments on commit 3725cb5

Please sign in to comment.