From 38902a998313893ab644d213f199857c9b43cc40 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Wed, 10 Mar 2021 16:32:45 +1100 Subject: [PATCH 01/16] wip --- .../common/xcontent/ObjectParserHelper.java | 15 ++++++-- .../xpack/core/security/action/ApiKey.java | 28 ++++++++++++--- .../security/action/CreateApiKeyRequest.java | 25 ++++++++++++++ .../action/CreateApiKeyRequestBuilder.java | 12 ++++++- .../xpack/security/Security.java | 4 +++ .../xpack/security/authc/ApiKeyService.java | 34 +++++++++++++++---- .../ingest/SetSecurityUserProcessor.java | 12 +++++++ .../security/authc/ApiKeyServiceTests.java | 4 +-- 8 files changed, 118 insertions(+), 16 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/xcontent/ObjectParserHelper.java b/server/src/main/java/org/elasticsearch/common/xcontent/ObjectParserHelper.java index 49345db15664d..c3e7a6c103136 100644 --- a/server/src/main/java/org/elasticsearch/common/xcontent/ObjectParserHelper.java +++ b/server/src/main/java/org/elasticsearch/common/xcontent/ObjectParserHelper.java @@ -29,13 +29,24 @@ public final class ObjectParserHelper { public void declareRawObject(final AbstractObjectParser parser, final BiConsumer consumer, final ParseField field) { - final CheckedFunction bytesParser = p -> { + final CheckedFunction bytesParser = getBytesParser(); + parser.declareField(consumer, bytesParser, field, ValueType.OBJECT); + } + + public void declareRawObjectOrNull(final AbstractObjectParser parser, + final BiConsumer consumer, + final ParseField field) { + final CheckedFunction bytesParser = getBytesParser(); + parser.declareField(consumer, bytesParser, field, ValueType.OBJECT_OR_NULL); + } + + private CheckedFunction getBytesParser() { + return p -> { try (XContentBuilder builder = JsonXContent.contentBuilder()) { builder.copyCurrentStructure(p); return BytesReference.bytes(builder); } }; - parser.declareField(consumer, bytesParser, field, ValueType.OBJECT); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java index 86ff8ce8ad22c..8cbc4c20b3bb8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java @@ -19,6 +19,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; @@ -36,8 +37,10 @@ public final class ApiKey implements ToXContentObject, Writeable { private final boolean invalidated; private final String username; private final String realm; + private final Map 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 metadata) { this.name = name; this.id = id; // As we do not yet support the nanosecond precision when we serialize to JSON, @@ -48,6 +51,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 ApiKey(StreamInput in) throws IOException { @@ -62,6 +66,11 @@ public ApiKey(StreamInput in) throws IOException { this.invalidated = in.readBoolean(); this.username = in.readString(); this.realm = in.readString(); + if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + this.metadata = in.readMap(); + } else { + this.metadata = Map.of(); + } } public String getId() { @@ -92,6 +101,10 @@ public String getRealm() { return realm; } + public Map getMetadata() { + return metadata; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject() @@ -103,7 +116,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } builder.field("invalidated", invalidated) .field("username", username) - .field("realm", realm); + .field("realm", realm) + .field("metadata", metadata); return builder.endObject(); } @@ -120,6 +134,9 @@ public void writeTo(StreamOutput out) throws IOException { out.writeBoolean(invalidated); out.writeString(username); out.writeString(realm); + if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + out.writeMap(metadata); + } } @Override @@ -148,9 +165,11 @@ public boolean equals(Object obj) { && Objects.equals(realm, other.realm); } + @SuppressWarnings("unchecked") static final ConstructingObjectParser 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) args[7]); }); static { PARSER.declareString(constructorArg(), new ParseField("name")); @@ -160,6 +179,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(constructorArg(), (p, c) -> p.map(), new ParseField("metadata")); } public static ApiKey fromXContent(XContentParser parser) throws IOException { @@ -169,7 +189,7 @@ public static ApiKey fromXContent(XContentParser parser) throws IOException { @Override public String toString() { return "ApiKey [name=" + name + ", id=" + id + ", creation=" + creation + ", expiration=" + expiration + ", invalidated=" - + invalidated + ", username=" + username + ", realm=" + realm + "]"; + + invalidated + ", username=" + username + ", realm=" + realm + ", metadata=" + metadata + "]"; } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequest.java index ac9d536c3363a..816fbd82dffec 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequest.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import static org.elasticsearch.action.ValidateActions.addValidationError; @@ -36,10 +37,12 @@ public final class CreateApiKeyRequest extends ActionRequest { private final String id; private String name; private TimeValue expiration; + private Map metadata; private List roleDescriptors = Collections.emptyList(); private WriteRequest.RefreshPolicy refreshPolicy = DEFAULT_REFRESH_POLICY; public CreateApiKeyRequest() { + super(); this.id = UUIDs.base64UUID(); // because auditing can currently only catch requests but not responses, // we generate the API key id soonest so it's part of the request body so it is audited } @@ -51,10 +54,16 @@ public CreateApiKeyRequest() { * @param expiration to specify expiration for the API key */ public CreateApiKeyRequest(String name, @Nullable List roleDescriptors, @Nullable TimeValue expiration) { + this(name, roleDescriptors, expiration, null); + } + + public CreateApiKeyRequest(String name, @Nullable List roleDescriptors, @Nullable TimeValue expiration, + @Nullable Map metadata) { this(); this.name = name; this.roleDescriptors = (roleDescriptors == null) ? List.of() : List.copyOf(roleDescriptors); this.expiration = expiration; + this.metadata = metadata; } public CreateApiKeyRequest(StreamInput in) throws IOException { @@ -72,6 +81,11 @@ public CreateApiKeyRequest(StreamInput in) throws IOException { this.expiration = in.readOptionalTimeValue(); this.roleDescriptors = List.copyOf(in.readList(RoleDescriptor::new)); this.refreshPolicy = WriteRequest.RefreshPolicy.readFrom(in); + if (in.getVersion().onOrAfter(Version.V_8_0_0)) { + this.metadata = in.readMap(); + } else { + this.metadata = null; + } } public String getId() { @@ -114,6 +128,14 @@ public void setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) { this.refreshPolicy = Objects.requireNonNull(refreshPolicy, "refresh policy may not be null"); } + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + @Override public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; @@ -147,5 +169,8 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalTimeValue(expiration); out.writeList(roleDescriptors); refreshPolicy.writeTo(out); + if (out.getVersion().onOrAfter(Version.V_8_0_0)) { + out.writeMap(metadata); + } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilder.java index 5772f2e3516c8..38e1b086fc4f4 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilder.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.List; +import java.util.Map; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; @@ -35,7 +36,8 @@ public final class CreateApiKeyRequestBuilder extends ActionRequestBuilder PARSER = new ConstructingObjectParser<>( "api_key_request", false, (args, v) -> { return new CreateApiKeyRequest((String) args[0], (List) args[1], - TimeValue.parseTimeValue((String) args[2], null, "expiration")); + TimeValue.parseTimeValue((String) args[2], null, "expiration"), + (Map) args[3]); }); static { @@ -45,6 +47,7 @@ public final class CreateApiKeyRequestBuilder extends ActionRequestBuilder p.map(), new ParseField("metadata")); } public CreateApiKeyRequestBuilder(ElasticsearchClient client) { @@ -71,6 +74,11 @@ public CreateApiKeyRequestBuilder setRefreshPolicy(WriteRequest.RefreshPolicy re return this; } + public CreateApiKeyRequestBuilder setMetadata(Map metadata) { + request.setMetadata(metadata); + return this; + } + public CreateApiKeyRequestBuilder source(BytesReference source, XContentType xContentType) throws IOException { final NamedXContentRegistry registry = NamedXContentRegistry.EMPTY; try (InputStream stream = source.streamInput(); @@ -79,6 +87,8 @@ public CreateApiKeyRequestBuilder source(BytesReference source, XContentType xCo setName(createApiKeyRequest.getName()); setRoleDescriptors(createApiKeyRequest.getRoleDescriptors()); setExpiration(createApiKeyRequest.getExpiration()); + setMetadata(createApiKeyRequest.getMetadata()); + } return this; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 0eb105ca5c688..101a2260df863 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -1285,6 +1285,10 @@ private static XContentBuilder getIndexMappings() { builder.field("dynamic", false); builder.endObject(); + builder.startObject("metadata_flattened"); + builder.field("type", "flattened"); + builder.endObject(); + builder.startObject("enabled"); builder.field("type", "boolean"); builder.endObject(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 56fda5ae8a0c9..1f3c46018703f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -39,6 +39,7 @@ import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.cache.Cache; import org.elasticsearch.common.cache.CacheBuilder; @@ -140,6 +141,7 @@ public class ApiKeyService { private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(ApiKeyService.class); public static final String API_KEY_ID_KEY = "_security_api_key_id"; public static final String API_KEY_NAME_KEY = "_security_api_key_name"; + public static final String API_KEY_METADATA_KEY = "_security_api_key_metadata"; public static final String API_KEY_REALM_NAME = "_es_api_key"; public static final String API_KEY_REALM_TYPE = "_es_api_key"; public static final String API_KEY_CREATOR_REALM_NAME = "_security_api_key_creator_realm_name"; @@ -261,7 +263,7 @@ private void createApiKeyAndIndexIt(Authentication authentication, CreateApiKeyR final Version version = clusterService.state().nodes().getMinNodeVersion(); try (XContentBuilder builder = newDocument(apiKey, request.getName(), authentication, roleDescriptorSet, created, expiration, - request.getRoleDescriptors(), version)) { + request.getRoleDescriptors(), version, request.getMetadata())) { final IndexRequest indexRequest = client.prepareIndex(SECURITY_MAIN_ALIAS) @@ -290,7 +292,7 @@ private void createApiKeyAndIndexIt(Authentication authentication, CreateApiKeyR */ XContentBuilder newDocument(SecureString apiKey, String name, Authentication authentication, Set userRoles, Instant created, Instant expiration, List keyRoles, - Version version) throws IOException { + Version version, @Nullable Map metadata) throws IOException { XContentBuilder builder = XContentFactory.jsonBuilder(); builder.startObject() .field("doc_type", "api_key") @@ -330,6 +332,7 @@ XContentBuilder newDocument(SecureString apiKey, String name, Authentication aut builder.field("name", name) .field("version", version.id) + .field("metadata_flattened", metadata) .startObject("creator") .field("principal", authentication.getUser().principal()) .field("full_name", authentication.getUser().fullName()) @@ -670,6 +673,9 @@ void validateApiKeyExpiration(ApiKeyDoc apiKeyDoc, ApiKeyCredentials credentials authResultMetadata.put(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, apiKeyDoc.limitedByRoleDescriptorsBytes); authResultMetadata.put(API_KEY_ID_KEY, credentials.getId()); authResultMetadata.put(API_KEY_NAME_KEY, apiKeyDoc.name); + if (apiKeyDoc.metadataFlattened != null) { + authResultMetadata.put(API_KEY_METADATA_KEY, apiKeyDoc.metadataFlattened); + } listener.onResponse(AuthenticationResult.success(apiKeyUser, authResultMetadata)); } else { listener.onResponse(AuthenticationResult.unsuccessful("api key is expired", null)); @@ -868,8 +874,10 @@ private void findApiKeys(final BoolQueryBuilder boolQuery, boolean filterOutInva Boolean invalidated = (Boolean) source.get("api_key_invalidated"); String username = (String) ((Map) source.get("creator")).get("principal"); String realm = (String) ((Map) source.get("creator")).get("realm"); + Map metadata = (Map) source.get("metadata_flattened"); return new ApiKey(name, id, Instant.ofEpochMilli(creation), - (expiration != null) ? Instant.ofEpochMilli(expiration) : null, invalidated, username, realm); + (expiration != null) ? Instant.ofEpochMilli(expiration) : null, invalidated, username, realm, + metadata != null ? metadata : Map.of()); })); } } @@ -1116,6 +1124,7 @@ private boolean verify(SecureString password) { public static final class ApiKeyDoc { + private static final BytesReference NULL_BYTES = new BytesArray("null"); static final InstantiatingObjectParser PARSER; static { InstantiatingObjectParser.Builder builder = @@ -1131,6 +1140,7 @@ public static final class ApiKeyDoc { parserHelper.declareRawObject(builder, constructorArg(), new ParseField("role_descriptors")); parserHelper.declareRawObject(builder, constructorArg(), new ParseField("limited_by_role_descriptors")); builder.declareObject(constructorArg(), (p, c) -> p.map(), new ParseField("creator")); + parserHelper.declareRawObjectOrNull(builder, optionalConstructorArg(), new ParseField("metadata_flattened")); PARSER = builder.build(); } @@ -1145,6 +1155,8 @@ public static final class ApiKeyDoc { final BytesReference roleDescriptorsBytes; final BytesReference limitedByRoleDescriptorsBytes; final Map creator; + @Nullable + final BytesReference metadataFlattened; public ApiKeyDoc( String docType, @@ -1156,7 +1168,8 @@ public ApiKeyDoc( int version, BytesReference roleDescriptorsBytes, BytesReference limitedByRoleDescriptorsBytes, - Map creator) { + Map creator, + @Nullable BytesReference metadataFlattened) { this.docType = docType; this.creationTime = creationTime; @@ -1168,6 +1181,7 @@ public ApiKeyDoc( this.roleDescriptorsBytes = roleDescriptorsBytes; this.limitedByRoleDescriptorsBytes = limitedByRoleDescriptorsBytes; this.creator = creator; + this.metadataFlattened = NULL_BYTES.equals(metadataFlattened) ? null : metadataFlattened; } public CachedApiKeyDoc toCachedApiKeyDoc() { @@ -1186,7 +1200,8 @@ public CachedApiKeyDoc toCachedApiKeyDoc() { version, creator, roleDescriptorsHash, - limitedByRoleDescriptorsHash); + limitedByRoleDescriptorsHash, + metadataFlattened); } static ApiKeyDoc fromXContent(XContentParser parser) { @@ -1209,6 +1224,8 @@ public static final class CachedApiKeyDoc { final Map creator; final String roleDescriptorsHash; final String limitedByRoleDescriptorsHash; + @Nullable + final BytesReference metadataFlattened; public CachedApiKeyDoc( long creationTime, long expirationTime, @@ -1216,7 +1233,8 @@ public CachedApiKeyDoc( String hash, String name, int version, Map creator, String roleDescriptorsHash, - String limitedByRoleDescriptorsHash) { + String limitedByRoleDescriptorsHash, + @Nullable BytesReference metadataFlattened) { this.creationTime = creationTime; this.expirationTime = expirationTime; this.invalidated = invalidated; @@ -1226,6 +1244,7 @@ public CachedApiKeyDoc( this.creator = creator; this.roleDescriptorsHash = roleDescriptorsHash; this.limitedByRoleDescriptorsHash = limitedByRoleDescriptorsHash; + this.metadataFlattened = metadataFlattened; } public ApiKeyDoc toApiKeyDoc(BytesReference roleDescriptorsBytes, BytesReference limitedByRoleDescriptorsBytes) { @@ -1239,7 +1258,8 @@ public ApiKeyDoc toApiKeyDoc(BytesReference roleDescriptorsBytes, BytesReference version, roleDescriptorsBytes, limitedByRoleDescriptorsBytes, - creator); + creator, + metadataFlattened); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessor.java index 928dad255a058..97aca06dd5a18 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessor.java @@ -8,6 +8,10 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.ingest.AbstractProcessor; import org.elasticsearch.ingest.IngestDocument; import org.elasticsearch.ingest.Processor; @@ -131,6 +135,14 @@ public IngestDocument execute(IngestDocument ingestDocument) throws Exception { if (apiKeyId != null) { apiKeyField.put("id", apiKeyId); } + final Object apiKeyMetadata = authentication.getMetadata().get(ApiKeyService.API_KEY_METADATA_KEY); + if (apiKeyMetadata != null) { + final Tuple> tuple = + XContentHelper.convertToMap((BytesReference) apiKeyMetadata, false, XContentType.JSON); + if (false == tuple.v2().isEmpty()) { + apiKeyField.put("metadata", tuple.v2()); + } + } if (false == apiKeyField.isEmpty()) { userObject.put(apiKey, apiKeyField); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index ed14cf2c3a48c..790f8789dc346 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -356,7 +356,7 @@ Version.CURRENT, randomFrom(AuthenticationType.REALM, AuthenticationType.TOKEN, } XContentBuilder docSource = service.newDocument(new SecureString(key.toCharArray()), "test", authentication, Collections.singleton(SUPERUSER_ROLE_DESCRIPTOR), Instant.now(), Instant.now().plus(expiry), keyRoles, - Version.CURRENT); + Version.CURRENT, null); if (invalidated) { Map map = XContentHelper.convertToMap(BytesReference.bytes(docSource), true, XContentType.JSON).v2(); map.put("api_key_invalidated", true); @@ -985,7 +985,7 @@ public static Authentication createApiKeyAuthentication(ApiKeyService apiKeyServ Version version) throws Exception { XContentBuilder keyDocSource = apiKeyService.newDocument( new SecureString(randomAlphaOfLength(16).toCharArray()), "test", authentication, - userRoles, Instant.now(), Instant.now().plus(Duration.ofSeconds(3600)), keyRoles, Version.CURRENT); + userRoles, Instant.now(), Instant.now().plus(Duration.ofSeconds(3600)), keyRoles, Version.CURRENT, null); final ApiKeyDoc apiKeyDoc = ApiKeyDoc.fromXContent( XContentHelper.createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, BytesReference.bytes(keyDocSource), XContentType.JSON)); From a5a52b12ba732b31a9eab2381deb237b753adaff Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 11 Mar 2021 14:51:07 +1100 Subject: [PATCH 02/16] working on tests --- .../xpack/core/security/action/ApiKey.java | 12 +- .../core/security/action/ApiKeyTests.java | 60 ++++++++ .../action/GetApiKeyResponseTests.java | 20 +-- .../xpack/security/authc/ApiKeyService.java | 4 +- .../security/authc/ApiKeyServiceTests.java | 132 +++++++++++------- .../apikey/RestGetApiKeyActionTests.java | 25 +++- 6 files changed, 186 insertions(+), 67 deletions(-) create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/ApiKeyTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java index 8cbc4c20b3bb8..1329963db2f8d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.core.security.action; import org.elasticsearch.Version; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -37,10 +38,11 @@ public final class ApiKey implements ToXContentObject, Writeable { private final boolean invalidated; private final String username; private final String realm; + @Nullable private final Map metadata; public ApiKey(String name, String id, Instant creation, Instant expiration, boolean invalidated, String username, String realm, - Map metadata) { + @Nullable Map metadata) { this.name = name; this.id = id; // As we do not yet support the nanosecond precision when we serialize to JSON, @@ -69,7 +71,7 @@ public ApiKey(StreamInput in) throws IOException { if (in.getVersion().onOrAfter(Version.V_8_0_0)) { this.metadata = in.readMap(); } else { - this.metadata = Map.of(); + this.metadata = null; } } @@ -117,7 +119,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field("invalidated", invalidated) .field("username", username) .field("realm", realm) - .field("metadata", metadata); + .field("metadata", (metadata == null ? Map.of() : metadata)); return builder.endObject(); } @@ -169,7 +171,7 @@ public boolean equals(Object obj) { static final ConstructingObjectParser 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], - (Map) args[7]); + (args[7] == null) ? null : (Map) args[7]); }); static { PARSER.declareString(constructorArg(), new ParseField("name")); @@ -179,7 +181,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(constructorArg(), (p, c) -> p.map(), new ParseField("metadata")); + PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField("metadata")); } public static ApiKey fromXContent(XContentParser parser) throws IOException { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/ApiKeyTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/ApiKeyTests.java new file mode 100644 index 0000000000000..d648525d9caec --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/ApiKeyTests.java @@ -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; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.time.Instant; +import java.util.Map; +import java.util.Objects; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +public class ApiKeyTests extends ESTestCase { + + public void testXContent() throws IOException { + final String name = randomAlphaOfLengthBetween(4, 10); + final String id = randomAlphaOfLength(20); + // between 1970 and 2065 + final Instant creation = Instant.ofEpochSecond(randomLongBetween(0, 3000000000L), randomLongBetween(0, 999999999)); + final Instant expiration = randomBoolean() ? null + : Instant.ofEpochSecond(randomLongBetween(0, 3000000000L), randomLongBetween(0, 999999999)); + final boolean invalidated = randomBoolean(); + final String username = randomAlphaOfLengthBetween(4, 10); + final String realmName = randomAlphaOfLengthBetween(3, 8); + final Map metadata = + randomBoolean() ? null : Map.of(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)); + + final ApiKey apiKey = new ApiKey(name, id, creation, expiration, invalidated, username, realmName, metadata); + + XContentBuilder builder = XContentFactory.jsonBuilder(); + apiKey.toXContent(builder, ToXContent.EMPTY_PARAMS); + final Map map = XContentHelper.convertToMap(BytesReference.bytes(builder), false, builder.contentType()).v2(); + + assertThat(map.get("name"), equalTo(name)); + assertThat(map.get("id"), equalTo(id)); + assertThat(map.get("creation"), equalTo(creation.toEpochMilli())); + if (expiration != null) { + assertThat(map.get("expiration"), equalTo(expiration.toEpochMilli())); + } else { + assertThat(map.containsKey("expiration"), is(false)); + } + assertThat(map.get("invalidated"), is(invalidated)); + assertThat(map.get("username"), equalTo(username)); + assertThat(map.get("realm"), equalTo(realmName)); + assertThat(map.get("metadata"), equalTo(Objects.requireNonNullElseGet(metadata, Map::of))); + } + +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponseTests.java index aede5d8844c67..be41ad28cf183 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponseTests.java @@ -19,6 +19,7 @@ import java.time.Instant; import java.util.Arrays; import java.util.Collections; +import java.util.Map; import static org.hamcrest.Matchers.equalTo; @@ -28,7 +29,8 @@ public void testSerialization() throws IOException { boolean withApiKeyName = randomBoolean(); boolean withExpiration = randomBoolean(); ApiKey apiKeyInfo = createApiKeyInfo((withApiKeyName) ? randomAlphaOfLength(4) : null, randomAlphaOfLength(5), Instant.now(), - (withExpiration) ? Instant.now() : null, false, randomAlphaOfLength(4), randomAlphaOfLength(5)); + (withExpiration) ? Instant.now() : null, false, randomAlphaOfLength(4), randomAlphaOfLength(5), + randomBoolean() ? null : Map.of(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8))); GetApiKeyResponse response = new GetApiKeyResponse(Collections.singletonList(apiKeyInfo)); try (BytesStreamOutput output = new BytesStreamOutput()) { response.writeTo(output); @@ -41,11 +43,11 @@ public void testSerialization() throws IOException { public void testToXContent() throws IOException { ApiKey apiKeyInfo1 = createApiKeyInfo("name1", "id-1", Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), false, - "user-a", "realm-x"); + "user-a", "realm-x", null); ApiKey apiKeyInfo2 = createApiKeyInfo("name2", "id-2", Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), true, - "user-b", "realm-y"); + "user-b", "realm-y", Map.of()); ApiKey apiKeyInfo3 = createApiKeyInfo(null, "id-3", Instant.ofEpochMilli(100000L), null, true, - "user-c", "realm-z"); + "user-c", "realm-z", Map.of("foo", "bar")); GetApiKeyResponse response = new GetApiKeyResponse(Arrays.asList(apiKeyInfo1, apiKeyInfo2, apiKeyInfo3)); XContentBuilder builder = XContentFactory.jsonBuilder(); response.toXContent(builder, ToXContent.EMPTY_PARAMS); @@ -53,18 +55,18 @@ public void testToXContent() throws IOException { "{" + "\"api_keys\":[" + "{\"id\":\"id-1\",\"name\":\"name1\",\"creation\":100000,\"expiration\":10000000,\"invalidated\":false," - + "\"username\":\"user-a\",\"realm\":\"realm-x\"}," + + "\"username\":\"user-a\",\"realm\":\"realm-x\",\"metadata\":{}}," + "{\"id\":\"id-2\",\"name\":\"name2\",\"creation\":100000,\"expiration\":10000000,\"invalidated\":true," - + "\"username\":\"user-b\",\"realm\":\"realm-y\"}," + + "\"username\":\"user-b\",\"realm\":\"realm-y\",\"metadata\":{}}," + "{\"id\":\"id-3\",\"name\":null,\"creation\":100000,\"invalidated\":true," - + "\"username\":\"user-c\",\"realm\":\"realm-z\"}" + + "\"username\":\"user-c\",\"realm\":\"realm-z\",\"metadata\":{\"foo\":\"bar\"}}" + "]" + "}")); } private ApiKey createApiKeyInfo(String name, String id, Instant creation, Instant expiration, boolean invalidated, String username, - String realm) { - return new ApiKey(name, id, creation, expiration, invalidated, username, realm); + String realm, Map metadata) { + return new ApiKey(name, id, creation, expiration, invalidated, username, realm, metadata); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 1f3c46018703f..cbeed9dc562b0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -876,8 +876,8 @@ private void findApiKeys(final BoolQueryBuilder boolQuery, boolean filterOutInva String realm = (String) ((Map) source.get("creator")).get("realm"); Map metadata = (Map) source.get("metadata_flattened"); return new ApiKey(name, id, Instant.ofEpochMilli(creation), - (expiration != null) ? Instant.ofEpochMilli(expiration) : null, invalidated, username, realm, - metadata != null ? metadata : Map.of()); + (expiration != null) ? Instant.ofEpochMilli(expiration) : null, + invalidated, username, realm, metadata); })); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index 790f8789dc346..12a86861e6c9d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -38,6 +38,7 @@ import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.VersionUtils; +import org.elasticsearch.test.XContentTestUtils; import org.elasticsearch.threadpool.FixedExecutorBuilder; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; @@ -58,6 +59,7 @@ import org.elasticsearch.xpack.security.authc.ApiKeyService.ApiKeyRoleDescriptors; import org.elasticsearch.xpack.security.authc.ApiKeyService.CachedApiKeyHashResult; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; +import org.elasticsearch.xpack.security.rest.action.apikey.RestGetApiKeyActionTests; import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; import org.elasticsearch.xpack.security.support.FeatureNotEnabledException; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -217,7 +219,7 @@ public void testAuthenticateWithApiKey() throws Exception { } else { user = new User("hulk", new String[]{"superuser"}, "Bruce Banner", "hulk@test.com", Map.of(), true); } - mockKeyDocument(service, id, key, user); + final Map metadata = mockKeyDocument(service, id, key, user); final AuthenticationResult auth = tryAuthenticate(service, id, key); assertThat(auth.getStatus(), is(AuthenticationResult.Status.SUCCESS)); @@ -229,23 +231,7 @@ public void testAuthenticateWithApiKey() throws Exception { assertThat(auth.getMetadata().get(ApiKeyService.API_KEY_CREATOR_REALM_TYPE), is("native")); assertThat(auth.getMetadata().get(ApiKeyService.API_KEY_ID_KEY), is(id)); assertThat(auth.getMetadata().get(ApiKeyService.API_KEY_NAME_KEY), is("test")); - } - - public void testAuthenticationIsSkippedIfLicenseDoesNotAllowIt() throws Exception { - final Settings settings = Settings.builder().put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true).build(); - final ApiKeyService service = createApiKeyService(settings); - - final String id = randomAlphaOfLength(12); - final String key = randomAlphaOfLength(16); - - final User user; - if (randomBoolean()) { - user = new User(randomAlphaOfLength(6), new String[] { randomAlphaOfLength(12) }, new User("authenticated_user", - new String[] { "other" })); - } else { - user = new User(randomAlphaOfLength(6), new String[] { randomAlphaOfLength(12) }); - } - mockKeyDocument(service, id, key, user); + checkAuthApiKeyMetadata(metadata, auth); } public void testAuthenticationFailureWithInvalidatedApiKey() throws Exception { @@ -316,7 +302,7 @@ public void testMixingValidAndInvalidCredentials() throws Exception { } else { user = new User("hulk", new String[] { "superuser" }); } - mockKeyDocument(service, id, realKey, user); + final Map metadata = mockKeyDocument(service, id, realKey, user); for (int i = 0; i < 3; i++) { final String wrongKey = "=" + randomAlphaOfLength(14) + "@"; @@ -329,40 +315,44 @@ public void testMixingValidAndInvalidCredentials() throws Exception { assertThat(auth.getStatus(), is(AuthenticationResult.Status.SUCCESS)); assertThat(auth.getUser(), notNullValue()); assertThat(auth.getUser().principal(), is("hulk")); + checkAuthApiKeyMetadata(metadata, auth); } } - private void mockKeyDocument(ApiKeyService service, String id, String key, User user) throws IOException { - mockKeyDocument(service, id, key, user, false, Duration.ofSeconds(3600)); + private Map mockKeyDocument(ApiKeyService service, String id, String key, User user) throws IOException { + return mockKeyDocument(service, id, key, user, false, Duration.ofSeconds(3600)); } - private void mockKeyDocument(ApiKeyService service, String id, String key, User user, boolean invalidated, - Duration expiry) throws IOException { - mockKeyDocument(service, id, key, user, invalidated, expiry, null); + private Map mockKeyDocument(ApiKeyService service, String id, String key, User user, boolean invalidated, + Duration expiry) throws IOException { + return mockKeyDocument(service, id, key, user, invalidated, expiry, null); } - private void mockKeyDocument(ApiKeyService service, String id, String key, User user, boolean invalidated, - Duration expiry, List keyRoles) throws IOException { + private Map mockKeyDocument(ApiKeyService service, String id, String key, User user, boolean invalidated, + Duration expiry, List keyRoles) throws IOException { final Authentication authentication; if (user.isRunAs()) { authentication = new Authentication(user, new RealmRef("authRealm", "test", "foo"), - new RealmRef("realm1", "native", "node01"), Version.CURRENT, - randomFrom(AuthenticationType.REALM, AuthenticationType.TOKEN, AuthenticationType.INTERNAL, - AuthenticationType.ANONYMOUS), Collections.emptyMap()); + new RealmRef("realm1", "native", "node01"), Version.CURRENT, + randomFrom(AuthenticationType.REALM, AuthenticationType.TOKEN, AuthenticationType.INTERNAL, + AuthenticationType.ANONYMOUS), Collections.emptyMap()); } else { authentication = new Authentication(user, new RealmRef("realm1", "native", "node01"), null, - Version.CURRENT, randomFrom(AuthenticationType.REALM, AuthenticationType.TOKEN, AuthenticationType.INTERNAL, - AuthenticationType.ANONYMOUS), Collections.emptyMap()); + Version.CURRENT, randomFrom(AuthenticationType.REALM, AuthenticationType.TOKEN, AuthenticationType.INTERNAL, + AuthenticationType.ANONYMOUS), Collections.emptyMap()); } + @SuppressWarnings("unchecked") + final Map metadata = RestGetApiKeyActionTests.randomMetadata(); XContentBuilder docSource = service.newDocument(new SecureString(key.toCharArray()), "test", authentication, Collections.singleton(SUPERUSER_ROLE_DESCRIPTOR), Instant.now(), Instant.now().plus(expiry), keyRoles, - Version.CURRENT, null); + Version.CURRENT, metadata); if (invalidated) { Map map = XContentHelper.convertToMap(BytesReference.bytes(docSource), true, XContentType.JSON).v2(); map.put("api_key_invalidated", true); docSource = XContentBuilder.builder(XContentType.JSON.xContent()).map(map); } SecurityMocks.mockGetRequest(client, id, BytesReference.bytes(docSource)); + return metadata; } private AuthenticationResult tryAuthenticate(ApiKeyService service, String id, String key) throws Exception { @@ -589,7 +579,7 @@ public void testApiKeyServiceDisabled() throws Exception { assertThat(e.getMetadata(FeatureNotEnabledException.DISABLED_FEATURE_METADATA), contains("api_keys")); } - public void testApiKeyCache() { + public void testApiKeyCache() throws IOException { final String apiKey = randomAlphaOfLength(16); Hasher hasher = getFastStoredHashAlgoForTests(); final char[] hash = hasher.hash(new SecureString(apiKey.toCharArray())); @@ -648,6 +638,7 @@ public void testAuthenticateWhileCacheBeingPopulated() throws Exception { final char[] hash = hasher.hash(new SecureString(apiKey.toCharArray())); Map sourceMap = buildApiKeySourceDoc(hash); + final Object metadata = sourceMap.get("metadata_flattened"); ApiKeyService realService = createApiKeyService(Settings.EMPTY); ApiKeyService service = Mockito.spy(realService); @@ -692,15 +683,20 @@ public void testAuthenticateWhileCacheBeingPopulated() throws Exception { hashWait.release(); - assertThat(future1.actionGet(TimeValue.timeValueSeconds(2)).isAuthenticated(), is(true)); - assertThat(future2.actionGet(TimeValue.timeValueMillis(100)).isAuthenticated(), is(true)); + final AuthenticationResult authResult1 = future1.actionGet(TimeValue.timeValueSeconds(2)); + assertThat(authResult1.isAuthenticated(), is(true)); + checkAuthApiKeyMetadata(metadata, authResult1); + + final AuthenticationResult authResult2 = future2.actionGet(TimeValue.timeValueMillis(100)); + assertThat(authResult2.isAuthenticated(), is(true)); + checkAuthApiKeyMetadata(metadata, authResult2); CachedApiKeyHashResult cachedApiKeyHashResult = service.getFromCache(creds.getId()); assertNotNull(cachedApiKeyHashResult); assertThat(cachedApiKeyHashResult.success, is(true)); } - public void testApiKeyCacheDisabled() { + public void testApiKeyCacheDisabled() throws IOException { final String apiKey = randomAlphaOfLength(16); Hasher hasher = getFastStoredHashAlgoForTests(); final char[] hash = hasher.hash(new SecureString(apiKey.toCharArray())); @@ -722,7 +718,7 @@ public void testApiKeyCacheDisabled() { assertNull(service.getRoleDescriptorsBytesCache()); } - public void testApiKeyDocCacheCanBeDisabledSeparately() { + public void testApiKeyDocCacheCanBeDisabledSeparately() throws IOException { final String apiKey = randomAlphaOfLength(16); Hasher hasher = getFastStoredHashAlgoForTests(); final char[] hash = hasher.hash(new SecureString(apiKey.toCharArray())); @@ -755,7 +751,8 @@ public void testApiKeyDocCache() throws IOException, ExecutionException, Interru final String docId = randomAlphaOfLength(16); final String apiKey = randomAlphaOfLength(16); ApiKeyCredentials apiKeyCredentials = new ApiKeyCredentials(docId, new SecureString(apiKey.toCharArray())); - mockKeyDocument(service, docId, apiKey, new User("hulk", "superuser"), false, Duration.ofSeconds(3600)); + final Map metadata = + mockKeyDocument(service, docId, apiKey, new User("hulk", "superuser"), false, Duration.ofSeconds(3600)); PlainActionFuture future = new PlainActionFuture<>(); service.loadApiKeyAndValidateCredentials(threadContext, apiKeyCredentials, future); final ApiKeyService.CachedApiKeyDoc cachedApiKeyDoc = service.getDocCache().get(docId); @@ -771,12 +768,18 @@ public void testApiKeyDocCache() throws IOException, ExecutionException, Interru final List limitedByRoleDescriptors = service.parseRoleDescriptors(docId, limitedByRoleDescriptorsBytes); assertEquals(1, limitedByRoleDescriptors.size()); assertEquals(SUPERUSER_ROLE_DESCRIPTOR, limitedByRoleDescriptors.get(0)); + if (metadata == null) { + assertNull(cachedApiKeyDoc.metadataFlattened); + } else { + assertThat(cachedApiKeyDoc.metadataFlattened, equalTo(XContentTestUtils.convertToXContent(metadata, XContentType.JSON))); + } // 2. A different API Key with the same role descriptors will share the entries in the role descriptor cache final String docId2 = randomAlphaOfLength(16); final String apiKey2 = randomAlphaOfLength(16); ApiKeyCredentials apiKeyCredentials2 = new ApiKeyCredentials(docId2, new SecureString(apiKey2.toCharArray())); - mockKeyDocument(service, docId2, apiKey2, new User("thor", "superuser"), false, Duration.ofSeconds(3600)); + final Map metadata2 = + mockKeyDocument(service, docId2, apiKey2, new User("thor", "superuser"), false, Duration.ofSeconds(3600)); PlainActionFuture future2 = new PlainActionFuture<>(); service.loadApiKeyAndValidateCredentials(threadContext, apiKeyCredentials2, future2); final ApiKeyService.CachedApiKeyDoc cachedApiKeyDoc2 = service.getDocCache().get(docId2); @@ -788,6 +791,11 @@ public void testApiKeyDocCache() throws IOException, ExecutionException, Interru final BytesReference limitedByRoleDescriptorsBytes2 = service.getRoleDescriptorsBytesCache().get(cachedApiKeyDoc2.limitedByRoleDescriptorsHash); assertSame(limitedByRoleDescriptorsBytes, limitedByRoleDescriptorsBytes2); + if (metadata2 == null) { + assertNull(cachedApiKeyDoc2.metadataFlattened); + } else { + assertThat(cachedApiKeyDoc2.metadataFlattened, equalTo(XContentTestUtils.convertToXContent(metadata2, XContentType.JSON))); + } // 3. Different role descriptors will be cached into a separate entry final String docId3 = randomAlphaOfLength(16); @@ -795,8 +803,8 @@ public void testApiKeyDocCache() throws IOException, ExecutionException, Interru ApiKeyCredentials apiKeyCredentials3 = new ApiKeyCredentials(docId3, new SecureString(apiKey3.toCharArray())); final List keyRoles = List.of(RoleDescriptor.parse("key-role", new BytesArray("{\"cluster\":[\"monitor\"]}"), true, XContentType.JSON)); - mockKeyDocument(service, docId3, apiKey3, new User("banner", "superuser"), - false, Duration.ofSeconds(3600), keyRoles); + final Map metadata3 = + mockKeyDocument(service, docId3, apiKey3, new User("banner", "superuser"), false, Duration.ofSeconds(3600), keyRoles); PlainActionFuture future3 = new PlainActionFuture<>(); service.loadApiKeyAndValidateCredentials(threadContext, apiKeyCredentials3, future3); final ApiKeyService.CachedApiKeyDoc cachedApiKeyDoc3 = service.getDocCache().get(docId3); @@ -809,22 +817,32 @@ public void testApiKeyDocCache() throws IOException, ExecutionException, Interru final BytesReference roleDescriptorsBytes3 = service.getRoleDescriptorsBytesCache().get(cachedApiKeyDoc3.roleDescriptorsHash); assertNotSame(roleDescriptorsBytes, roleDescriptorsBytes3); assertEquals(3, service.getRoleDescriptorsBytesCache().count()); + if (metadata3 == null) { + assertNull(cachedApiKeyDoc3.metadataFlattened); + } else { + assertThat(cachedApiKeyDoc3.metadataFlattened, equalTo(XContentTestUtils.convertToXContent(metadata3, XContentType.JSON))); + } // 4. Will fetch document from security index if role descriptors are not found even when // cachedApiKeyDoc is available service.getRoleDescriptorsBytesCache().invalidateAll(); - mockKeyDocument(service, docId, apiKey, new User("hulk", "superuser"), false, Duration.ofSeconds(3600)); + final Map metadata4 = + mockKeyDocument(service, docId, apiKey, new User("hulk", "superuser"), false, Duration.ofSeconds(3600)); PlainActionFuture future4 = new PlainActionFuture<>(); service.loadApiKeyAndValidateCredentials(threadContext, apiKeyCredentials, future4); verify(client, times(4)).get(any(GetRequest.class), any(ActionListener.class)); assertEquals(2, service.getRoleDescriptorsBytesCache().count()); - assertSame(AuthenticationResult.Status.SUCCESS, future4.get().getStatus()); + final AuthenticationResult authResult4 = future4.get(); + assertSame(AuthenticationResult.Status.SUCCESS, authResult4.getStatus()); + checkAuthApiKeyMetadata(metadata4, authResult4); // 5. Cached entries will be used for the same API key doc SecurityMocks.mockGetRequestException(client, new EsRejectedExecutionException("rejected")); PlainActionFuture future5 = new PlainActionFuture<>(); service.loadApiKeyAndValidateCredentials(threadContext, apiKeyCredentials, future5); - assertSame(AuthenticationResult.Status.SUCCESS, future5.get().getStatus()); + final AuthenticationResult authResult5 = future5.get(); + assertSame(AuthenticationResult.Status.SUCCESS, authResult5.getStatus()); + checkAuthApiKeyMetadata(metadata4, authResult5); } public void testWillGetLookedUpByRealmNameIfExists() { @@ -889,6 +907,7 @@ public void testCachedApiKeyValidationWillNotBeBlockedByUnCachedApiKey() throws Hasher hasher = getFastStoredHashAlgoForTests(); final char[] hash = hasher.hash(new SecureString(apiKey1.toCharArray())); Map sourceMap = buildApiKeySourceDoc(hash); + final Object metadata = sourceMap.get("metadata_flattened"); mockSourceDocument(creds.getId(), sourceMap); // Authenticate the key once to cache it @@ -897,6 +916,7 @@ public void testCachedApiKeyValidationWillNotBeBlockedByUnCachedApiKey() throws service.authenticateWithApiKeyIfPresent(threadPool.getThreadContext(), future); final AuthenticationResult authenticationResult = future.get(); assertEquals(AuthenticationResult.Status.SUCCESS, authenticationResult.getStatus()); + checkAuthApiKeyMetadata(metadata,authenticationResult); // Now force the hashing thread pool to saturate so that any un-cached keys cannot be validated final ExecutorService mockExecutorService = mock(ExecutorService.class); @@ -926,6 +946,7 @@ public void testCachedApiKeyValidationWillNotBeBlockedByUnCachedApiKey() throws service.authenticateWithApiKeyIfPresent(threadPool.getThreadContext(), future3); final AuthenticationResult authenticationResult3 = future3.get(); assertEquals(AuthenticationResult.Status.SUCCESS, authenticationResult3.getStatus()); + checkAuthApiKeyMetadata(metadata, authenticationResult3); } public void testApiKeyDocDeserialization() throws IOException { @@ -985,7 +1006,8 @@ public static Authentication createApiKeyAuthentication(ApiKeyService apiKeyServ Version version) throws Exception { XContentBuilder keyDocSource = apiKeyService.newDocument( new SecureString(randomAlphaOfLength(16).toCharArray()), "test", authentication, - userRoles, Instant.now(), Instant.now().plus(Duration.ofSeconds(3600)), keyRoles, Version.CURRENT, null); + userRoles, Instant.now(), Instant.now().plus(Duration.ofSeconds(3600)), keyRoles, Version.CURRENT, + randomBoolean() ? null : Map.of(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8))); final ApiKeyDoc apiKeyDoc = ApiKeyDoc.fromXContent( XContentHelper.createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, BytesReference.bytes(keyDocSource), XContentType.JSON)); @@ -1067,6 +1089,8 @@ private Map buildApiKeySourceDoc(char[] hash) { creatorMap.put("metadata", Collections.emptyMap()); sourceMap.put("creator", creatorMap); sourceMap.put("api_key_invalidated", false); + //noinspection unchecked + sourceMap.put("metadata_flattened", RestGetApiKeyActionTests.randomMetadata()); return sourceMap; } @@ -1083,7 +1107,9 @@ private void mockSourceDocument(String id, Map sourceMap) throws } } - private ApiKeyDoc buildApiKeyDoc(char[] hash, long expirationTime, boolean invalidated) { + private ApiKeyDoc buildApiKeyDoc(char[] hash, long expirationTime, boolean invalidated) throws IOException { + final BytesReference metadataBytes = + XContentTestUtils.convertToXContent(RestGetApiKeyActionTests.randomMetadata(), XContentType.JSON); return new ApiKeyDoc( "api_key", Clock.systemUTC().instant().toEpochMilli(), @@ -1101,7 +1127,19 @@ private ApiKeyDoc buildApiKeyDoc(char[] hash, long expirationTime, boolean inval "realm", "realm1", "realm_type", "realm_type1", "metadata", Map.of() - ) + ), + metadataBytes ); } + + private void checkAuthApiKeyMetadata(Object metadata, AuthenticationResult authResult1) throws IOException { + if (metadata == null) { + assertThat(authResult1.getMetadata().containsKey(ApiKeyService.API_KEY_METADATA_KEY), is(false)); + } else { + //noinspection unchecked + assertThat( + authResult1.getMetadata().get(ApiKeyService.API_KEY_METADATA_KEY), + equalTo(XContentTestUtils.convertToXContent((Map) metadata, XContentType.JSON))); + } + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java index db12ff34ab341..e5a6a26692b72 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java @@ -84,8 +84,11 @@ public void sendResponse(RestResponse restResponse) { }; final Instant creation = Instant.now(); final Instant expiration = randomFrom(Arrays.asList(null, Instant.now().plus(10, ChronoUnit.DAYS))); + @SuppressWarnings("unchecked") + final Map metadata = randomMetadata(); final GetApiKeyResponse getApiKeyResponseExpected = new GetApiKeyResponse( - Collections.singletonList(new ApiKey("api-key-name-1", "api-key-id-1", creation, expiration, false, "user-x", "realm-1"))); + Collections.singletonList( + new ApiKey("api-key-name-1", "api-key-id-1", creation, expiration, false, "user-x", "realm-1", metadata))); try (NodeClient client = new NodeClient(Settings.EMPTY, threadPool) { @SuppressWarnings("unchecked") @@ -126,7 +129,8 @@ void doExecute(ActionType action, Request request, ActionListener action, Request request, ActionListener randomMetadata() { + return randomFrom( + Map.of("application", randomAlphaOfLength(5), + "number", 1, + "numbers", List.of(1, 3, 5), + "environment", Map.of("os", "linux", "level", 42, "category", "trusted") + ), + Map.of(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)), + Map.of(), + null); + } + private static MapBuilder mapBuilder() { return MapBuilder.newMapBuilder(); } From b0e51340b3ba1e2a283cb3b4396fe05d4602e6c9 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 11 Mar 2021 18:05:37 +1100 Subject: [PATCH 03/16] doc and yaml tests --- .../security/create-api-keys.asciidoc | 22 +- .../rest-api/security/get-api-keys.asciidoc | 9 +- .../security/authc/ApiKeyIntegTests.java | 206 +++++++++++------- .../test/mixed_cluster/120_api_key.yml | 42 ++++ .../test/mixed_cluster/120_api_key_auth.yml | 23 -- .../test/old_cluster/120_api_key.yml | 16 ++ .../test/upgraded_cluster/120_api_key.yml | 58 +++++ 7 files changed, 272 insertions(+), 104 deletions(-) create mode 100644 x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/120_api_key.yml delete mode 100644 x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/120_api_key_auth.yml create mode 100644 x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/120_api_key.yml create mode 100644 x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/120_api_key.yml diff --git a/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc b/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc index 1d35a8fbd5e90..b972d3e7cf3e8 100644 --- a/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc +++ b/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc @@ -28,16 +28,16 @@ See the note under `role_descriptors`. The API keys are created by the {es} API key service, which is automatically enabled when you configure TLS on the HTTP interface. See <>. Alternatively, -you can explicitly enable the `xpack.security.authc.api_key.enabled` setting. When -you are running in production mode, a bootstrap check prevents you from enabling -the API key service unless you also enable TLS on the HTTP interface. +you can explicitly enable the `xpack.security.authc.api_key.enabled` setting. When +you are running in production mode, a bootstrap check prevents you from enabling +the API key service unless you also enable TLS on the HTTP interface. A successful create API key API call returns a JSON structure that contains the API key, its unique id, and its name. If applicable, it also returns expiration -information for the API key in milliseconds. +information for the API key in milliseconds. NOTE: By default, API keys never expire. You can specify expiration information -when you create the API keys. +when you create the API keys. See <> for configuration settings related to API key service. @@ -54,7 +54,7 @@ The following parameters can be specified in the body of a POST or PUT request: `role_descriptors`:: (Optional, array-of-role-descriptor) An array of role descriptors for this API key. This parameter is optional. When it is not specified or is an empty array, -then the API key will have a _point in time snapshot of permissions of the +then the API key will have a _point in time snapshot of permissions of the authenticated user_. If you supply role descriptors then the resultant permissions would be an intersection of API keys permissions and authenticated user's permissions thereby limiting the access scope for API keys. @@ -74,6 +74,8 @@ authentication; it will not have authority to call {es} APIs. (Optional, string) Expiration time for the API key. By default, API keys never expire. +`metadata`:: +(object) Arbitrary metadata that you want to associate with the API key. [[security-api-create-api-key-example]] ==== {api-examples-title} @@ -105,6 +107,14 @@ POST /_security/api_key } ] } + }, + "metadata": { + "application": "my-application", + "environment": { + "level": 1, + "trusted": true, + "tags": ["dev", "staging"] + } } } ------------------------------------------------------------ diff --git a/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc b/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc index ca9f90c2f6961..c808ca1d2f98e 100644 --- a/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc +++ b/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc @@ -147,6 +147,9 @@ Following creates an API key POST /_security/api_key { "name": "my-api-key-1" + "metadata": { + "application": "my-application" + } } ------------------------------------------------------------ @@ -182,7 +185,10 @@ A successful call returns a JSON structure that contains the information of one "expiration": 1548551550158, <5> "invalidated": false, <6> "username": "myuser", <7> - "realm": "native1" <8> + "realm": "native1", <8> + "metadata": { <9> + "application": "myapp" + } }, { "id": "api-key-id-2", @@ -206,3 +212,4 @@ A successful call returns a JSON structure that contains the information of one a value of `true`. Otherwise, it is `false`. <7> Principal for which this API key was created <8> Realm name of the principal for which this API key was created +<9> Metadata of the API key diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java index 8ae8f1aafb101..9865d494f5e4c 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java @@ -55,6 +55,7 @@ import org.elasticsearch.xpack.core.security.action.user.PutUserRequest; import org.elasticsearch.xpack.core.security.action.user.PutUserResponse; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.security.rest.action.apikey.RestGetApiKeyActionTests; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.junit.After; import org.junit.Before; @@ -67,6 +68,7 @@ import java.util.Arrays; import java.util.Base64; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -75,7 +77,9 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; import static org.elasticsearch.test.SecuritySettingsSource.TEST_SUPERUSER; @@ -173,6 +177,7 @@ public void testCreateApiKey() throws Exception { .setName("test key") .setExpiration(TimeValue.timeValueHours(TimeUnit.DAYS.toHours(7L))) .setRoleDescriptors(Collections.singletonList(descriptor)) + .setMetadata(RestGetApiKeyActionTests.randomMetadata()) .get(); assertEquals("test key", response.getName()); @@ -221,7 +226,8 @@ public void testMultipleApiKeysCanHaveSameName() { Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client).setName(keyName).setExpiration(null) - .setRoleDescriptors(Collections.singletonList(descriptor)).get(); + .setRoleDescriptors(Collections.singletonList(descriptor)) + .setMetadata(RestGetApiKeyActionTests.randomMetadata()).get(); assertNotNull(response.getId()); assertNotNull(response.getKey()); responses.add(response); @@ -242,7 +248,7 @@ public void testCreateApiKeyWithoutNameWillFail() { public void testInvalidateApiKeysForRealm() throws InterruptedException, ExecutionException { int noOfApiKeys = randomIntBetween(3, 5); - List responses = createApiKeys(noOfApiKeys, null); + List responses = createApiKeys(noOfApiKeys, null).v1(); Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); @@ -253,7 +259,7 @@ public void testInvalidateApiKeysForRealm() throws InterruptedException, Executi public void testInvalidateApiKeysForUser() throws Exception { int noOfApiKeys = randomIntBetween(3, 5); - List responses = createApiKeys(noOfApiKeys, null); + List responses = createApiKeys(noOfApiKeys, null).v1(); Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); @@ -264,7 +270,7 @@ public void testInvalidateApiKeysForUser() throws Exception { } public void testInvalidateApiKeysForRealmAndUser() throws InterruptedException, ExecutionException { - List responses = createApiKeys(1, null); + List responses = createApiKeys(1, null).v1(); Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); @@ -275,7 +281,7 @@ public void testInvalidateApiKeysForRealmAndUser() throws InterruptedException, } public void testInvalidateApiKeysForApiKeyId() throws InterruptedException, ExecutionException { - List responses = createApiKeys(1, null); + List responses = createApiKeys(1, null).v1(); Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); @@ -285,7 +291,7 @@ public void testInvalidateApiKeysForApiKeyId() throws InterruptedException, Exec } public void testInvalidateApiKeysForApiKeyName() throws InterruptedException, ExecutionException { - List responses = createApiKeys(1, null); + List responses = createApiKeys(1, null).v1(); Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); @@ -360,7 +366,7 @@ public void testInvalidatedApiKeysDeletedByRemover() throws Exception { Client client = waitForExpiredApiKeysRemoverTriggerReadyAndGetClient().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); - List createdApiKeys = createApiKeys(2, null); + List createdApiKeys = createApiKeys(2, null).v1(); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.usingApiKeyId(createdApiKeys.get(0).getId(), false), @@ -446,7 +452,7 @@ public void testExpiredApiKeysBehaviorWhenKeysExpired1WeekBeforeAnd1DayBefore() Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); int noOfKeys = 4; - List createdApiKeys = createApiKeys(noOfKeys, null); + List createdApiKeys = createApiKeys(noOfKeys, null).v1(); Instant created = Instant.now(); PlainActionFuture getApiKeyResponseListener = new PlainActionFuture<>(); @@ -522,7 +528,8 @@ private void refreshSecurityIndex() throws Exception { } public void testActiveApiKeysWithNoExpirationNeverGetDeletedByRemover() throws Exception { - List responses = createApiKeys(2, null); + final Tuple, List>> tuple = createApiKeys(2, null); + List responses = tuple.v1(); Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); @@ -537,13 +544,14 @@ public void testActiveApiKeysWithNoExpirationNeverGetDeletedByRemover() throws E PlainActionFuture getApiKeyResponseListener = new PlainActionFuture<>(); client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingRealmName("file"), getApiKeyResponseListener); GetApiKeyResponse response = getApiKeyResponseListener.get(); - verifyGetResponse(2, responses, response, Collections.singleton(responses.get(0).getId()), + verifyGetResponse(2, responses, tuple.v2(), response, Collections.singleton(responses.get(0).getId()), Collections.singletonList(responses.get(1).getId())); } public void testGetApiKeysForRealm() throws InterruptedException, ExecutionException { int noOfApiKeys = randomIntBetween(3, 5); - List responses = createApiKeys(noOfApiKeys, null); + final Tuple, List>> tuple = createApiKeys(noOfApiKeys, null); + List responses = tuple.v1(); Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); boolean invalidate = randomBoolean(); @@ -565,41 +573,45 @@ public void testGetApiKeysForRealm() throws InterruptedException, ExecutionExcep PlainActionFuture listener = new PlainActionFuture<>(); client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingRealmName("file"), listener); GetApiKeyResponse response = listener.get(); - verifyGetResponse(noOfApiKeys, responses, response, + verifyGetResponse(noOfApiKeys, responses, tuple.v2(), response, expectedValidKeyIds, invalidatedApiKeyIds); } public void testGetApiKeysForUser() throws Exception { int noOfApiKeys = randomIntBetween(3, 5); - List responses = createApiKeys(noOfApiKeys, null); + final Tuple, List>> tuple = createApiKeys(noOfApiKeys, null); + List responses = tuple.v1(); Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingUserName(TEST_SUPERUSER), listener); GetApiKeyResponse response = listener.get(); - verifyGetResponse(noOfApiKeys, responses, response, responses.stream().map(o -> o.getId()).collect(Collectors.toSet()), null); + verifyGetResponse(noOfApiKeys, responses, tuple.v2(), + response, responses.stream().map(o -> o.getId()).collect(Collectors.toSet()), null); } public void testGetApiKeysForRealmAndUser() throws InterruptedException, ExecutionException { - List responses = createApiKeys(1, null); + final Tuple, List>> tuple = createApiKeys(1, null); + List responses = tuple.v1(); Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingRealmAndUserName("file", TEST_SUPERUSER), listener); GetApiKeyResponse response = listener.get(); - verifyGetResponse(1, responses, response, Collections.singleton(responses.get(0).getId()), null); + verifyGetResponse(1, responses, tuple.v2(), response, Collections.singleton(responses.get(0).getId()), null); } public void testGetApiKeysForApiKeyId() throws InterruptedException, ExecutionException { - List responses = createApiKeys(1, null); + final Tuple, List>> tuple = createApiKeys(1, null); + List responses = tuple.v1(); Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyId(responses.get(0).getId(), false), listener); GetApiKeyResponse response = listener.get(); - verifyGetResponse(1, responses, response, Collections.singleton(responses.get(0).getId()), null); + verifyGetResponse(1, responses, tuple.v2(), response, Collections.singleton(responses.get(0).getId()), null); } public void testGetApiKeysForApiKeyName() throws InterruptedException, ExecutionException { @@ -608,52 +620,57 @@ public void testGetApiKeysForApiKeyName() throws InterruptedException, Execution basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING)); final int noOfApiKeys = randomIntBetween(1, 3); - final List createApiKeyResponses1 = createApiKeys(noOfApiKeys, null); - final List createApiKeyResponses2 = createApiKeys( - headers, noOfApiKeys, "another-test-key-", null, "monitor"); + final Tuple, List>> tuple1 = createApiKeys(noOfApiKeys, null); + final List createApiKeyResponses1 = tuple1.v1(); + final Tuple, List>> tuple2 = + createApiKeys(headers, noOfApiKeys, "another-test-key-", null, "monitor"); + final List createApiKeyResponses2 = tuple2.v1(); Client client = client().filterWithHeader(headers); PlainActionFuture listener = new PlainActionFuture<>(); @SuppressWarnings("unchecked") List responses = randomFrom(createApiKeyResponses1, createApiKeyResponses2); + List> metadatas = responses == createApiKeyResponses1 ? tuple1.v2() : tuple2.v2(); client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyName(responses.get(0).getName(), false), listener); - verifyGetResponse(1, responses, listener.get(), Collections.singleton(responses.get(0).getId()), null); + verifyGetResponse(1, responses, metadatas, listener.get(), Collections.singleton(responses.get(0).getId()), null); PlainActionFuture listener2 = new PlainActionFuture<>(); client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyName("test-key*", false), listener2); - verifyGetResponse(noOfApiKeys, createApiKeyResponses1, listener2.get(), + verifyGetResponse(noOfApiKeys, createApiKeyResponses1, tuple1.v2(), listener2.get(), createApiKeyResponses1.stream().map(CreateApiKeyResponse::getId).collect(Collectors.toSet()), null); PlainActionFuture listener3 = new PlainActionFuture<>(); client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyName("*", false), listener3); responses = Stream.concat(createApiKeyResponses1.stream(), createApiKeyResponses2.stream()).collect(Collectors.toList()); - verifyGetResponse(2 * noOfApiKeys, responses, listener3.get(), + metadatas = Stream.concat(tuple1.v2().stream(), tuple2.v2().stream()).collect(Collectors.toList()); + verifyGetResponse(2 * noOfApiKeys, responses, metadatas, listener3.get(), responses.stream().map(CreateApiKeyResponse::getId).collect(Collectors.toSet()), null); PlainActionFuture listener4 = new PlainActionFuture<>(); client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyName("does-not-exist*", false), listener4); - verifyGetResponse(0, Collections.emptyList(), listener4.get(), Collections.emptySet(), null); + verifyGetResponse(0, Collections.emptyList(), null, listener4.get(), Collections.emptySet(), null); PlainActionFuture listener5 = new PlainActionFuture<>(); client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyName("another-test-key*", false), listener5); - verifyGetResponse(noOfApiKeys, createApiKeyResponses2, listener5.get(), + verifyGetResponse(noOfApiKeys, createApiKeyResponses2, tuple2.v2(), listener5.get(), createApiKeyResponses2.stream().map(CreateApiKeyResponse::getId).collect(Collectors.toSet()), null); } public void testGetApiKeysOwnedByCurrentAuthenticatedUser() throws InterruptedException, ExecutionException { int noOfSuperuserApiKeys = randomIntBetween(3, 5); int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5); - List defaultUserCreatedKeys = createApiKeys(noOfSuperuserApiKeys, null); + List defaultUserCreatedKeys = createApiKeys(noOfSuperuserApiKeys, null).v1(); String userWithManageApiKeyRole = randomFrom("user_with_manage_api_key_role", "user_with_manage_own_api_key_role"); - List userWithManageApiKeyRoleApiKeys = createApiKeys(userWithManageApiKeyRole, - noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); + final Tuple, List>> tuple = + createApiKeys(userWithManageApiKeyRole, noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); + List userWithManageApiKeyRoleApiKeys = tuple.v1(); final Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(userWithManageApiKeyRole, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.forOwnedApiKeys(), listener); GetApiKeyResponse response = listener.get(); - verifyGetResponse(userWithManageApiKeyRole, noOfApiKeysForUserWithManageApiKeyRole, userWithManageApiKeyRoleApiKeys, + verifyGetResponse(userWithManageApiKeyRole, noOfApiKeysForUserWithManageApiKeyRole, userWithManageApiKeyRoleApiKeys, tuple.v2(), response, userWithManageApiKeyRoleApiKeys.stream().map(o -> o.getId()).collect(Collectors.toSet()), null); } @@ -662,12 +679,17 @@ public void testGetApiKeysOwnedByRunAsUserWhenOwnerIsTrue() throws ExecutionExce int noOfSuperuserApiKeys = randomIntBetween(3, 5); int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5); createApiKeys(noOfSuperuserApiKeys, null); - List userWithManageOwnApiKeyRoleApiKeys = createApiKeys("user_with_manage_own_api_key_role", - "user_with_run_as_role", noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); + final Tuple, List>> tuple = createApiKeys("user_with_manage_own_api_key_role", + "user_with_run_as_role", + noOfApiKeysForUserWithManageApiKeyRole, + null, + "monitor"); + List userWithManageOwnApiKeyRoleApiKeys = tuple.v1(); PlainActionFuture listener = new PlainActionFuture<>(); getClientForRunAsUser().execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.forOwnedApiKeys(), listener); GetApiKeyResponse response = listener.get(); - verifyGetResponse("user_with_manage_own_api_key_role", noOfApiKeysForUserWithManageApiKeyRole, userWithManageOwnApiKeyRoleApiKeys, + verifyGetResponse("user_with_manage_own_api_key_role", noOfApiKeysForUserWithManageApiKeyRole, + userWithManageOwnApiKeyRoleApiKeys, tuple.v2(), response, userWithManageOwnApiKeyRoleApiKeys.stream().map(o -> o.getId()).collect(Collectors.toSet()), null); } @@ -676,13 +698,18 @@ public void testGetApiKeysOwnedByRunAsUserWhenRunAsUserInfoIsGiven() throws Exec int noOfSuperuserApiKeys = randomIntBetween(3, 5); int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5); createApiKeys(noOfSuperuserApiKeys, null); - List userWithManageOwnApiKeyRoleApiKeys = createApiKeys("user_with_manage_own_api_key_role", - "user_with_run_as_role", noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); + final Tuple, List>> tuple = createApiKeys("user_with_manage_own_api_key_role", + "user_with_run_as_role", + noOfApiKeysForUserWithManageApiKeyRole, + null, + "monitor"); + List userWithManageOwnApiKeyRoleApiKeys = tuple.v1(); PlainActionFuture listener = new PlainActionFuture<>(); getClientForRunAsUser().execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingRealmAndUserName("file", "user_with_manage_own_api_key_role"), listener); GetApiKeyResponse response = listener.get(); - verifyGetResponse("user_with_manage_own_api_key_role", noOfApiKeysForUserWithManageApiKeyRole, userWithManageOwnApiKeyRoleApiKeys, + verifyGetResponse("user_with_manage_own_api_key_role", noOfApiKeysForUserWithManageApiKeyRole, + userWithManageOwnApiKeyRoleApiKeys, tuple.v2(), response, userWithManageOwnApiKeyRoleApiKeys.stream().map(o -> o.getId()).collect(Collectors.toSet()), null); } @@ -692,7 +719,7 @@ public void testGetApiKeysOwnedByRunAsUserWillNotWorkWhenAuthUserInfoIsGiven() t int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5); createApiKeys(noOfSuperuserApiKeys, null); final List userWithManageOwnApiKeyRoleApiKeys = createApiKeys("user_with_manage_own_api_key_role", - "user_with_run_as_role", noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); + "user_with_run_as_role", noOfApiKeysForUserWithManageApiKeyRole, null, "monitor").v1(); PlainActionFuture listener = new PlainActionFuture<>(); @SuppressWarnings("unchecked") final Tuple invalidRealmAndUserPair = randomFrom( @@ -710,11 +737,14 @@ public void testGetAllApiKeys() throws InterruptedException, ExecutionException int noOfSuperuserApiKeys = randomIntBetween(3, 5); int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5); int noOfApiKeysForUserWithManageOwnApiKeyRole = randomIntBetween(3, 7); - List defaultUserCreatedKeys = createApiKeys(noOfSuperuserApiKeys, null); - List userWithManageApiKeyRoleApiKeys = createApiKeys("user_with_manage_api_key_role", - noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); - List userWithManageOwnApiKeyRoleApiKeys = createApiKeys("user_with_manage_own_api_key_role", - noOfApiKeysForUserWithManageOwnApiKeyRole, null, "monitor"); + final Tuple, List>> defaultUserTuple = createApiKeys(noOfSuperuserApiKeys, null); + List defaultUserCreatedKeys = defaultUserTuple.v1(); + final Tuple, List>> userWithManageTuple = + createApiKeys("user_with_manage_api_key_role", noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); + List userWithManageApiKeyRoleApiKeys = userWithManageTuple.v1(); + final Tuple, List>> userWithManageOwnTuple = + createApiKeys("user_with_manage_own_api_key_role", noOfApiKeysForUserWithManageOwnApiKeyRole, null, "monitor"); + List userWithManageOwnApiKeyRoleApiKeys = userWithManageOwnTuple.v1(); final Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue("user_with_manage_api_key_role", TEST_PASSWORD_SECURE_STRING))); @@ -725,8 +755,10 @@ public void testGetAllApiKeys() throws InterruptedException, ExecutionException List allApiKeys = new ArrayList<>(); Stream.of(defaultUserCreatedKeys, userWithManageApiKeyRoleApiKeys, userWithManageOwnApiKeyRoleApiKeys).forEach( allApiKeys::addAll); + final List> metadatas = Stream.of(defaultUserTuple.v2(), userWithManageTuple.v2(), userWithManageOwnTuple.v2()) + .flatMap(List::stream).collect(Collectors.toList()); verifyGetResponse(new String[] {TEST_SUPERUSER, "user_with_manage_api_key_role", - "user_with_manage_own_api_key_role" }, totalApiKeys, allApiKeys, response, + "user_with_manage_own_api_key_role" }, totalApiKeys, allApiKeys, metadatas, response, allApiKeys.stream().map(o -> o.getId()).collect(Collectors.toSet()), null); } @@ -734,11 +766,11 @@ public void testGetAllApiKeysFailsForUserWithNoRoleOrRetrieveOwnApiKeyRole() thr int noOfSuperuserApiKeys = randomIntBetween(3, 5); int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5); int noOfApiKeysForUserWithManageOwnApiKeyRole = randomIntBetween(3, 7); - List defaultUserCreatedKeys = createApiKeys(noOfSuperuserApiKeys, null); + List defaultUserCreatedKeys = createApiKeys(noOfSuperuserApiKeys, null).v1(); List userWithManageApiKeyRoleApiKeys = createApiKeys("user_with_manage_api_key_role", - noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); + noOfApiKeysForUserWithManageApiKeyRole, null, "monitor").v1(); List userWithManageOwnApiKeyRoleApiKeys = createApiKeys("user_with_manage_own_api_key_role", - noOfApiKeysForUserWithManageOwnApiKeyRole, null, "monitor"); + noOfApiKeysForUserWithManageOwnApiKeyRole, null, "monitor").v1(); final String withUser = randomFrom("user_with_manage_own_api_key_role", "user_with_no_api_key_role"); final Client client = client().filterWithHeader( @@ -752,10 +784,10 @@ public void testGetAllApiKeysFailsForUserWithNoRoleOrRetrieveOwnApiKeyRole() thr public void testInvalidateApiKeysOwnedByCurrentAuthenticatedUser() throws InterruptedException, ExecutionException { int noOfSuperuserApiKeys = randomIntBetween(3, 5); int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5); - List defaultUserCreatedKeys = createApiKeys(noOfSuperuserApiKeys, null); + List defaultUserCreatedKeys = createApiKeys(noOfSuperuserApiKeys, null).v1(); String userWithManageApiKeyRole = randomFrom("user_with_manage_api_key_role", "user_with_manage_own_api_key_role"); List userWithManageApiKeyRoleApiKeys = createApiKeys(userWithManageApiKeyRole, - noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); + noOfApiKeysForUserWithManageApiKeyRole, null, "monitor").v1(); final Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(userWithManageApiKeyRole, TEST_PASSWORD_SECURE_STRING))); @@ -772,7 +804,7 @@ public void testInvalidateApiKeysOwnedByRunAsUserWhenOwnerIsTrue() throws Interr int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5); createApiKeys(noOfSuperuserApiKeys, null); List userWithManageApiKeyRoleApiKeys = createApiKeys("user_with_manage_own_api_key_role", - "user_with_run_as_role", noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); + "user_with_run_as_role", noOfApiKeysForUserWithManageApiKeyRole, null, "monitor").v1(); PlainActionFuture listener = new PlainActionFuture<>(); getClientForRunAsUser().execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.forOwnedApiKeys(), listener); InvalidateApiKeyResponse invalidateResponse = listener.get(); @@ -785,7 +817,7 @@ public void testInvalidateApiKeysOwnedByRunAsUserWhenRunAsUserInfoIsGiven() thro int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5); createApiKeys(noOfSuperuserApiKeys, null); List userWithManageApiKeyRoleApiKeys = createApiKeys("user_with_manage_own_api_key_role", - "user_with_run_as_role", noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); + "user_with_run_as_role", noOfApiKeysForUserWithManageApiKeyRole, null, "monitor").v1(); PlainActionFuture listener = new PlainActionFuture<>(); getClientForRunAsUser().execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.usingRealmAndUserName("file", "user_with_manage_own_api_key_role"), listener); @@ -799,7 +831,7 @@ public void testInvalidateApiKeysOwnedByRunAsUserWillNotWorkWhenAuthUserInfoIsGi int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5); createApiKeys(noOfSuperuserApiKeys, null); List userWithManageApiKeyRoleApiKeys = createApiKeys("user_with_manage_own_api_key_role", - "user_with_run_as_role", noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); + "user_with_run_as_role", noOfApiKeysForUserWithManageApiKeyRole, null, "monitor").v1(); PlainActionFuture listener = new PlainActionFuture<>(); @SuppressWarnings("unchecked") final Tuple invalidRealmAndUserPair = randomFrom( @@ -815,14 +847,16 @@ public void testInvalidateApiKeysOwnedByRunAsUserWillNotWorkWhenAuthUserInfoIsGi public void testApiKeyAuthorizationApiKeyMustBeAbleToRetrieveItsOwnInformationButNotAnyOtherKeysCreatedBySameOwner() throws InterruptedException, ExecutionException { - List responses = createApiKeys(TEST_SUPERUSER, 2, null, (String[]) null); + final Tuple, List>> tuple = + createApiKeys(TEST_SUPERUSER, 2, null, (String[]) null); + List responses = tuple.v1(); final String base64ApiKeyKeyValue = Base64.getEncoder().encodeToString( (responses.get(0).getId() + ":" + responses.get(0).getKey().toString()).getBytes(StandardCharsets.UTF_8)); Client client = client().filterWithHeader(Map.of("Authorization", "ApiKey " + base64ApiKeyKeyValue)); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyId(responses.get(0).getId(), randomBoolean()), listener); GetApiKeyResponse response = listener.get(); - verifyGetResponse(1, responses, response, Collections.singleton(responses.get(0).getId()), null); + verifyGetResponse(1, responses, tuple.v2(), response, Collections.singleton(responses.get(0).getId()), null); final PlainActionFuture failureListener = new PlainActionFuture<>(); // for any other API key id, it must deny access @@ -840,7 +874,7 @@ public void testApiKeyAuthorizationApiKeyMustBeAbleToRetrieveItsOwnInformationBu public void testApiKeyWithManageOwnPrivilegeIsAbleToInvalidateItselfButNotAnyOtherKeysCreatedBySameOwner() throws InterruptedException, ExecutionException { - List responses = createApiKeys(TEST_SUPERUSER, 2, null, "manage_own_api_key"); + List responses = createApiKeys(TEST_SUPERUSER, 2, null, "manage_own_api_key").v1(); final String base64ApiKeyKeyValue = Base64.getEncoder().encodeToString( (responses.get(0).getId() + ":" + responses.get(0).getKey().toString()).getBytes(StandardCharsets.UTF_8)); Client client = client().filterWithHeader(Map.of("Authorization", "ApiKey " + base64ApiKeyKeyValue)); @@ -877,6 +911,7 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException { .setName("key-1") .setRoleDescriptors(Collections.singletonList( new RoleDescriptor("role", new String[] { "manage_api_key" }, null, null))) + .setMetadata(RestGetApiKeyActionTests.randomMetadata()) .get(); assertEquals("key-1", response.getName()); @@ -891,7 +926,7 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException { final String expectedMessage = "creating derived api keys requires an explicit role descriptor that is empty"; final IllegalArgumentException e1 = expectThrows(IllegalArgumentException.class, - () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-2").get()); + () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-2").setMetadata(RestGetApiKeyActionTests.randomMetadata()).get()); assertThat(e1.getMessage(), containsString(expectedMessage)); final IllegalArgumentException e2 = expectThrows(IllegalArgumentException.class, @@ -901,6 +936,7 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException { final IllegalArgumentException e3 = expectThrows(IllegalArgumentException.class, () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-4") + .setMetadata(RestGetApiKeyActionTests.randomMetadata()) .setRoleDescriptors(Collections.singletonList( new RoleDescriptor("role", new String[] { "manage_own_api_key" }, null, null) )).get()); @@ -913,10 +949,12 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException { final IllegalArgumentException e4 = expectThrows(IllegalArgumentException.class, () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-5") + .setMetadata(RestGetApiKeyActionTests.randomMetadata()) .setRoleDescriptors(roleDescriptors).get()); assertThat(e4.getMessage(), containsString(expectedMessage)); final CreateApiKeyResponse key100Response = new CreateApiKeyRequestBuilder(clientKey1).setName("key-100") + .setMetadata(RestGetApiKeyActionTests.randomMetadata()) .setRoleDescriptors(Collections.singletonList( new RoleDescriptor("role", null, null, null) )).get(); @@ -944,6 +982,7 @@ public void testAuthenticationReturns429WhenThreadPoolIsSaturated() throws IOExc final CreateApiKeyResponse createApiKeyResponse = new CreateApiKeyRequestBuilder(client) .setName("auth only key") .setRoleDescriptors(Collections.singletonList(descriptor)) + .setMetadata(RestGetApiKeyActionTests.randomMetadata()) .get(); assertNotNull(createApiKeyResponse.getId()); @@ -1098,6 +1137,7 @@ private Tuple createApiKeyAndAuthenticateWithIt() throws IOExcep final CreateApiKeyResponse createApiKeyResponse = new CreateApiKeyRequestBuilder(client) .setName("test key") + .setMetadata(RestGetApiKeyActionTests.randomMetadata()) .get(); final String docId = createApiKeyResponse.getId(); final String base64ApiKeyKeyValue = Base64.getEncoder().encodeToString( @@ -1116,79 +1156,97 @@ private void assertApiKeyNotCreated(Client client, String keyName) throws Execut } private void verifyGetResponse(int expectedNumberOfApiKeys, List responses, + List> metadatas, GetApiKeyResponse response, Set validApiKeyIds, List invalidatedApiKeyIds) { - verifyGetResponse(TEST_SUPERUSER, expectedNumberOfApiKeys, responses, response, validApiKeyIds, + verifyGetResponse(TEST_SUPERUSER, expectedNumberOfApiKeys, responses, metadatas, response, validApiKeyIds, invalidatedApiKeyIds); } private void verifyGetResponse(String user, int expectedNumberOfApiKeys, List responses, + List> metadatas, GetApiKeyResponse response, Set validApiKeyIds, List invalidatedApiKeyIds) { - verifyGetResponse(new String[]{user}, expectedNumberOfApiKeys, responses, response, validApiKeyIds, invalidatedApiKeyIds); + verifyGetResponse( + new String[]{user}, expectedNumberOfApiKeys, responses, metadatas, response, validApiKeyIds, invalidatedApiKeyIds); } private void verifyGetResponse(String[] user, int expectedNumberOfApiKeys, List responses, + List> metadatas, GetApiKeyResponse response, Set validApiKeyIds, List invalidatedApiKeyIds) { assertThat(response.getApiKeyInfos().length, equalTo(expectedNumberOfApiKeys)); List expectedIds = responses.stream().filter(o -> validApiKeyIds.contains(o.getId())).map(o -> o.getId()) - .collect(Collectors.toList()); + .collect(Collectors.toList()); List actualIds = Arrays.stream(response.getApiKeyInfos()).filter(o -> o.isInvalidated() == false).map(o -> o.getId()) - .collect(Collectors.toList()); + .collect(Collectors.toList()); assertThat(actualIds, containsInAnyOrder(expectedIds.toArray(Strings.EMPTY_ARRAY))); List expectedNames = responses.stream().filter(o -> validApiKeyIds.contains(o.getId())).map(o -> o.getName()) - .collect(Collectors.toList()); + .collect(Collectors.toList()); List actualNames = Arrays.stream(response.getApiKeyInfos()).filter(o -> o.isInvalidated() == false).map(o -> o.getName()) - .collect(Collectors.toList()); + .collect(Collectors.toList()); assertThat(actualNames, containsInAnyOrder(expectedNames.toArray(Strings.EMPTY_ARRAY))); Set expectedUsernames = (validApiKeyIds.isEmpty()) ? Collections.emptySet() - : Set.of(user); + : Set.of(user); Set actualUsernames = Arrays.stream(response.getApiKeyInfos()).filter(o -> o.isInvalidated() == false) - .map(o -> o.getUsername()).collect(Collectors.toSet()); + .map(o -> o.getUsername()).collect(Collectors.toSet()); assertThat(actualUsernames, containsInAnyOrder(expectedUsernames.toArray(Strings.EMPTY_ARRAY))); if (invalidatedApiKeyIds != null) { List actualInvalidatedApiKeyIds = Arrays.stream(response.getApiKeyInfos()).filter(o -> o.isInvalidated()) - .map(o -> o.getId()).collect(Collectors.toList()); + .map(o -> o.getId()).collect(Collectors.toList()); assertThat(invalidatedApiKeyIds, containsInAnyOrder(actualInvalidatedApiKeyIds.toArray(Strings.EMPTY_ARRAY))); } + if (metadatas != null) { + final HashMap> idToMetadata = IntStream.range(0, responses.size()).collect( + (Supplier>>) HashMap::new, + (m, i) -> m.put(responses.get(i).getId(), metadatas.get(i)), + HashMap::putAll); + for (ApiKey apiKey : response.getApiKeyInfos()) { + assertThat(apiKey.getMetadata(), equalTo(idToMetadata.get(apiKey.getId()))); + } + } } - private List createApiKeys(int noOfApiKeys, TimeValue expiration) { + private Tuple, List>> createApiKeys(int noOfApiKeys, TimeValue expiration) { return createApiKeys(TEST_SUPERUSER, noOfApiKeys, expiration, "monitor"); } - private List createApiKeys(String user, int noOfApiKeys, TimeValue expiration, String... clusterPrivileges) { + private Tuple, List>> createApiKeys( + String user, int noOfApiKeys, TimeValue expiration, String... clusterPrivileges) { final Map headers = Collections.singletonMap("Authorization", basicAuthHeaderValue(user, TEST_PASSWORD_SECURE_STRING)); return createApiKeys(headers, noOfApiKeys, expiration, clusterPrivileges); } - private List createApiKeys(String owningUser, String authenticatingUser, - int noOfApiKeys, TimeValue expiration, String... clusterPrivileges) { + private Tuple, List>> createApiKeys( + String owningUser, String authenticatingUser, int noOfApiKeys, TimeValue expiration, String... clusterPrivileges) { final Map headers = Map.of( "Authorization", basicAuthHeaderValue(authenticatingUser, TEST_PASSWORD_SECURE_STRING), "es-security-runas-user", owningUser); return createApiKeys(headers, noOfApiKeys, expiration, clusterPrivileges); } - private List createApiKeys(Map headers, - int noOfApiKeys, TimeValue expiration, String... clusterPrivileges) { + private Tuple, List>> createApiKeys( + Map headers, int noOfApiKeys, TimeValue expiration, String... clusterPrivileges) { return createApiKeys(headers, noOfApiKeys, "test-key-", expiration, clusterPrivileges); } - private List createApiKeys(Map headers, int noOfApiKeys, String namePrefix, - TimeValue expiration, String... clusterPrivileges) { + private Tuple, List>> createApiKeys( + Map headers, int noOfApiKeys, String namePrefix, TimeValue expiration, String... clusterPrivileges) { + List> metadatas = new ArrayList<>(noOfApiKeys); List responses = new ArrayList<>(); for (int i = 0; i < noOfApiKeys; i++) { final RoleDescriptor descriptor = new RoleDescriptor("role", clusterPrivileges, null, null); Client client = client().filterWithHeader(headers); + final Map metadata = RestGetApiKeyActionTests.randomMetadata(); + metadatas.add(metadata); final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client) .setName(namePrefix + randomAlphaOfLengthBetween(5, 9) + i).setExpiration(expiration) - .setRoleDescriptors(Collections.singletonList(descriptor)).get(); + .setRoleDescriptors(Collections.singletonList(descriptor)) + .setMetadata(metadata).get(); assertNotNull(response.getId()); assertNotNull(response.getKey()); responses.add(response); } assertThat(responses.size(), is(noOfApiKeys)); - return responses; + return new Tuple<>(responses, metadatas); } /** diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/120_api_key.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/120_api_key.yml new file mode 100644 index 0000000000000..20be95d12b39c --- /dev/null +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/120_api_key.yml @@ -0,0 +1,42 @@ +--- +"Test API key authentication will work in a mixed cluster": + + - skip: + features: headers + + - do: + security.create_api_key: + body: > + { + "name": "my-mixed-api-key-1" + } + - match: { name: "my-mixed-api-key-1" } + - is_true: id + - is_true: api_key + - transform_and_set: { login_creds: "#base64EncodeCredentials(id,api_key)" } + + - do: + headers: + Authorization: ApiKey ${login_creds} + nodes.info: {} + - match: { _nodes.failed: 0 } + + +--- +"Create API key with metadata in a mixed cluster": + + - skip: + features: [headers, node_selector] + + - do: + node_selector: + version: "7.13.0 - " + security.create_api_key: + body: > + { + "name": "my-mixed-api-key-2", + "metadata": {"foo": "bar"} + } + - match: { name: "my-mixed-api-key-2" } + - is_true: id + - is_true: api_key diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/120_api_key_auth.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/120_api_key_auth.yml deleted file mode 100644 index 34b019d0d9911..0000000000000 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/120_api_key_auth.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- -"Test API key authentication will work in a mixed cluster": - - - skip: - features: headers - - - do: - security.create_api_key: - body: > - { - "name": "my-api-key" - } - - match: { name: "my-api-key" } - - is_true: id - - is_true: api_key - - transform_and_set: { login_creds: "#base64EncodeCredentials(id,api_key)" } - - - do: - headers: - Authorization: ApiKey ${login_creds} - nodes.info: {} - - match: { _nodes.failed: 0 } - diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/120_api_key.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/120_api_key.yml new file mode 100644 index 0000000000000..20d4e18b62cb6 --- /dev/null +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/120_api_key.yml @@ -0,0 +1,16 @@ +--- +"Create API key in the old cluster": + + - skip: + features: headers + + - do: + security.create_api_key: + body: > + { + "name": "my-old-api-key" + } + - match: { name: "my-old-api-key" } + - is_true: id + - is_true: api_key + diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/120_api_key.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/120_api_key.yml new file mode 100644 index 0000000000000..2e3769dbf813e --- /dev/null +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/120_api_key.yml @@ -0,0 +1,58 @@ +--- +"Create API key in the upgraded cluster": + + - skip: + features: headers + + - do: + security.create_api_key: + body: > + { + "name": "my-new-api-key", + "metadata": {"application": "myapp"} + } + - match: { name: "my-new-api-key" } + - is_true: id + - is_true: api_key + +--- +"Get API keys in the upgraded cluster": + + - do: + security.get_api_key: + name: "my-old-api-key" + + - length: { api_keys: 1 } + + - match: { api_keys.0.name: "my-old-api-key" } + - match: { api_keys.0.metadata: {} } + + - do: + security.get_api_key: + name: "my-new-api-key" + + - length: { api_keys: 1 } + + - match: { api_keys.0.name: "my-new-api-key" } + - match: { api_keys.0.metadata: { "application": "myapp" } } + + - do: + security.get_api_key: + name: "my-mixed-api-key-1" + + - length: { api_keys: 1 } + + - match: { api_keys.0.name: "my-mixed-api-key-1" } + - match: { api_keys.0.metadata: {} } + + - do: + security.get_api_key: + name: "my-mixed-api-key-2" + + - length: { api_keys: 1 } + + - match: { api_keys.0.name: "my-mixed-api-key-2" } +# We cannot assert metadata for this API key because it is possible +# that the security index is on an old node and the metadata is dropped +# when transfer through the wire. But at least the key will be created +# and retrieved successfully From bea484487f39aa41b4854d6cdfd1de9e9bdf31cf Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 11 Mar 2021 18:28:50 +1100 Subject: [PATCH 04/16] update on client side code --- .../client/security/CreateApiKeyRequest.java | 17 +++++++++++++++-- .../client/security/support/ApiKey.java | 14 +++++++++++--- .../documentation/SecurityDocumentationIT.java | 2 +- .../client/security/GetApiKeyResponseTests.java | 2 +- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java index 9947f02600c65..dc627bb0e23bf 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -28,19 +29,28 @@ public final class CreateApiKeyRequest implements Validatable, ToXContentObject private final TimeValue expiration; private final List roles; private final RefreshPolicy refreshPolicy; + private final Map 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 roles, @Nullable TimeValue expiration, - @Nullable final RefreshPolicy refreshPolicy) { + @Nullable final RefreshPolicy refreshPolicy, + @Nullable Map 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 roles, @Nullable TimeValue expiration, + @Nullable final RefreshPolicy refreshPolicy) { + this(name, roles, expiration, refreshPolicy, null); } public String getName() { @@ -74,7 +84,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 @@ -106,6 +116,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } builder.endObject(); } + if (metadata != null) { + builder.field("metadata", metadata); + } builder.endObject(); return builder.endObject(); } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java index d054e7e08a2e1..28156e5ed9abe 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java @@ -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; @@ -32,8 +33,10 @@ public final class ApiKey { private final boolean invalidated; private final String username; private final String realm; + private final Map 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 metadata) { this.name = name; this.id = id; // 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 this.invalidated = invalidated; this.username = username; this.realm = realm; + this.metadata = metadata; } public String getId() { @@ -113,12 +117,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 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) args[7]); }); static { PARSER.declareField(optionalConstructorArg(), (p, c) -> p.textOrNull(), new ParseField("name"), @@ -129,6 +136,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(constructorArg(), (p, c) -> p.map(), new ParseField("metadata")); } public static ApiKey fromXContent(XContentParser parser) throws IOException { diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java index d74bd32e4c888..69e5807a13140 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java @@ -2124,7 +2124,7 @@ public void testGetApiKey() throws Exception { 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", null); { // tag::get-api-key-id-request GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId(), false); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetApiKeyResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetApiKeyResponseTests.java index ffb894833dc7c..ac538a5b400a1 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetApiKeyResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetApiKeyResponseTests.java @@ -86,6 +86,6 @@ private static GetApiKeyResponse mutateTestItem(GetApiKeyResponse original) { private static ApiKey createApiKeyInfo(String name, String id, Instant creation, Instant expiration, boolean invalidated, String username, String realm) { - return new ApiKey(name, id, creation, expiration, invalidated, username, realm); + return new ApiKey(name, id, creation, expiration, invalidated, username, realm, null); } } From 16db065bbbec756a7437f8ce2758afec55ff1bfe Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 11 Mar 2021 20:12:45 +1100 Subject: [PATCH 05/16] fix tests --- .../java/org/elasticsearch/client/security/support/ApiKey.java | 2 +- x-pack/docs/en/rest-api/security/get-api-keys.asciidoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java index 28156e5ed9abe..b4f81367ea078 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java @@ -136,7 +136,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(constructorArg(), (p, c) -> p.map(), new ParseField("metadata")); + PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField("metadata")); } public static ApiKey fromXContent(XContentParser parser) throws IOException { diff --git a/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc b/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc index c808ca1d2f98e..817ea27b7b5d0 100644 --- a/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc +++ b/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc @@ -146,7 +146,7 @@ Following creates an API key ------------------------------------------------------------ POST /_security/api_key { - "name": "my-api-key-1" + "name": "my-api-key-1", "metadata": { "application": "my-application" } From 126de75d70cd76372afb9cbb05516d233d353738 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 11 Mar 2021 20:16:41 +1100 Subject: [PATCH 06/16] remove changes to setsecurityuser processor --- .../security/ingest/SetSecurityUserProcessor.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessor.java index 97aca06dd5a18..928dad255a058 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessor.java @@ -8,10 +8,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.collect.Tuple; -import org.elasticsearch.common.xcontent.XContentHelper; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.ingest.AbstractProcessor; import org.elasticsearch.ingest.IngestDocument; import org.elasticsearch.ingest.Processor; @@ -135,14 +131,6 @@ public IngestDocument execute(IngestDocument ingestDocument) throws Exception { if (apiKeyId != null) { apiKeyField.put("id", apiKeyId); } - final Object apiKeyMetadata = authentication.getMetadata().get(ApiKeyService.API_KEY_METADATA_KEY); - if (apiKeyMetadata != null) { - final Tuple> tuple = - XContentHelper.convertToMap((BytesReference) apiKeyMetadata, false, XContentType.JSON); - if (false == tuple.v2().isEmpty()) { - apiKeyField.put("metadata", tuple.v2()); - } - } if (false == apiKeyField.isEmpty()) { userObject.put(apiKey, apiKeyField); } From c0a2456eae2ee2f179d7470df3a9e3dffb6345b5 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 11 Mar 2021 20:26:02 +1100 Subject: [PATCH 07/16] refactor --- .../core/security/action/ApiKeyTests.java | 16 ++++++++++++-- .../security/authc/ApiKeyIntegTests.java | 22 +++++++++---------- .../security/authc/ApiKeyServiceTests.java | 8 +++---- .../apikey/RestGetApiKeyActionTests.java | 20 ++++------------- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/ApiKeyTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/ApiKeyTests.java index d648525d9caec..9ef0051daf410 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/ApiKeyTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/ApiKeyTests.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.time.Instant; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -34,8 +35,7 @@ public void testXContent() throws IOException { final boolean invalidated = randomBoolean(); final String username = randomAlphaOfLengthBetween(4, 10); final String realmName = randomAlphaOfLengthBetween(3, 8); - final Map metadata = - randomBoolean() ? null : Map.of(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)); + final Map metadata = randomMetadata(); final ApiKey apiKey = new ApiKey(name, id, creation, expiration, invalidated, username, realmName, metadata); @@ -57,4 +57,16 @@ public void testXContent() throws IOException { assertThat(map.get("metadata"), equalTo(Objects.requireNonNullElseGet(metadata, Map::of))); } + @SuppressWarnings("unchecked") + public static Map randomMetadata() { + return randomFrom( + Map.of("application", randomAlphaOfLength(5), + "number", 1, + "numbers", List.of(1, 3, 5), + "environment", Map.of("os", "linux", "level", 42, "category", "trusted") + ), + Map.of(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)), + Map.of(), + null); + } } diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java index 9865d494f5e4c..8aa18156a1ed5 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java @@ -40,6 +40,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.action.ApiKey; +import org.elasticsearch.xpack.core.security.action.ApiKeyTests; import org.elasticsearch.xpack.core.security.action.ClearSecurityCacheAction; import org.elasticsearch.xpack.core.security.action.ClearSecurityCacheRequest; import org.elasticsearch.xpack.core.security.action.ClearSecurityCacheResponse; @@ -55,7 +56,6 @@ import org.elasticsearch.xpack.core.security.action.user.PutUserRequest; import org.elasticsearch.xpack.core.security.action.user.PutUserResponse; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; -import org.elasticsearch.xpack.security.rest.action.apikey.RestGetApiKeyActionTests; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.junit.After; import org.junit.Before; @@ -177,7 +177,7 @@ public void testCreateApiKey() throws Exception { .setName("test key") .setExpiration(TimeValue.timeValueHours(TimeUnit.DAYS.toHours(7L))) .setRoleDescriptors(Collections.singletonList(descriptor)) - .setMetadata(RestGetApiKeyActionTests.randomMetadata()) + .setMetadata(ApiKeyTests.randomMetadata()) .get(); assertEquals("test key", response.getName()); @@ -227,7 +227,7 @@ public void testMultipleApiKeysCanHaveSameName() { Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client).setName(keyName).setExpiration(null) .setRoleDescriptors(Collections.singletonList(descriptor)) - .setMetadata(RestGetApiKeyActionTests.randomMetadata()).get(); + .setMetadata(ApiKeyTests.randomMetadata()).get(); assertNotNull(response.getId()); assertNotNull(response.getKey()); responses.add(response); @@ -911,7 +911,7 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException { .setName("key-1") .setRoleDescriptors(Collections.singletonList( new RoleDescriptor("role", new String[] { "manage_api_key" }, null, null))) - .setMetadata(RestGetApiKeyActionTests.randomMetadata()) + .setMetadata(ApiKeyTests.randomMetadata()) .get(); assertEquals("key-1", response.getName()); @@ -926,7 +926,7 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException { final String expectedMessage = "creating derived api keys requires an explicit role descriptor that is empty"; final IllegalArgumentException e1 = expectThrows(IllegalArgumentException.class, - () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-2").setMetadata(RestGetApiKeyActionTests.randomMetadata()).get()); + () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-2").setMetadata(ApiKeyTests.randomMetadata()).get()); assertThat(e1.getMessage(), containsString(expectedMessage)); final IllegalArgumentException e2 = expectThrows(IllegalArgumentException.class, @@ -936,7 +936,7 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException { final IllegalArgumentException e3 = expectThrows(IllegalArgumentException.class, () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-4") - .setMetadata(RestGetApiKeyActionTests.randomMetadata()) + .setMetadata(ApiKeyTests.randomMetadata()) .setRoleDescriptors(Collections.singletonList( new RoleDescriptor("role", new String[] { "manage_own_api_key" }, null, null) )).get()); @@ -949,12 +949,12 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException { final IllegalArgumentException e4 = expectThrows(IllegalArgumentException.class, () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-5") - .setMetadata(RestGetApiKeyActionTests.randomMetadata()) + .setMetadata(ApiKeyTests.randomMetadata()) .setRoleDescriptors(roleDescriptors).get()); assertThat(e4.getMessage(), containsString(expectedMessage)); final CreateApiKeyResponse key100Response = new CreateApiKeyRequestBuilder(clientKey1).setName("key-100") - .setMetadata(RestGetApiKeyActionTests.randomMetadata()) + .setMetadata(ApiKeyTests.randomMetadata()) .setRoleDescriptors(Collections.singletonList( new RoleDescriptor("role", null, null, null) )).get(); @@ -982,7 +982,7 @@ public void testAuthenticationReturns429WhenThreadPoolIsSaturated() throws IOExc final CreateApiKeyResponse createApiKeyResponse = new CreateApiKeyRequestBuilder(client) .setName("auth only key") .setRoleDescriptors(Collections.singletonList(descriptor)) - .setMetadata(RestGetApiKeyActionTests.randomMetadata()) + .setMetadata(ApiKeyTests.randomMetadata()) .get(); assertNotNull(createApiKeyResponse.getId()); @@ -1137,7 +1137,7 @@ private Tuple createApiKeyAndAuthenticateWithIt() throws IOExcep final CreateApiKeyResponse createApiKeyResponse = new CreateApiKeyRequestBuilder(client) .setName("test key") - .setMetadata(RestGetApiKeyActionTests.randomMetadata()) + .setMetadata(ApiKeyTests.randomMetadata()) .get(); final String docId = createApiKeyResponse.getId(); final String base64ApiKeyKeyValue = Base64.getEncoder().encodeToString( @@ -1235,7 +1235,7 @@ private Tuple, List>> createApiKe for (int i = 0; i < noOfApiKeys; i++) { final RoleDescriptor descriptor = new RoleDescriptor("role", clusterPrivileges, null, null); Client client = client().filterWithHeader(headers); - final Map metadata = RestGetApiKeyActionTests.randomMetadata(); + final Map metadata = ApiKeyTests.randomMetadata(); metadatas.add(metadata); final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client) .setName(namePrefix + randomAlphaOfLengthBetween(5, 9) + i).setExpiration(expiration) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index 12a86861e6c9d..373ec9cf99050 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -44,6 +44,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.SecurityContext; +import org.elasticsearch.xpack.core.security.action.ApiKeyTests; import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType; @@ -59,7 +60,6 @@ import org.elasticsearch.xpack.security.authc.ApiKeyService.ApiKeyRoleDescriptors; import org.elasticsearch.xpack.security.authc.ApiKeyService.CachedApiKeyHashResult; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; -import org.elasticsearch.xpack.security.rest.action.apikey.RestGetApiKeyActionTests; import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; import org.elasticsearch.xpack.security.support.FeatureNotEnabledException; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -342,7 +342,7 @@ Version.CURRENT, randomFrom(AuthenticationType.REALM, AuthenticationType.TOKEN, AuthenticationType.ANONYMOUS), Collections.emptyMap()); } @SuppressWarnings("unchecked") - final Map metadata = RestGetApiKeyActionTests.randomMetadata(); + final Map metadata = ApiKeyTests.randomMetadata(); XContentBuilder docSource = service.newDocument(new SecureString(key.toCharArray()), "test", authentication, Collections.singleton(SUPERUSER_ROLE_DESCRIPTOR), Instant.now(), Instant.now().plus(expiry), keyRoles, Version.CURRENT, metadata); @@ -1090,7 +1090,7 @@ private Map buildApiKeySourceDoc(char[] hash) { sourceMap.put("creator", creatorMap); sourceMap.put("api_key_invalidated", false); //noinspection unchecked - sourceMap.put("metadata_flattened", RestGetApiKeyActionTests.randomMetadata()); + sourceMap.put("metadata_flattened", ApiKeyTests.randomMetadata()); return sourceMap; } @@ -1109,7 +1109,7 @@ private void mockSourceDocument(String id, Map sourceMap) throws private ApiKeyDoc buildApiKeyDoc(char[] hash, long expirationTime, boolean invalidated) throws IOException { final BytesReference metadataBytes = - XContentTestUtils.convertToXContent(RestGetApiKeyActionTests.randomMetadata(), XContentType.JSON); + XContentTestUtils.convertToXContent(ApiKeyTests.randomMetadata(), XContentType.JSON); return new ApiKeyDoc( "api_key", Clock.systemUTC().instant().toEpochMilli(), diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java index e5a6a26692b72..0300b723e546e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java @@ -29,6 +29,7 @@ import org.elasticsearch.test.rest.FakeRestRequest; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.security.action.ApiKey; +import org.elasticsearch.xpack.core.security.action.ApiKeyTests; import org.elasticsearch.xpack.core.security.action.GetApiKeyRequest; import org.elasticsearch.xpack.core.security.action.GetApiKeyResponse; @@ -85,7 +86,7 @@ public void sendResponse(RestResponse restResponse) { final Instant creation = Instant.now(); final Instant expiration = randomFrom(Arrays.asList(null, Instant.now().plus(10, ChronoUnit.DAYS))); @SuppressWarnings("unchecked") - final Map metadata = randomMetadata(); + final Map metadata = ApiKeyTests.randomMetadata(); final GetApiKeyResponse getApiKeyResponseExpected = new GetApiKeyResponse( Collections.singletonList( new ApiKey("api-key-name-1", "api-key-id-1", creation, expiration, false, "user-x", "realm-1", metadata))); @@ -159,9 +160,9 @@ public void sendResponse(RestResponse restResponse) { final Instant creation = Instant.now(); final Instant expiration = randomFrom(Arrays.asList(null, Instant.now().plus(10, ChronoUnit.DAYS))); final ApiKey apiKey1 = new ApiKey("api-key-name-1", "api-key-id-1", creation, expiration, false, - "user-x", "realm-1", randomMetadata()); + "user-x", "realm-1", ApiKeyTests.randomMetadata()); final ApiKey apiKey2 = new ApiKey("api-key-name-2", "api-key-id-2", creation, expiration, false, - "user-y", "realm-1", randomMetadata()); + "user-y", "realm-1", ApiKeyTests.randomMetadata()); final GetApiKeyResponse getApiKeyResponseExpectedWhenOwnerFlagIsTrue = new GetApiKeyResponse(Collections.singletonList(apiKey1)); final GetApiKeyResponse getApiKeyResponseExpectedWhenOwnerFlagIsFalse = new GetApiKeyResponse(List.of(apiKey1, apiKey2)); @@ -206,19 +207,6 @@ void doExecute(ActionType action, Request request, ActionListener randomMetadata() { - return randomFrom( - Map.of("application", randomAlphaOfLength(5), - "number", 1, - "numbers", List.of(1, 3, 5), - "environment", Map.of("os", "linux", "level", 42, "category", "trusted") - ), - Map.of(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)), - Map.of(), - null); - } - private static MapBuilder mapBuilder() { return MapBuilder.newMapBuilder(); } From 0bedaa07ea037e2c960d01e14a87d8b8faee2866 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 11 Mar 2021 20:56:16 +1100 Subject: [PATCH 08/16] fix test --- .../resources/rest-api-spec/test/mixed_cluster/120_api_key.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/120_api_key.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/120_api_key.yml index 20be95d12b39c..01870e1b73fa1 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/120_api_key.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/120_api_key.yml @@ -30,7 +30,7 @@ - do: node_selector: - version: "7.13.0 - " + version: "8.0.0 - " security.create_api_key: body: > { From 28d0e2883ea5cd922002b67a01b8b2c57d5b4dd3 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 11 Mar 2021 21:41:36 +1100 Subject: [PATCH 09/16] fix tests --- x-pack/qa/rolling-upgrade/build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/qa/rolling-upgrade/build.gradle b/x-pack/qa/rolling-upgrade/build.gradle index 15584b244c5ac..cf700d6c326b8 100644 --- a/x-pack/qa/rolling-upgrade/build.gradle +++ b/x-pack/qa/rolling-upgrade/build.gradle @@ -128,7 +128,9 @@ for (Version bwcVersion : BuildParams.bwcVersions.wireCompatible) { 'mixed_cluster/80_transform_jobs_crud/Test put batch transform on mixed cluster', 'mixed_cluster/80_transform_jobs_crud/Test put continuous transform on mixed cluster', 'mixed_cluster/90_ml_data_frame_analytics_crud/Put an outlier_detection job on the mixed cluster', - 'mixed_cluster/110_enrich/Enrich stats query smoke test for mixed cluster' + 'mixed_cluster/110_enrich/Enrich stats query smoke test for mixed cluster', + 'mixed_cluster/120_api_key/Test API key authentication will work in a mixed cluster', + 'mixed_cluster/120_api_key/Create API key with metadata in a mixed cluster' ].join(',') } From 9030640e400dfd129bf86705f90d56308147ac5d Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 11 Mar 2021 23:52:04 +1100 Subject: [PATCH 10/16] Update tests for HLRC --- .../client/security/CreateApiKeyRequest.java | 6 ++- .../client/security/support/ApiKey.java | 4 ++ .../SecurityRequestConvertersTests.java | 4 +- .../SecurityDocumentationIT.java | 41 +++++++++++++------ .../security/CreateApiKeyRequestTests.java | 39 +++++++++++++----- .../security/GrantApiKeyRequestTests.java | 35 ++++++++++++---- .../rest-api/security/get-api-keys.asciidoc | 3 +- 7 files changed, 98 insertions(+), 34 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java index dc627bb0e23bf..e3449d7c612fd 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java @@ -69,6 +69,10 @@ public RefreshPolicy getRefreshPolicy() { return refreshPolicy; } + public Map getMetadata() { + return metadata; + } + @Override public int hashCode() { return Objects.hash(name, refreshPolicy, roles, expiration); @@ -116,10 +120,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } builder.endObject(); } + builder.endObject(); if (metadata != null) { builder.field("metadata", metadata); } - builder.endObject(); return builder.endObject(); } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java index b4f81367ea078..93b97d862b6ce 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java @@ -94,6 +94,10 @@ public String getRealm() { return realm; } + public Map getMetadata() { + return metadata; + } + @Override public int hashCode() { return Objects.hash(name, id, creation, expiration, invalidated, username, realm); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java index 764f3823586e1..45b31474fa326 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java @@ -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; @@ -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 metadata = CreateApiKeyRequestTests.randomMetadata(); + final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy, metadata); return createApiKeyRequest; } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java index 69e5807a13140..acfac3dc652e7 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java @@ -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; @@ -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 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 @@ -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 listener; // tag::create-api-key-execute-listener @@ -2027,6 +2029,7 @@ public void testGrantApiKey() throws Exception { final Instant start = Instant.now(); + final Map metadata = CreateApiKeyRequestTests.randomMetadata(); CheckedConsumer apiKeyVerifier = (created) -> { final GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(created.getId(), false); final GetApiKeyResponse getApiKeyResponse = client.security().getApiKey(getApiKeyRequest, RequestOptions.DEFAULT); @@ -2039,6 +2042,11 @@ 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); @@ -2046,7 +2054,7 @@ public void testGrantApiKey() throws Exception { { 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 @@ -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); @@ -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 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", null); + Instant.now().plusMillis(expiration.getMillis()), false, "test_user", "default_file", metadata); { // tag::get-api-key-id-request GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId(), false); @@ -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 { @@ -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 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()); @@ -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()); @@ -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()); @@ -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()); @@ -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()); @@ -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()); @@ -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()); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateApiKeyRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateApiKeyRequestTests.java index 1a7b63aecec3e..466ff2a1958a7 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateApiKeyRequestTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateApiKeyRequestTests.java @@ -17,14 +17,17 @@ import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.EqualsHashCodeTestUtils; +import org.elasticsearch.test.XContentTestUtils; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import static org.hamcrest.Matchers.equalTo; @@ -38,16 +41,19 @@ public void test() throws IOException { roles.add(Role.builder().name("r2").clusterPrivileges(ClusterPrivilegeName.ALL) .indicesPrivileges(IndicesPrivileges.builder().indices("ind-y").privileges(IndexPrivilegeName.ALL).build()).build()); - CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest("api-key", roles, null, null); + final Map apiKeyMetadata = randomMetadata(); + CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest("api-key", roles, null, null, apiKeyMetadata); final XContentBuilder builder = XContentFactory.jsonBuilder(); createApiKeyRequest.toXContent(builder, ToXContent.EMPTY_PARAMS); final String output = Strings.toString(builder); + final String apiKeyMetadataString = apiKeyMetadata == null ? "" + : ",\"metadata\":" + XContentTestUtils.convertToXContent(apiKeyMetadata, XContentType.JSON).utf8ToString(); assertThat(output, equalTo( "{\"name\":\"api-key\",\"role_descriptors\":{\"r1\":{\"applications\":[],\"cluster\":[\"all\"],\"indices\":[{\"names\":" + "[\"ind-x\"],\"privileges\":[\"all\"],\"allow_restricted_indices\":false}],\"metadata\":{},\"run_as\":[]}," + "\"r2\":{\"applications\":[],\"cluster\":" + "[\"all\"],\"indices\":[{\"names\":[\"ind-y\"],\"privileges\":[\"all\"],\"allow_restricted_indices\":false}]," - + "\"metadata\":{},\"run_as\":[]}}}")); + + "\"metadata\":{},\"run_as\":[]}}" + apiKeyMetadataString + "}")); } public void testEqualsHashCode() { @@ -57,38 +63,49 @@ public void testEqualsHashCode() { final TimeValue expiration = null; final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); - CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy); + CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy, randomMetadata()); EqualsHashCodeTestUtils.checkEqualsAndHashCode(createApiKeyRequest, (original) -> { - return new CreateApiKeyRequest(original.getName(), original.getRoles(), original.getExpiration(), original.getRefreshPolicy()); + return new CreateApiKeyRequest(original.getName(), original.getRoles(), original.getExpiration(), original.getRefreshPolicy(), + original.getMetadata()); }); EqualsHashCodeTestUtils.checkEqualsAndHashCode(createApiKeyRequest, (original) -> { - return new CreateApiKeyRequest(original.getName(), original.getRoles(), original.getExpiration(), original.getRefreshPolicy()); + return new CreateApiKeyRequest(original.getName(), original.getRoles(), original.getExpiration(), original.getRefreshPolicy(), + original.getMetadata()); }, CreateApiKeyRequestTests::mutateTestItem); } private static CreateApiKeyRequest mutateTestItem(CreateApiKeyRequest original) { - switch (randomIntBetween(0, 3)) { + switch (randomIntBetween(0, 4)) { case 0: return new CreateApiKeyRequest(randomAlphaOfLength(5), original.getRoles(), original.getExpiration(), - original.getRefreshPolicy()); + original.getRefreshPolicy(), original.getMetadata()); case 1: return new CreateApiKeyRequest(original.getName(), Collections.singletonList(Role.builder().name(randomAlphaOfLength(6)).clusterPrivileges(ClusterPrivilegeName.ALL) .indicesPrivileges( IndicesPrivileges.builder().indices(randomAlphaOfLength(4)).privileges(IndexPrivilegeName.ALL).build()) .build()), - original.getExpiration(), original.getRefreshPolicy()); + original.getExpiration(), original.getRefreshPolicy(), original.getMetadata()); case 2: return new CreateApiKeyRequest(original.getName(), original.getRoles(), TimeValue.timeValueSeconds(10000), - original.getRefreshPolicy()); + original.getRefreshPolicy(), original.getMetadata()); case 3: List values = Arrays.stream(RefreshPolicy.values()).filter(rp -> rp != original.getRefreshPolicy()) .collect(Collectors.toList()); - return new CreateApiKeyRequest(original.getName(), original.getRoles(), original.getExpiration(), randomFrom(values)); + return new CreateApiKeyRequest(original.getName(), original.getRoles(), original.getExpiration(), randomFrom(values), + original.getMetadata()); + case 4: + return new CreateApiKeyRequest(original.getName(), original.getRoles(), original.getExpiration(), original.getRefreshPolicy(), + randomValueOtherThan(original.getMetadata(), CreateApiKeyRequestTests::randomMetadata)); default: return new CreateApiKeyRequest(randomAlphaOfLength(5), original.getRoles(), original.getExpiration(), - original.getRefreshPolicy()); + original.getRefreshPolicy(), original.getMetadata()); } } + + @SuppressWarnings("unchecked") + public static Map randomMetadata() { + return randomFrom(Map.of("status", "active"), Map.of(), null); + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GrantApiKeyRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GrantApiKeyRequestTests.java index 411dc061817fb..c837c52f717ea 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GrantApiKeyRequestTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GrantApiKeyRequestTests.java @@ -17,11 +17,14 @@ import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.EqualsHashCodeTestUtils; +import org.elasticsearch.test.XContentTestUtils; import java.io.IOException; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import static org.hamcrest.Matchers.equalTo; @@ -29,18 +32,22 @@ public class GrantApiKeyRequestTests extends ESTestCase { public void testToXContent() throws IOException { - final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest("api-key", List.of(), null, null); + final Map apiKeyMetadata = CreateApiKeyRequestTests.randomMetadata(); + final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest("api-key", List.of(), null, null, + apiKeyMetadata); final GrantApiKeyRequest.Grant grant = GrantApiKeyRequest.Grant.passwordGrant("kamala.khan", "JerseyGirl!".toCharArray()); final GrantApiKeyRequest grantApiKeyRequest = new GrantApiKeyRequest(grant, createApiKeyRequest); final XContentBuilder builder = XContentFactory.jsonBuilder(); grantApiKeyRequest.toXContent(builder, ToXContent.EMPTY_PARAMS); final String output = Strings.toString(builder); + final String apiKeyMetadataString = apiKeyMetadata == null ? "" + : ",\"metadata\":" + XContentTestUtils.convertToXContent(apiKeyMetadata, XContentType.JSON).utf8ToString(); assertThat(output, equalTo( "{" + "\"grant_type\":\"password\"," + "\"username\":\"kamala.khan\"," + "\"password\":\"JerseyGirl!\"," + - "\"api_key\":{\"name\":\"api-key\",\"role_descriptors\":{}}" + + "\"api_key\":{\"name\":\"api-key\",\"role_descriptors\":{}" + apiKeyMetadataString + "}" + "}")); } @@ -61,7 +68,8 @@ public void testEqualsHashCode() { final TimeValue expiration = randomBoolean() ? null : TimeValue.timeValueHours(randomIntBetween(4, 100)); final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); - final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy); + final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy, + CreateApiKeyRequestTests.randomMetadata()); final GrantApiKeyRequest.Grant grant = randomBoolean() ? GrantApiKeyRequest.Grant.passwordGrant(randomAlphaOfLength(8), randomAlphaOfLengthBetween(6, 12).toCharArray()) : GrantApiKeyRequest.Grant.accessTokenGrant(randomAlphaOfLength(24)); @@ -89,7 +97,8 @@ private CreateApiKeyRequest clone(CreateApiKeyRequest apiKeyRequest) { apiKeyRequest.getName(), apiKeyRequest.getRoles().stream().map(r -> Role.builder().clone(r).build()).collect(Collectors.toUnmodifiableList()), apiKeyRequest.getExpiration(), - apiKeyRequest.getRefreshPolicy() + apiKeyRequest.getRefreshPolicy(), + apiKeyRequest.getMetadata() ); } @@ -106,7 +115,8 @@ private static GrantApiKeyRequest mutateTestItem(GrantApiKeyRequest original) { randomAlphaOfLengthBetween(10, 15), original.getApiKeyRequest().getRoles(), original.getApiKeyRequest().getExpiration(), - original.getApiKeyRequest().getRefreshPolicy() + original.getApiKeyRequest().getRefreshPolicy(), + original.getApiKeyRequest().getMetadata() ) ); case 2: @@ -115,17 +125,28 @@ private static GrantApiKeyRequest mutateTestItem(GrantApiKeyRequest original) { original.getApiKeyRequest().getName(), List.of(), // No role limits original.getApiKeyRequest().getExpiration(), - original.getApiKeyRequest().getRefreshPolicy() + original.getApiKeyRequest().getRefreshPolicy(), + original.getApiKeyRequest().getMetadata() ) ); case 3: + return new GrantApiKeyRequest(original.getGrant(), + new CreateApiKeyRequest( + original.getApiKeyRequest().getName(), + original.getApiKeyRequest().getRoles(), + original.getApiKeyRequest().getExpiration(), + original.getApiKeyRequest().getRefreshPolicy(), + randomValueOtherThan(original.getApiKeyRequest().getMetadata(), CreateApiKeyRequestTests::randomMetadata) + ) + ); default: return new GrantApiKeyRequest(original.getGrant(), new CreateApiKeyRequest( original.getApiKeyRequest().getName(), original.getApiKeyRequest().getRoles(), TimeValue.timeValueMinutes(randomIntBetween(10, 120)), - original.getApiKeyRequest().getRefreshPolicy() + original.getApiKeyRequest().getRefreshPolicy(), + original.getApiKeyRequest().getMetadata() ) ); } diff --git a/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc b/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc index 817ea27b7b5d0..dce3f48370028 100644 --- a/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc +++ b/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc @@ -196,7 +196,8 @@ A successful call returns a JSON structure that contains the information of one "creation": 1548550550158, "invalidated": false, "username": "user-y", - "realm": "realm-2" + "realm": "realm-2", + "metadata": {} } ] } From c9a6667f16327b61601130949cc8aa8cbd5d1cb0 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 19 Mar 2021 00:25:43 +1100 Subject: [PATCH 11/16] Address feedback --- .../client/security/CreateApiKeyRequest.java | 2 +- .../client/security/support/ApiKey.java | 2 +- .../security/CreateApiKeyRequestTests.java | 51 ++++++++++++------- .../security/create-api-keys.asciidoc | 3 ++ .../security/action/CreateApiKeyRequest.java | 5 ++ .../action/CreateApiKeyRequestTests.java | 14 +++++ 6 files changed, 58 insertions(+), 19 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java index e3449d7c612fd..fded84a1672a5 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java @@ -75,7 +75,7 @@ public Map getMetadata() { @Override public int hashCode() { - return Objects.hash(name, refreshPolicy, roles, expiration); + return Objects.hash(name, refreshPolicy, roles, expiration, metadata); } @Override diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java index 93b97d862b6ce..1503dc7f57d6e 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java @@ -100,7 +100,7 @@ public Map getMetadata() { @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 diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateApiKeyRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateApiKeyRequestTests.java index 466ff2a1958a7..c68530357a4ee 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateApiKeyRequestTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateApiKeyRequestTests.java @@ -12,20 +12,17 @@ import org.elasticsearch.client.security.user.privileges.Role; import org.elasticsearch.client.security.user.privileges.Role.ClusterPrivilegeName; import org.elasticsearch.client.security.user.privileges.Role.IndexPrivilegeName; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.xcontent.ToXContent; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.EqualsHashCodeTestUtils; -import org.elasticsearch.test.XContentTestUtils; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -43,17 +40,33 @@ public void test() throws IOException { final Map apiKeyMetadata = randomMetadata(); CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest("api-key", roles, null, null, apiKeyMetadata); - final XContentBuilder builder = XContentFactory.jsonBuilder(); - createApiKeyRequest.toXContent(builder, ToXContent.EMPTY_PARAMS); - final String output = Strings.toString(builder); - final String apiKeyMetadataString = apiKeyMetadata == null ? "" - : ",\"metadata\":" + XContentTestUtils.convertToXContent(apiKeyMetadata, XContentType.JSON).utf8ToString(); - assertThat(output, equalTo( - "{\"name\":\"api-key\",\"role_descriptors\":{\"r1\":{\"applications\":[],\"cluster\":[\"all\"],\"indices\":[{\"names\":" - + "[\"ind-x\"],\"privileges\":[\"all\"],\"allow_restricted_indices\":false}],\"metadata\":{},\"run_as\":[]}," - + "\"r2\":{\"applications\":[],\"cluster\":" - + "[\"all\"],\"indices\":[{\"names\":[\"ind-y\"],\"privileges\":[\"all\"],\"allow_restricted_indices\":false}]," - + "\"metadata\":{},\"run_as\":[]}}" + apiKeyMetadataString + "}")); + + Map expected = new HashMap<>(Map.of( + "name", "api-key", + "role_descriptors", Map.of( + "r1", Map.of( + "applications", List.of(), + "cluster", List.of("all"), + "indices", List.of( + Map.of("names", List.of("ind-x"), "privileges", List.of("all"), "allow_restricted_indices", false)), + "metadata", Map.of(), + "run_as", List.of()), + "r2", Map.of( + "applications", List.of(), + "cluster", List.of("all"), + "indices", List.of( + Map.of("names", List.of("ind-y"), "privileges", List.of("all"), "allow_restricted_indices", false)), + "metadata", Map.of(), + "run_as", List.of())) + )); + if (apiKeyMetadata != null) { + expected.put("metadata", apiKeyMetadata); + } + + assertThat( + XContentHelper.convertToMap(XContentHelper.toXContent( + createApiKeyRequest, XContentType.JSON, false), false, XContentType.JSON).v2(), + equalTo(expected)); } public void testEqualsHashCode() { @@ -106,6 +119,10 @@ private static CreateApiKeyRequest mutateTestItem(CreateApiKeyRequest original) @SuppressWarnings("unchecked") public static Map randomMetadata() { - return randomFrom(Map.of("status", "active"), Map.of(), null); + return randomFrom( + Map.of("status", "active", "level", 42, "nested", Map.of("foo", "bar")), + Map.of("status", "active"), + Map.of(), + null); } } diff --git a/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc b/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc index b972d3e7cf3e8..0589a27083787 100644 --- a/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc +++ b/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc @@ -76,6 +76,9 @@ expire. `metadata`:: (object) Arbitrary metadata that you want to associate with the API key. +It supports nested data structure. +Within the `metadata` object, keys beginning with `_` are reserved for +system usage. [[security-api-create-api-key-example]] ==== {api-examples-title} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequest.java index 816fbd82dffec..0b7d918361e92 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequest.java @@ -18,6 +18,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.support.MetadataUtils; import java.io.IOException; import java.util.Collections; @@ -152,6 +153,10 @@ public ActionRequestValidationException validate() { validationException = addValidationError("api key name may not begin with an underscore", validationException); } } + if (metadata != null && MetadataUtils.containsReservedMetadata(metadata)) { + validationException = + addValidationError("metadata keys may not start with [" + MetadataUtils.RESERVED_PREFIX + "]", validationException); + } return validationException; } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestTests.java index 98d51545b8a0e..244d1a5af8af1 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestTests.java @@ -15,12 +15,15 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.support.MetadataUtils; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; public class CreateApiKeyRequestTests extends ESTestCase { @@ -72,6 +75,17 @@ public void testNameValidation() { assertThat(ve.validationErrors().get(0), containsString("api key name may not begin with an underscore")); } + public void testMetadataKeyValidation() { + final String name = randomAlphaOfLengthBetween(1, 256); + CreateApiKeyRequest request = new CreateApiKeyRequest(); + request.setName(name); + request.setMetadata(Map.of("_foo", "bar")); + final ActionRequestValidationException ve = request.validate(); + assertNotNull(ve); + assertThat(ve.validationErrors().size(), equalTo(1)); + assertThat(ve.validationErrors().get(0), containsString("metadata keys may not start with [_]")); + } + public void testSerialization() throws IOException { final String name = randomAlphaOfLengthBetween(1, 256); final TimeValue expiration = randomBoolean() ? null : From 0d6b0c2f7921c23ed95362f11904cc26844d10bc Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 19 Mar 2021 09:03:22 +1100 Subject: [PATCH 12/16] checkstyle --- .../xpack/core/security/action/CreateApiKeyRequestTests.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestTests.java index 244d1a5af8af1..6e8d5a3d46b2b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestTests.java @@ -15,7 +15,6 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; -import org.elasticsearch.xpack.core.security.support.MetadataUtils; import java.io.IOException; import java.util.ArrayList; From d05d1f96097af209a9d229061599195ad8f2ed59 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 19 Mar 2021 11:15:59 +1100 Subject: [PATCH 13/16] Fix test --- .../rest-api-spec/test/upgraded_cluster/120_api_key.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/120_api_key.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/120_api_key.yml index 2e3769dbf813e..3e010cb835df0 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/120_api_key.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/120_api_key.yml @@ -1,5 +1,5 @@ --- -"Create API key in the upgraded cluster": +"Create and Get API key in the upgraded cluster": - skip: features: headers @@ -15,9 +15,6 @@ - is_true: id - is_true: api_key ---- -"Get API keys in the upgraded cluster": - - do: security.get_api_key: name: "my-old-api-key" From fb53e14c107c1c2f70beb142368a050f76f99a1d Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 26 Mar 2021 10:51:15 +1100 Subject: [PATCH 14/16] address feedback --- .../xpack/core/security/action/ApiKey.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java index 1329963db2f8d..1b80d6d8a8747 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java @@ -38,7 +38,6 @@ public final class ApiKey implements ToXContentObject, Writeable { private final boolean invalidated; private final String username; private final String realm; - @Nullable private final Map metadata; public ApiKey(String name, String id, Instant creation, Instant expiration, boolean invalidated, String username, String realm, @@ -53,7 +52,7 @@ public ApiKey(String name, String id, Instant creation, Instant expiration, bool this.invalidated = invalidated; this.username = username; this.realm = realm; - this.metadata = metadata; + this.metadata = metadata == null ? Map.of() : metadata; } public ApiKey(StreamInput in) throws IOException { @@ -71,7 +70,7 @@ public ApiKey(StreamInput in) throws IOException { if (in.getVersion().onOrAfter(Version.V_8_0_0)) { this.metadata = in.readMap(); } else { - this.metadata = null; + this.metadata = Map.of(); } } @@ -143,7 +142,7 @@ public void writeTo(StreamOutput out) throws IOException { @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 @@ -164,7 +163,8 @@ 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") From 691b3501741c2244914bd7c9af7e2e2ace8c97b0 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 26 Mar 2021 10:57:54 +1100 Subject: [PATCH 15/16] tweak --- x-pack/docs/en/rest-api/security/create-api-keys.asciidoc | 2 -- .../elasticsearch/xpack/core/security/action/ApiKeyTests.java | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc b/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc index a44710fdf1d2c..84002f2e3fba7 100644 --- a/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc +++ b/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc @@ -28,8 +28,6 @@ See the note under `role_descriptors`. The API keys are created by the {es} API key service, which is automatically enabled when you configure TLS on the HTTP interface. See <>. Alternatively, - - you can explicitly enable the `xpack.security.authc.api_key.enabled` setting. When you are running in production mode, a bootstrap check prevents you from enabling the API key service unless you also enable TLS on the HTTP interface. diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/ApiKeyTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/ApiKeyTests.java index 9ef0051daf410..46f899049e057 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/ApiKeyTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/ApiKeyTests.java @@ -22,6 +22,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; public class ApiKeyTests extends ESTestCase { @@ -38,6 +39,8 @@ public void testXContent() throws IOException { final Map metadata = randomMetadata(); final ApiKey apiKey = new ApiKey(name, id, creation, expiration, invalidated, username, realmName, metadata); + // The metadata will never be null because the constructor convert it to empty map if a null is passed in + assertThat(apiKey.getMetadata(), notNullValue()); XContentBuilder builder = XContentFactory.jsonBuilder(); apiKey.toXContent(builder, ToXContent.EMPTY_PARAMS); From 39c0f6acb8e5853923601fa4909bf9010da39889 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 26 Mar 2021 12:02:01 +1100 Subject: [PATCH 16/16] fix tests --- .../elasticsearch/xpack/security/authc/ApiKeyIntegTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java index 8aa18156a1ed5..190e7441a18c5 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java @@ -1199,7 +1199,8 @@ private void verifyGetResponse(String[] user, int expectedNumberOfApiKeys, List< (m, i) -> m.put(responses.get(i).getId(), metadatas.get(i)), HashMap::putAll); for (ApiKey apiKey : response.getApiKeyInfos()) { - assertThat(apiKey.getMetadata(), equalTo(idToMetadata.get(apiKey.getId()))); + final Map metadata = idToMetadata.get(apiKey.getId()); + assertThat(apiKey.getMetadata(), equalTo(metadata == null ? Map.of() : metadata)); } } }