From dfaf71c6d83637fb43a5b19cf95b8429a16c2890 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Mon, 19 Feb 2024 17:35:31 +0200 Subject: [PATCH 1/8] ApiKey with realm_type --- .../core/security/action/apikey/ApiKey.java | 27 ++++++++++++++++--- .../security/action/apikey/ApiKeyTests.java | 3 +++ .../action/apikey/GetApiKeyResponseTests.java | 9 +++++++ .../xpack/security/authc/ApiKeyService.java | 1 + .../apikey/RestGetApiKeyActionTests.java | 4 +++ 5 files changed, 40 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java index 3ab487560f2b8..06e8c619d9549 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java @@ -88,6 +88,7 @@ public String value() { private final Instant invalidation; private final String username; private final String realm; + private final String realmType; private final Map metadata; @Nullable private final List roleDescriptors; @@ -104,6 +105,7 @@ public ApiKey( @Nullable Instant invalidation, String username, String realm, + @Nullable String realmType, @Nullable Map metadata, @Nullable List roleDescriptors, @Nullable List limitedByRoleDescriptors @@ -118,6 +120,7 @@ public ApiKey( invalidation, username, realm, + realmType, metadata, roleDescriptors, limitedByRoleDescriptors == null ? null : new RoleDescriptorsIntersection(List.of(Set.copyOf(limitedByRoleDescriptors))) @@ -134,6 +137,7 @@ private ApiKey( Instant invalidation, String username, String realm, + @Nullable String realmType, @Nullable Map metadata, @Nullable List roleDescriptors, @Nullable RoleDescriptorsIntersection limitedBy @@ -150,6 +154,7 @@ private ApiKey( this.invalidation = (invalidation != null) ? Instant.ofEpochMilli(invalidation.toEpochMilli()) : null; this.username = username; this.realm = realm; + this.realmType = realmType; this.metadata = metadata == null ? Map.of() : metadata; this.roleDescriptors = roleDescriptors != null ? List.copyOf(roleDescriptors) : null; // This assertion will need to be changed (or removed) when derived keys are properly supported @@ -193,6 +198,10 @@ public String getRealm() { return realm; } + public @Nullable String getRealmType() { + return realmType; + } + public Map getMetadata() { return metadata; } @@ -223,7 +232,11 @@ public XContentBuilder innerToXContent(XContentBuilder builder, Params params) t if (invalidation != null) { builder.field("invalidation", invalidation.toEpochMilli()); } - builder.field("username", username).field("realm", realm).field("metadata", (metadata == null ? Map.of() : metadata)); + builder.field("username", username).field("realm", realm); + if (realmType != null) { + builder.field("realm_type", realmType); + } + builder.field("metadata", (metadata == null ? Map.of() : metadata)); if (roleDescriptors != null) { builder.startObject("role_descriptors"); for (var roleDescriptor : roleDescriptors) { @@ -287,6 +300,7 @@ public int hashCode() { invalidation, username, realm, + realmType, metadata, roleDescriptors, limitedBy @@ -314,6 +328,7 @@ public boolean equals(Object obj) { && Objects.equals(invalidation, other.invalidation) && Objects.equals(username, other.username) && Objects.equals(realm, other.realm) + && Objects.equals(realmType, other.realmType) && Objects.equals(metadata, other.metadata) && Objects.equals(roleDescriptors, other.roleDescriptors) && Objects.equals(limitedBy, other.limitedBy); @@ -331,9 +346,10 @@ public boolean equals(Object obj) { (args[6] == null) ? null : Instant.ofEpochMilli((Long) args[6]), (String) args[7], (String) args[8], - (args[9] == null) ? null : (Map) args[9], - (List) args[10], - (RoleDescriptorsIntersection) args[11] + (String) args[9], + (args[10] == null) ? null : (Map) args[10], + (List) args[11], + (RoleDescriptorsIntersection) args[12] ); }); static { @@ -346,6 +362,7 @@ public boolean equals(Object obj) { PARSER.declareLong(optionalConstructorArg(), new ParseField("invalidation")); PARSER.declareString(constructorArg(), new ParseField("username")); PARSER.declareString(constructorArg(), new ParseField("realm")); + PARSER.declareStringOrNull(optionalConstructorArg(), new ParseField("realm_type")); PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField("metadata")); PARSER.declareNamedObjects(optionalConstructorArg(), (p, c, n) -> { p.nextToken(); @@ -383,6 +400,8 @@ public String toString() { + username + ", realm=" + realm + + ", realm_type=" + + realmType + ", metadata=" + metadata + ", role_descriptors=" diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKeyTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKeyTests.java index 02bce50ed3483..361928590556a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKeyTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKeyTests.java @@ -68,6 +68,7 @@ public void testXContent() throws IOException { assertThat(map.get("invalidated"), is(apiKey.isInvalidated())); assertThat(map.get("username"), equalTo(apiKey.getUsername())); assertThat(map.get("realm"), equalTo(apiKey.getRealm())); + assertThat(map.get("realm_type"), equalTo(apiKey.getRealmType())); assertThat(map.get("metadata"), equalTo(Objects.requireNonNullElseGet(apiKey.getMetadata(), Map::of))); if (apiKey.getRoleDescriptors() == null) { @@ -172,6 +173,7 @@ public static ApiKey randomApiKeyInstance() { : null; final String username = randomAlphaOfLengthBetween(4, 10); final String realmName = randomAlphaOfLengthBetween(3, 8); + final String realmType = randomFrom(randomAlphaOfLengthBetween(3, 8), null); final Map metadata = randomMetadata(); final List roleDescriptors = type == ApiKey.Type.CROSS_CLUSTER ? List.of(randomCrossClusterAccessRoleDescriptor()) @@ -190,6 +192,7 @@ public static ApiKey randomApiKeyInstance() { invalidation, username, realmName, + realmType, metadata, roleDescriptors, limitedByRoleDescriptors diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyResponseTests.java index 0b287f2fb6329..d5de84045096a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyResponseTests.java @@ -61,6 +61,7 @@ public void testToXContent() throws IOException { "realm-x", null, null, + null, List.of() // empty limited-by role descriptor to simulate derived keys ); ApiKey apiKeyInfo2 = createApiKeyInfo( @@ -73,6 +74,7 @@ public void testToXContent() throws IOException { Instant.ofEpochMilli(100000000L), "user-b", "realm-y", + "realm-type-y", Map.of(), List.of(), limitedByRoleDescriptors @@ -87,6 +89,7 @@ public void testToXContent() throws IOException { Instant.ofEpochMilli(100000000L), "user-c", "realm-z", + "realm-type-z", Map.of("foo", "bar"), roleDescriptors, limitedByRoleDescriptors @@ -111,6 +114,7 @@ public void testToXContent() throws IOException { Instant.ofEpochMilli(100000000L), "user-c", "realm-z", + "realm-type-z", Map.of("foo", "bar"), crossClusterAccessRoleDescriptors, null @@ -145,6 +149,7 @@ public void testToXContent() throws IOException { "invalidation": 100000000, "username": "user-b", "realm": "realm-y", + "realm_type": "realm-type-y", "metadata": {}, "role_descriptors": {}, "limited_by": [ @@ -185,6 +190,7 @@ public void testToXContent() throws IOException { "invalidation": 100000000, "username": "user-c", "realm": "realm-z", + "realm_type": "realm-type-z", "metadata": { "foo": "bar" }, @@ -252,6 +258,7 @@ public void testToXContent() throws IOException { "invalidation": 100000000, "username": "user-c", "realm": "realm-z", + "realm_type": "realm-type-z", "metadata": { "foo": "bar" }, @@ -321,6 +328,7 @@ private ApiKey createApiKeyInfo( Instant invalidation, String username, String realm, + String realmType, Map metadata, List roleDescriptors, List limitedByRoleDescriptors @@ -335,6 +343,7 @@ private ApiKey createApiKeyInfo( invalidation, username, realm, + realmType, metadata, roleDescriptors, limitedByRoleDescriptors 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 7cf045ad0f9f5..e7d4ac0c061ea 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 @@ -2004,6 +2004,7 @@ private ApiKey convertSearchHitToApiKeyInfo(SearchHit hit, boolean withLimitedBy apiKeyDoc.invalidation != -1 ? Instant.ofEpochMilli(apiKeyDoc.invalidation) : null, (String) apiKeyDoc.creator.get("principal"), (String) apiKeyDoc.creator.get("realm"), + (String) apiKeyDoc.creator.get("realm_type"), metadata, roleDescriptors, limitedByRoleDescriptors 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 2ee42b360f02a..76a01f100b8ad 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 @@ -117,6 +117,7 @@ public void sendResponse(RestResponse restResponse) { null, "user-x", "realm-1", + "realm-type-1", metadata, roleDescriptors, limitedByRoleDescriptors @@ -176,6 +177,7 @@ public void doE null, "user-x", "realm-1", + "realm-type-1", metadata, roleDescriptors, limitedByRoleDescriptors @@ -226,6 +228,7 @@ public void sendResponse(RestResponse restResponse) { null, "user-x", "realm-1", + "realm-type-1", ApiKeyTests.randomMetadata(), type == ApiKey.Type.CROSS_CLUSTER ? List.of(randomCrossClusterAccessRoleDescriptor()) @@ -242,6 +245,7 @@ public void sendResponse(RestResponse restResponse) { null, "user-y", "realm-1", + "realm-type-1", ApiKeyTests.randomMetadata(), type == ApiKey.Type.CROSS_CLUSTER ? List.of(randomCrossClusterAccessRoleDescriptor()) From 3bd8b2410ceab1caf2b4fd007ace25b15435eb10 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Mon, 19 Feb 2024 18:05:11 +0200 Subject: [PATCH 2/8] Testsssss --- .../xpack/security/apikey/ApiKeyRestIT.java | 4 ++++ .../security/authc/apikey/ApiKeySingleNodeTests.java | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java index 4e639e14eda6e..850dfe5dffa99 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java @@ -300,6 +300,8 @@ public void testGrantApiKeyForOtherUserWithPassword() throws IOException { ApiKey apiKey = getApiKey((String) responseBody.get("id")); assertThat(apiKey.getUsername(), equalTo(END_USER)); + assertThat(apiKey.getRealm(), equalTo("default_native")); + assertThat(apiKey.getRealmType(), equalTo("native")); } public void testGrantApiKeyForOtherUserWithAccessToken() throws IOException { @@ -329,6 +331,8 @@ public void testGrantApiKeyForOtherUserWithAccessToken() throws IOException { ApiKey apiKey = getApiKey((String) responseBody.get("id")); assertThat(apiKey.getUsername(), equalTo(END_USER)); + assertThat(apiKey.getRealm(), equalTo("default_native")); + assertThat(apiKey.getRealmType(), equalTo("native")); Instant minExpiry = before.plus(2, ChronoUnit.HOURS); Instant maxExpiry = after.plus(2, ChronoUnit.HOURS); diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java index bffc6c165c818..707e7b2846a9b 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java @@ -312,7 +312,10 @@ public void testGrantApiKeyForUserWithRunAs() throws IOException { final String apiKeyId = createApiKeyResponse.getId(); final String base64ApiKeyKeyValue = Base64.getEncoder() .encodeToString((apiKeyId + ":" + createApiKeyResponse.getKey().toString()).getBytes(StandardCharsets.UTF_8)); - assertThat(securityClient.getApiKey(apiKeyId).getUsername(), equalTo("user2")); + ApiKey apiKey = securityClient.getApiKey(apiKeyId); + assertThat(apiKey.getUsername(), equalTo("user2")); + assertThat(apiKey.getRealm(), equalTo("index")); + assertThat(apiKey.getRealmType(), equalTo("native")); final Client clientWithGrantedKey = client().filterWithHeader(Map.of("Authorization", "ApiKey " + base64ApiKeyKeyValue)); // The API key has privileges (inherited from user2) to check cluster health clientWithGrantedKey.execute(TransportClusterHealthAction.TYPE, new ClusterHealthRequest()).actionGet(); @@ -618,6 +621,7 @@ public void testCreateCrossClusterApiKey() throws IOException { assertThat(getApiKeyInfo.getMetadata(), anEmptyMap()); assertThat(getApiKeyInfo.getUsername(), equalTo("test_user")); assertThat(getApiKeyInfo.getRealm(), equalTo("file")); + assertThat(getApiKeyInfo.getRealmType(), equalTo("file")); // Check the API key attributes with Query API final QueryApiKeyRequest queryApiKeyRequest = new QueryApiKeyRequest( @@ -638,6 +642,7 @@ public void testCreateCrossClusterApiKey() throws IOException { assertThat(queryApiKeyInfo.getMetadata(), anEmptyMap()); assertThat(queryApiKeyInfo.getUsername(), equalTo("test_user")); assertThat(queryApiKeyInfo.getRealm(), equalTo("file")); + assertThat(queryApiKeyInfo.getRealmType(), equalTo("file")); } public void testUpdateCrossClusterApiKey() throws IOException { @@ -672,6 +677,7 @@ public void testUpdateCrossClusterApiKey() throws IOException { assertThat(getApiKeyInfo.getMetadata(), anEmptyMap()); assertThat(getApiKeyInfo.getUsername(), equalTo("test_user")); assertThat(getApiKeyInfo.getRealm(), equalTo("file")); + assertThat(getApiKeyInfo.getRealmType(), equalTo("file")); final CrossClusterApiKeyRoleDescriptorBuilder roleDescriptorBuilder; final boolean shouldUpdateAccess = randomBoolean(); @@ -745,6 +751,7 @@ public void testUpdateCrossClusterApiKey() throws IOException { assertThat(queryApiKeyInfo.getMetadata(), equalTo(updateMetadata == null ? Map.of() : updateMetadata)); assertThat(queryApiKeyInfo.getUsername(), equalTo("test_user")); assertThat(queryApiKeyInfo.getRealm(), equalTo("file")); + assertThat(queryApiKeyInfo.getRealmType(), equalTo("file")); } // Cross-cluster API keys cannot be created by an API key even if it has manage_security privilege From 5e69bdae2df894eca90f042fb011621c88c28cd6 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Mon, 19 Feb 2024 18:46:10 +0200 Subject: [PATCH 3/8] @Nullable RealmConfig.RealmIdentifier getRealmIdentifier --- .../xpack/core/security/action/apikey/ApiKey.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java index 06e8c619d9549..cbaa341d14965 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java @@ -16,6 +16,7 @@ import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection; @@ -202,6 +203,13 @@ public String getRealm() { return realmType; } + public @Nullable RealmConfig.RealmIdentifier getRealmIdentifier() { + if (realm != null && realmType != null) { + return new RealmConfig.RealmIdentifier(realmType, realm); + } + return null; + } + public Map getMetadata() { return metadata; } From 16168ae6ec2153c630e76a958756a89662471eab Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Mon, 19 Feb 2024 19:10:54 +0200 Subject: [PATCH 4/8] Some ApiKeyServiceTests.java --- .../security/authc/ApiKeyServiceTests.java | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) 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 ac11dee8d4a48..b46f1a23a2371 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 @@ -54,6 +54,7 @@ import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.TimeValue; +import org.elasticsearch.core.Tuple; import org.elasticsearch.index.get.GetResult; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilders; @@ -890,6 +891,24 @@ private Map mockKeyDocument( Duration expiry, @Nullable List keyRoles, ApiKey.Type type + ) throws IOException { + var apiKeyDoc = newApiKeyDocument(key, user, authUser, invalidated, expiry, keyRoles, type); + SecurityMocks.mockGetRequest( + client, + id, + BytesReference.bytes(XContentBuilder.builder(XContentType.JSON.xContent()).map(apiKeyDoc.v1())) + ); + return apiKeyDoc.v2(); + } + + private static Tuple, Map> newApiKeyDocument( + String key, + User user, + @Nullable User authUser, + boolean invalidated, + Duration expiry, + @Nullable List keyRoles, + ApiKey.Type type ) throws IOException { final Authentication authentication; if (authUser != null) { @@ -906,7 +925,7 @@ private Map mockKeyDocument( .realmRef(new RealmRef("realm1", "native", "node01")) .build(false); } - final Map metadata = ApiKeyTests.randomMetadata(); + Map metadataMap = ApiKeyTests.randomMetadata(); XContentBuilder docSource = ApiKeyService.newDocument( getFastStoredHashAlgoForTests().hash(new SecureString(key.toCharArray())), "test", @@ -917,15 +936,13 @@ private Map mockKeyDocument( keyRoles, type, Version.CURRENT, - metadata + metadataMap ); + Map keyMap = XContentHelper.convertToMap(BytesReference.bytes(docSource), true, XContentType.JSON).v2(); 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); + keyMap.put("api_key_invalidated", true); } - SecurityMocks.mockGetRequest(client, id, BytesReference.bytes(docSource)); - return metadata; + return new Tuple<>(keyMap, metadataMap); } private AuthenticationResult tryAuthenticate(ApiKeyService service, String id, String key, ApiKey.Type type) throws Exception { From 0f35795e36c5f47c701ea923e9877da8c502f632 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Tue, 20 Feb 2024 13:34:39 +0200 Subject: [PATCH 5/8] Test --- .../xpack/security/authc/ApiKeyService.java | 1 - .../security/authc/ApiKeyServiceTests.java | 111 ++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) 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 e7d4ac0c061ea..fea0c812e7e42 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 @@ -1937,7 +1937,6 @@ public void getApiKeys( public void queryApiKeys(SearchRequest searchRequest, boolean withLimitedBy, ActionListener listener) { ensureEnabled(); - final SecurityIndexManager frozenSecurityIndex = securityIndex.defensiveCopy(); if (frozenSecurityIndex.indexExists() == false) { logger.debug("security index does not exist"); 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 b46f1a23a2371..df454ddffe96f 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 @@ -30,12 +30,14 @@ import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.search.TransportSearchAction; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.update.UpdateRequestBuilder; import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.client.internal.Client; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.CheckedSupplier; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; @@ -92,12 +94,14 @@ import org.elasticsearch.xpack.core.security.action.apikey.CrossClusterApiKeyRoleDescriptorBuilder; import org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyResponse; import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper; import org.elasticsearch.xpack.core.security.authc.AuthenticationTests; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmDomain; import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer; import org.elasticsearch.xpack.core.security.authc.support.Hasher; @@ -325,6 +329,109 @@ public void testGetApiKeys() throws Exception { assertThat(getApiKeyResponse.getApiKeyInfos(), emptyArray()); } + @SuppressWarnings("unchecked") + public void testApiKeysOwnerRealmIdentifier() throws Exception { + String realm1 = randomAlphaOfLength(4); + String realm1Type = randomAlphaOfLength(4); + String realm2 = randomAlphaOfLength(4); + when(clock.instant()).thenReturn(Instant.ofEpochMilli(randomMillisUpToYear9999())); + when(client.threadPool()).thenReturn(threadPool); + when(client.prepareSearch(eq(SECURITY_MAIN_ALIAS))).thenReturn(new SearchRequestBuilder(client)); + ApiKeyService service = createApiKeyService( + Settings.builder().put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true).build() + ); + CheckedSupplier searchResponseSupplier = () -> { + // 2 API keys, one with a "null" (missing) realm type + SearchHit[] searchHits = new SearchHit[2]; + searchHits[0] = SearchHit.unpooled(randomIntBetween(0, Integer.MAX_VALUE), "0"); + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + Map apiKeySourceDoc = buildApiKeySourceDoc("some_hash".toCharArray()); + ((Map) apiKeySourceDoc.get("creator")).put("realm", realm1); + ((Map) apiKeySourceDoc.get("creator")).put("realm_type", realm1Type); + builder.map(apiKeySourceDoc); + searchHits[0].sourceRef(BytesReference.bytes(builder)); + } + searchHits[1] = SearchHit.unpooled(randomIntBetween(0, Integer.MAX_VALUE), "1"); + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + Map apiKeySourceDoc = buildApiKeySourceDoc("some_hash".toCharArray()); + ((Map) apiKeySourceDoc.get("creator")).put("realm", realm2); + if (randomBoolean()) { + ((Map) apiKeySourceDoc.get("creator")).put("realm_type", null); + } else { + ((Map) apiKeySourceDoc.get("creator")).remove("realm_type"); + } + builder.map(apiKeySourceDoc); + searchHits[1].sourceRef(BytesReference.bytes(builder)); + } + return new SearchResponse( + SearchHits.unpooled( + searchHits, + new TotalHits(searchHits.length, TotalHits.Relation.EQUAL_TO), + randomFloat(), + null, + null, + null + ), + null, + null, + false, + null, + null, + 0, + randomAlphaOfLengthBetween(3, 8), + 1, + 1, + 0, + 10, + null, + null + ); + }; + doAnswer(invocation -> { + ActionListener.respondAndRelease((ActionListener) invocation.getArguments()[1], searchResponseSupplier.get()); + return null; + }).when(client).search(any(SearchRequest.class), anyActionListener()); + doAnswer(invocation -> { + ActionListener.respondAndRelease((ActionListener) invocation.getArguments()[2], searchResponseSupplier.get()); + return null; + }).when(client).execute(eq(TransportSearchAction.TYPE), any(SearchRequest.class), anyActionListener()); + { + PlainActionFuture getApiKeyResponsePlainActionFuture = new PlainActionFuture<>(); + service.getApiKeys( + generateRandomStringArray(4, 4, true, true), + randomFrom(randomAlphaOfLengthBetween(3, 8), null), + randomFrom(randomAlphaOfLengthBetween(3, 8), null), + generateRandomStringArray(4, 4, true, true), + randomBoolean(), + randomBoolean(), + getApiKeyResponsePlainActionFuture + ); + GetApiKeyResponse getApiKeyResponse = getApiKeyResponsePlainActionFuture.get(); + assertThat(getApiKeyResponse.getApiKeyInfos().length, is(2)); + assertThat(getApiKeyResponse.getApiKeyInfos()[0].getRealm(), is(realm1)); + assertThat(getApiKeyResponse.getApiKeyInfos()[0].getRealmType(), is(realm1Type)); + assertThat(getApiKeyResponse.getApiKeyInfos()[0].getRealmIdentifier(), is(new RealmConfig.RealmIdentifier(realm1Type, realm1))); + assertThat(getApiKeyResponse.getApiKeyInfos()[1].getRealm(), is(realm2)); + assertThat(getApiKeyResponse.getApiKeyInfos()[1].getRealmType(), nullValue()); + assertThat(getApiKeyResponse.getApiKeyInfos()[1].getRealmIdentifier(), nullValue()); + } + { + PlainActionFuture queryApiKeyResponsePlainActionFuture = new PlainActionFuture<>(); + service.queryApiKeys(new SearchRequest(".security"), false, queryApiKeyResponsePlainActionFuture); + QueryApiKeyResponse queryApiKeyResponse = queryApiKeyResponsePlainActionFuture.get(); + assertThat(queryApiKeyResponse.getItems().length, is(2)); + assertThat(queryApiKeyResponse.getItems()[0].getApiKey().getRealm(), is(realm1)); + assertThat(queryApiKeyResponse.getItems()[0].getApiKey().getRealmType(), is(realm1Type)); + assertThat( + queryApiKeyResponse.getItems()[0].getApiKey().getRealmIdentifier(), + is(new RealmConfig.RealmIdentifier(realm1Type, realm1)) + ); + assertThat(queryApiKeyResponse.getItems()[1].getApiKey().getRealm(), is(realm2)); + assertThat(queryApiKeyResponse.getItems()[1].getApiKey().getRealmType(), nullValue()); + assertThat(queryApiKeyResponse.getItems()[1].getApiKey().getRealmIdentifier(), nullValue()); + } + } + @SuppressWarnings("unchecked") public void testInvalidateApiKeys() throws Exception { final Settings settings = Settings.builder().put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true).build(); @@ -2877,6 +2984,10 @@ private Map buildApiKeySourceDoc(char[] hash) { creatorMap.put("full_name", "test user"); creatorMap.put("email", "test@user.com"); creatorMap.put("metadata", Collections.emptyMap()); + creatorMap.put("realm", randomAlphaOfLength(4)); + if (randomBoolean()) { + creatorMap.put("realm_type", randomAlphaOfLength(4)); + } sourceMap.put("creator", creatorMap); sourceMap.put("api_key_invalidated", false); // noinspection unchecked From cd4274f9e772e786a1648ad7503064667fbd04a0 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Tue, 20 Feb 2024 13:40:15 +0200 Subject: [PATCH 6/8] realm_type in docs --- docs/reference/rest-api/security/get-api-keys.asciidoc | 2 ++ docs/reference/rest-api/security/query-api-key.asciidoc | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/reference/rest-api/security/get-api-keys.asciidoc b/docs/reference/rest-api/security/get-api-keys.asciidoc index d75edda9296a5..a02e8adb67b4f 100644 --- a/docs/reference/rest-api/security/get-api-keys.asciidoc +++ b/docs/reference/rest-api/security/get-api-keys.asciidoc @@ -134,6 +134,7 @@ A successful call returns a JSON structure that contains the information of the "invalidated": false, <6> "username": "myuser", <7> "realm": "native1", <8> + "realm_type": "native", "metadata": { <9> "application": "myapp" }, @@ -289,6 +290,7 @@ A successful call returns a JSON structure that contains the information of one "invalidated": false, "username": "myuser", "realm": "native1", + "realm_type": "native", "metadata": { "application": "myapp" }, diff --git a/docs/reference/rest-api/security/query-api-key.asciidoc b/docs/reference/rest-api/security/query-api-key.asciidoc index 88fef9a21ff88..e16ba267203b8 100644 --- a/docs/reference/rest-api/security/query-api-key.asciidoc +++ b/docs/reference/rest-api/security/query-api-key.asciidoc @@ -299,6 +299,7 @@ retrieved from one or more API keys: "invalidated": false, "username": "elastic", "realm": "reserved", + "realm_type": "reserved", "metadata": { "letter": "a" }, @@ -411,6 +412,7 @@ A successful call returns a JSON structure for API key information including its "invalidated": false, "username": "myuser", "realm": "native1", + "realm_type": "native", "metadata": { "application": "my-application" }, From b8450dc3ae65c5ebe50d980b0c4a82a30735abe8 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Tue, 20 Feb 2024 13:47:04 +0200 Subject: [PATCH 7/8] Update docs/changelog/105629.yaml --- docs/changelog/105629.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/105629.yaml diff --git a/docs/changelog/105629.yaml b/docs/changelog/105629.yaml new file mode 100644 index 0000000000000..00fa73a759558 --- /dev/null +++ b/docs/changelog/105629.yaml @@ -0,0 +1,5 @@ +pr: 105629 +summary: Show owner `realm_type` for returned API keys +area: Security +type: enhancement +issues: [] From 771b0c3118a95cd207ff36baf260a3954bbdc6d1 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Tue, 20 Feb 2024 19:50:41 +0200 Subject: [PATCH 8/8] Nit --- .../elasticsearch/xpack/core/security/action/apikey/ApiKey.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java index cbaa341d14965..ae345870e718b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java @@ -89,6 +89,7 @@ public String value() { private final Instant invalidation; private final String username; private final String realm; + @Nullable private final String realmType; private final Map metadata; @Nullable