浏览代码

Expose owner realm_type in the returned API key information (#105629)

When querying or getting API key information, ES returns the key owner's
username and realm (i.e. the realm name that authenticated the username
that last updated the API key).
This PR adds the realm_type to the information on the key's owner.
Albert Zaharovits 1 年之前
父节点
当前提交
065158e222

+ 5 - 0
docs/changelog/105629.yaml

@@ -0,0 +1,5 @@
+pr: 105629
+summary: Show owner `realm_type` for returned API keys
+area: Security
+type: enhancement
+issues: []

+ 2 - 0
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"
       },

+ 2 - 0
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"
       },

+ 32 - 4
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java

@@ -16,6 +16,7 @@ import org.elasticsearch.xcontent.ParseField;
 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;
 
@@ -88,6 +89,8 @@ public final class ApiKey implements ToXContentObject {
     private final Instant invalidation;
     private final String username;
     private final String realm;
+    @Nullable
+    private final String realmType;
     private final Map<String, Object> metadata;
     @Nullable
     private final List<RoleDescriptor> roleDescriptors;
@@ -104,6 +107,7 @@ public final class ApiKey implements ToXContentObject {
         @Nullable Instant invalidation,
         String username,
         String realm,
+        @Nullable String realmType,
         @Nullable Map<String, Object> metadata,
         @Nullable List<RoleDescriptor> roleDescriptors,
         @Nullable List<RoleDescriptor> limitedByRoleDescriptors
@@ -118,6 +122,7 @@ public final class ApiKey implements ToXContentObject {
             invalidation,
             username,
             realm,
+            realmType,
             metadata,
             roleDescriptors,
             limitedByRoleDescriptors == null ? null : new RoleDescriptorsIntersection(List.of(Set.copyOf(limitedByRoleDescriptors)))
@@ -134,6 +139,7 @@ public final class ApiKey implements ToXContentObject {
         Instant invalidation,
         String username,
         String realm,
+        @Nullable String realmType,
         @Nullable Map<String, Object> metadata,
         @Nullable List<RoleDescriptor> roleDescriptors,
         @Nullable RoleDescriptorsIntersection limitedBy
@@ -150,6 +156,7 @@ public final class ApiKey implements ToXContentObject {
         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 +200,17 @@ public final class ApiKey implements ToXContentObject {
         return realm;
     }
 
+    public @Nullable String getRealmType() {
+        return realmType;
+    }
+
+    public @Nullable RealmConfig.RealmIdentifier getRealmIdentifier() {
+        if (realm != null && realmType != null) {
+            return new RealmConfig.RealmIdentifier(realmType, realm);
+        }
+        return null;
+    }
+
     public Map<String, Object> getMetadata() {
         return metadata;
     }
@@ -223,7 +241,11 @@ public final class ApiKey implements ToXContentObject {
         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 +309,7 @@ public final class ApiKey implements ToXContentObject {
             invalidation,
             username,
             realm,
+            realmType,
             metadata,
             roleDescriptors,
             limitedBy
@@ -314,6 +337,7 @@ public final class ApiKey implements ToXContentObject {
             && 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 +355,10 @@ public final class ApiKey implements ToXContentObject {
             (args[6] == null) ? null : Instant.ofEpochMilli((Long) args[6]),
             (String) args[7],
             (String) args[8],
-            (args[9] == null) ? null : (Map<String, Object>) args[9],
-            (List<RoleDescriptor>) args[10],
-            (RoleDescriptorsIntersection) args[11]
+            (String) args[9],
+            (args[10] == null) ? null : (Map<String, Object>) args[10],
+            (List<RoleDescriptor>) args[11],
+            (RoleDescriptorsIntersection) args[12]
         );
     });
     static {
@@ -346,6 +371,7 @@ public final class ApiKey implements ToXContentObject {
         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 +409,8 @@ public final class ApiKey implements ToXContentObject {
             + username
             + ", realm="
             + realm
+            + ", realm_type="
+            + realmType
             + ", metadata="
             + metadata
             + ", role_descriptors="

+ 3 - 0
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKeyTests.java

@@ -68,6 +68,7 @@ public class ApiKeyTests extends ESTestCase {
         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 class ApiKeyTests extends ESTestCase {
             : null;
         final String username = randomAlphaOfLengthBetween(4, 10);
         final String realmName = randomAlphaOfLengthBetween(3, 8);
+        final String realmType = randomFrom(randomAlphaOfLengthBetween(3, 8), null);
         final Map<String, Object> metadata = randomMetadata();
         final List<RoleDescriptor> roleDescriptors = type == ApiKey.Type.CROSS_CLUSTER
             ? List.of(randomCrossClusterAccessRoleDescriptor())
@@ -190,6 +192,7 @@ public class ApiKeyTests extends ESTestCase {
             invalidation,
             username,
             realmName,
+            realmType,
             metadata,
             roleDescriptors,
             limitedByRoleDescriptors

+ 9 - 0
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyResponseTests.java

@@ -61,6 +61,7 @@ public class GetApiKeyResponseTests extends ESTestCase {
             "realm-x",
             null,
             null,
+            null,
             List.of() // empty limited-by role descriptor to simulate derived keys
         );
         ApiKey apiKeyInfo2 = createApiKeyInfo(
@@ -73,6 +74,7 @@ public class GetApiKeyResponseTests extends ESTestCase {
             Instant.ofEpochMilli(100000000L),
             "user-b",
             "realm-y",
+            "realm-type-y",
             Map.of(),
             List.of(),
             limitedByRoleDescriptors
@@ -87,6 +89,7 @@ public class GetApiKeyResponseTests extends ESTestCase {
             Instant.ofEpochMilli(100000000L),
             "user-c",
             "realm-z",
+            "realm-type-z",
             Map.of("foo", "bar"),
             roleDescriptors,
             limitedByRoleDescriptors
@@ -111,6 +114,7 @@ public class GetApiKeyResponseTests extends ESTestCase {
             Instant.ofEpochMilli(100000000L),
             "user-c",
             "realm-z",
+            "realm-type-z",
             Map.of("foo", "bar"),
             crossClusterAccessRoleDescriptors,
             null
@@ -145,6 +149,7 @@ public class GetApiKeyResponseTests extends ESTestCase {
                   "invalidation": 100000000,
                   "username": "user-b",
                   "realm": "realm-y",
+                  "realm_type": "realm-type-y",
                   "metadata": {},
                   "role_descriptors": {},
                   "limited_by": [
@@ -185,6 +190,7 @@ public class GetApiKeyResponseTests extends ESTestCase {
                   "invalidation": 100000000,
                   "username": "user-c",
                   "realm": "realm-z",
+                  "realm_type": "realm-type-z",
                   "metadata": {
                     "foo": "bar"
                   },
@@ -252,6 +258,7 @@ public class GetApiKeyResponseTests extends ESTestCase {
                   "invalidation": 100000000,
                   "username": "user-c",
                   "realm": "realm-z",
+                  "realm_type": "realm-type-z",
                   "metadata": {
                     "foo": "bar"
                   },
@@ -321,6 +328,7 @@ public class GetApiKeyResponseTests extends ESTestCase {
         Instant invalidation,
         String username,
         String realm,
+        String realmType,
         Map<String, Object> metadata,
         List<RoleDescriptor> roleDescriptors,
         List<RoleDescriptor> limitedByRoleDescriptors
@@ -335,6 +343,7 @@ public class GetApiKeyResponseTests extends ESTestCase {
             invalidation,
             username,
             realm,
+            realmType,
             metadata,
             roleDescriptors,
             limitedByRoleDescriptors

+ 4 - 0
x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java

@@ -300,6 +300,8 @@ public class ApiKeyRestIT extends SecurityOnTrialLicenseRestTestCase {
 
         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 class ApiKeyRestIT extends SecurityOnTrialLicenseRestTestCase {
 
         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);

+ 8 - 1
x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java

@@ -312,7 +312,10 @@ public class ApiKeySingleNodeTests extends SecuritySingleNodeTestCase {
         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 class ApiKeySingleNodeTests extends SecuritySingleNodeTestCase {
         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 class ApiKeySingleNodeTests extends SecuritySingleNodeTestCase {
         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 class ApiKeySingleNodeTests extends SecuritySingleNodeTestCase {
         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 class ApiKeySingleNodeTests extends SecuritySingleNodeTestCase {
         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

+ 1 - 1
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java

@@ -1937,7 +1937,6 @@ public class ApiKeyService {
 
     public void queryApiKeys(SearchRequest searchRequest, boolean withLimitedBy, ActionListener<QueryApiKeyResponse> listener) {
         ensureEnabled();
-
         final SecurityIndexManager frozenSecurityIndex = securityIndex.defensiveCopy();
         if (frozenSecurityIndex.indexExists() == false) {
             logger.debug("security index does not exist");
@@ -2004,6 +2003,7 @@ public class ApiKeyService {
             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

+ 135 - 7
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java

@@ -30,12 +30,14 @@ import org.elasticsearch.action.index.IndexResponse;
 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;
@@ -54,6 +56,7 @@ import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
 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;
@@ -91,12 +94,14 @@ import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse;
 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;
@@ -324,6 +329,109 @@ public class ApiKeyServiceTests extends ESTestCase {
         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<SearchResponse, IOException> 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<String, Object> apiKeySourceDoc = buildApiKeySourceDoc("some_hash".toCharArray());
+                ((Map<String, Object>) apiKeySourceDoc.get("creator")).put("realm", realm1);
+                ((Map<String, Object>) 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<String, Object> apiKeySourceDoc = buildApiKeySourceDoc("some_hash".toCharArray());
+                ((Map<String, Object>) apiKeySourceDoc.get("creator")).put("realm", realm2);
+                if (randomBoolean()) {
+                    ((Map<String, Object>) apiKeySourceDoc.get("creator")).put("realm_type", null);
+                } else {
+                    ((Map<String, Object>) 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<SearchResponse>) invocation.getArguments()[1], searchResponseSupplier.get());
+            return null;
+        }).when(client).search(any(SearchRequest.class), anyActionListener());
+        doAnswer(invocation -> {
+            ActionListener.respondAndRelease((ActionListener<SearchResponse>) invocation.getArguments()[2], searchResponseSupplier.get());
+            return null;
+        }).when(client).execute(eq(TransportSearchAction.TYPE), any(SearchRequest.class), anyActionListener());
+        {
+            PlainActionFuture<GetApiKeyResponse> 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<QueryApiKeyResponse> 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();
@@ -890,6 +998,24 @@ public class ApiKeyServiceTests extends ESTestCase {
         Duration expiry,
         @Nullable List<RoleDescriptor> 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<String, Object>, Map<String, Object>> newApiKeyDocument(
+        String key,
+        User user,
+        @Nullable User authUser,
+        boolean invalidated,
+        Duration expiry,
+        @Nullable List<RoleDescriptor> keyRoles,
+        ApiKey.Type type
     ) throws IOException {
         final Authentication authentication;
         if (authUser != null) {
@@ -906,7 +1032,7 @@ public class ApiKeyServiceTests extends ESTestCase {
                 .realmRef(new RealmRef("realm1", "native", "node01"))
                 .build(false);
         }
-        final Map<String, Object> metadata = ApiKeyTests.randomMetadata();
+        Map<String, Object> metadataMap = ApiKeyTests.randomMetadata();
         XContentBuilder docSource = ApiKeyService.newDocument(
             getFastStoredHashAlgoForTests().hash(new SecureString(key.toCharArray())),
             "test",
@@ -917,15 +1043,13 @@ public class ApiKeyServiceTests extends ESTestCase {
             keyRoles,
             type,
             Version.CURRENT,
-            metadata
+            metadataMap
         );
+        Map<String, Object> keyMap = XContentHelper.convertToMap(BytesReference.bytes(docSource), true, XContentType.JSON).v2();
         if (invalidated) {
-            Map<String, Object> 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<User> tryAuthenticate(ApiKeyService service, String id, String key, ApiKey.Type type) throws Exception {
@@ -2860,6 +2984,10 @@ public class ApiKeyServiceTests extends ESTestCase {
         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

+ 4 - 0
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java

@@ -117,6 +117,7 @@ public class RestGetApiKeyActionTests extends ESTestCase {
                     null,
                     "user-x",
                     "realm-1",
+                    "realm-type-1",
                     metadata,
                     roleDescriptors,
                     limitedByRoleDescriptors
@@ -176,6 +177,7 @@ public class RestGetApiKeyActionTests extends ESTestCase {
                         null,
                         "user-x",
                         "realm-1",
+                        "realm-type-1",
                         metadata,
                         roleDescriptors,
                         limitedByRoleDescriptors
@@ -226,6 +228,7 @@ public class RestGetApiKeyActionTests extends ESTestCase {
             null,
             "user-x",
             "realm-1",
+            "realm-type-1",
             ApiKeyTests.randomMetadata(),
             type == ApiKey.Type.CROSS_CLUSTER
                 ? List.of(randomCrossClusterAccessRoleDescriptor())
@@ -242,6 +245,7 @@ public class RestGetApiKeyActionTests extends ESTestCase {
             null,
             "user-y",
             "realm-1",
+            "realm-type-1",
             ApiKeyTests.randomMetadata(),
             type == ApiKey.Type.CROSS_CLUSTER
                 ? List.of(randomCrossClusterAccessRoleDescriptor())