Forráskód Böngészése

Add grant-api-key to HLRC (#68190)

This adds support for "Grant API Key" to the Java High Level Rest
Client.

This API was added in Elasticsearch 7.7 but did not have explicit support
in the HLRC.
Tim Vernum 4 éve
szülő
commit
528d029524

+ 32 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java

@@ -52,6 +52,7 @@ import org.elasticsearch.client.security.GetUserPrivilegesRequest;
 import org.elasticsearch.client.security.GetUserPrivilegesResponse;
 import org.elasticsearch.client.security.GetUsersRequest;
 import org.elasticsearch.client.security.GetUsersResponse;
+import org.elasticsearch.client.security.GrantApiKeyRequest;
 import org.elasticsearch.client.security.HasPrivilegesRequest;
 import org.elasticsearch.client.security.HasPrivilegesResponse;
 import org.elasticsearch.client.security.InvalidateApiKeyRequest;
@@ -1064,6 +1065,37 @@ public final class SecurityClient {
                 InvalidateApiKeyResponse::fromXContent, listener, emptySet());
     }
 
+    /**
+     * Create an API Key on behalf of another user.<br>
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-grant-api-key.html">
+     * the docs</a> for more.
+     *
+     * @param request the request to grant an API key
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @return the response from the create API key call
+     * @throws IOException in case there is a problem sending the request or parsing back the response
+     */
+    public CreateApiKeyResponse grantApiKey(final GrantApiKeyRequest request, final RequestOptions options) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::grantApiKey, options,
+            CreateApiKeyResponse::fromXContent, emptySet());
+    }
+
+    /**
+     * Asynchronously creates an API key on behalf of another user.<br>
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-grant-api-key.html">
+     * the docs</a> for more.
+     *
+     * @param request the request to grant an API key
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @param listener the listener to be notified upon request completion
+     * @return cancellable that may be used to cancel the request
+     */
+    public Cancellable grantApiKeyAsync(final GrantApiKeyRequest request, final RequestOptions options,
+                                         final ActionListener<CreateApiKeyResponse> listener) {
+        return restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::grantApiKey, options,
+            CreateApiKeyResponse::fromXContent, listener, emptySet());
+    }
+
     /**
      * Get an Elasticsearch access token from an {@code X509Certificate} chain. The certificate chain is that of the client from a mutually
      * authenticated TLS session, and it is validated by the PKI realms with {@code delegation.enabled} toggled to {@code true}.<br>

+ 10 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java

@@ -31,6 +31,7 @@ import org.elasticsearch.client.security.GetPrivilegesRequest;
 import org.elasticsearch.client.security.GetRoleMappingsRequest;
 import org.elasticsearch.client.security.GetRolesRequest;
 import org.elasticsearch.client.security.GetUsersRequest;
+import org.elasticsearch.client.security.GrantApiKeyRequest;
 import org.elasticsearch.client.security.HasPrivilegesRequest;
 import org.elasticsearch.client.security.InvalidateApiKeyRequest;
 import org.elasticsearch.client.security.InvalidateTokenRequest;
@@ -296,6 +297,15 @@ final class SecurityRequestConverters {
         return request;
     }
 
+    static Request grantApiKey(final GrantApiKeyRequest grantApiKeyRequest) throws IOException {
+        final Request request = new Request(HttpPost.METHOD_NAME, "/_security/api_key/grant");
+        request.setEntity(createEntity(grantApiKeyRequest, REQUEST_BODY_CONTENT_TYPE));
+        final RequestConverters.Params params = new RequestConverters.Params();
+        params.withRefreshPolicy(grantApiKeyRequest.getApiKeyRequest().getRefreshPolicy());
+        request.addParameters(params.asMap());
+        return request;
+    }
+
     static Request getApiKey(final GetApiKeyRequest getApiKeyRequest) throws IOException {
         final Request request = new Request(HttpGet.METHOD_NAME, "/_security/api_key");
         if (Strings.hasText(getApiKeyRequest.getId())) {

+ 156 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/security/GrantApiKeyRequest.java

@@ -0,0 +1,156 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.client.security;
+
+import org.elasticsearch.client.Validatable;
+import org.elasticsearch.common.CharArrays;
+import org.elasticsearch.common.xcontent.ToXContentFragment;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Request to create a new API Key on behalf of another user.
+ */
+public final class GrantApiKeyRequest implements Validatable, ToXContentObject {
+
+    private final Grant grant;
+    private final CreateApiKeyRequest apiKeyRequest;
+
+    public static class Grant implements ToXContentFragment {
+        private final String grantType;
+        private final String username;
+        private final char[] password;
+        private final String accessToken;
+
+        private Grant(String grantType, String username, char[] password, String accessToken) {
+            this.grantType = Objects.requireNonNull(grantType, "Grant type may not be null");
+            this.username = username;
+            this.password = password;
+            this.accessToken = accessToken;
+        }
+
+        public static Grant passwordGrant(String username, char[] password) {
+            return new Grant(
+                "password",
+                Objects.requireNonNull(username, "Username may not be null"),
+                Objects.requireNonNull(password, "Password may not be null"),
+                null);
+        }
+
+        public static Grant accessTokenGrant(String accessToken) {
+            return new Grant(
+                "access_token",
+                null,
+                null,
+                Objects.requireNonNull(accessToken, "Access token may not be null")
+            );
+        }
+
+        public String getGrantType() {
+            return grantType;
+        }
+
+        public String getUsername() {
+            return username;
+        }
+
+        public char[] getPassword() {
+            return password;
+        }
+
+        public String getAccessToken() {
+            return accessToken;
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            builder.field("grant_type", grantType);
+            if (username != null) {
+                builder.field("username", username);
+            }
+            if (password != null) {
+                byte[] passwordBytes = CharArrays.toUtf8Bytes(password);
+                try {
+                    builder.field("password").utf8Value(passwordBytes, 0, passwordBytes.length);
+                } finally {
+                    Arrays.fill(passwordBytes, (byte) 0);
+                }
+            }
+            if (accessToken != null) {
+                builder.field("access_token", accessToken);
+            }
+            return builder;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            Grant grant = (Grant) o;
+            return grantType.equals(grant.grantType)
+                && Objects.equals(username, grant.username)
+                && Arrays.equals(password, grant.password)
+                && Objects.equals(accessToken, grant.accessToken);
+        }
+
+        @Override
+        public int hashCode() {
+            int result = Objects.hash(grantType, username, accessToken);
+            result = 31 * result + Arrays.hashCode(password);
+            return result;
+        }
+    }
+
+    public GrantApiKeyRequest(Grant grant, CreateApiKeyRequest apiKeyRequest) {
+        this.grant = Objects.requireNonNull(grant, "Grant may not be null");
+        this.apiKeyRequest = Objects.requireNonNull(apiKeyRequest, "Create API key request may not be null");
+    }
+
+    public Grant getGrant() {
+        return grant;
+    }
+
+    public CreateApiKeyRequest getApiKeyRequest() {
+        return apiKeyRequest;
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        grant.toXContent(builder, params);
+        builder.field("api_key", apiKeyRequest);
+        return builder.endObject();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        final GrantApiKeyRequest that = (GrantApiKeyRequest) o;
+        return Objects.equals(this.grant, that.grant)
+            && Objects.equals(this.apiKeyRequest, that.apiKeyRequest);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(grant, apiKeyRequest);
+    }
+}

+ 11 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/Role.java

@@ -212,6 +212,17 @@ public final class Role {
         private Builder() {
         }
 
+        public Builder clone(Role role) {
+            return this
+                .name(role.name)
+                .clusterPrivileges(role.clusterPrivileges)
+                .globalApplicationPrivileges(role.globalPrivileges)
+                .indicesPrivileges(role.indicesPrivileges)
+                .applicationResourcePrivileges(role.applicationPrivileges)
+                .runAsPrivilege(role.runAsPrivilege)
+                .metadata(role.metadata);
+        }
+
         public Builder name(String name) {
             if (Strings.hasText(name) == false){
                 throw new IllegalArgumentException("role name must be provided");

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

@@ -27,6 +27,7 @@ import org.elasticsearch.client.security.GetPrivilegesRequest;
 import org.elasticsearch.client.security.GetRoleMappingsRequest;
 import org.elasticsearch.client.security.GetRolesRequest;
 import org.elasticsearch.client.security.GetUsersRequest;
+import org.elasticsearch.client.security.GrantApiKeyRequest;
 import org.elasticsearch.client.security.InvalidateApiKeyRequest;
 import org.elasticsearch.client.security.PutPrivilegesRequest;
 import org.elasticsearch.client.security.PutRoleMappingRequest;
@@ -425,23 +426,52 @@ public class SecurityRequestConvertersTests extends ESTestCase {
     }
 
     public void testCreateApiKey() throws IOException {
+        final CreateApiKeyRequest createApiKeyRequest = buildCreateApiKeyRequest();
+
+        final Map<String, String> expectedParams;
+        final RefreshPolicy refreshPolicy = createApiKeyRequest.getRefreshPolicy();
+        if (refreshPolicy != RefreshPolicy.NONE) {
+            expectedParams = Collections.singletonMap("refresh", refreshPolicy.getValue());
+        } else {
+            expectedParams = Collections.emptyMap();
+        }
+
+        final Request request = SecurityRequestConverters.createApiKey(createApiKeyRequest);
+        assertEquals(HttpPost.METHOD_NAME, request.getMethod());
+        assertEquals("/_security/api_key", request.getEndpoint());
+        assertEquals(expectedParams, request.getParameters());
+        assertToXContentBody(createApiKeyRequest, request.getEntity());
+    }
+
+    private CreateApiKeyRequest buildCreateApiKeyRequest() {
         final String name = randomAlphaOfLengthBetween(4, 7);
         final List<Role> roles = Collections.singletonList(Role.builder().name("r1").clusterPrivileges(ClusterPrivilegeName.ALL)
                 .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);
+        return createApiKeyRequest;
+    }
+
+    public void testGrantApiKey() throws IOException {
+        final CreateApiKeyRequest createApiKeyRequest = buildCreateApiKeyRequest();
+        final GrantApiKeyRequest grantApiKeyRequest = new GrantApiKeyRequest(randomBoolean()
+            ? GrantApiKeyRequest.Grant.accessTokenGrant(randomAlphaOfLength(24))
+            : GrantApiKeyRequest.Grant.passwordGrant(randomAlphaOfLengthBetween(4, 12), randomAlphaOfLengthBetween(14, 18).toCharArray()),
+            createApiKeyRequest);
         final Map<String, String> expectedParams;
+        final RefreshPolicy refreshPolicy = createApiKeyRequest.getRefreshPolicy();
         if (refreshPolicy != RefreshPolicy.NONE) {
             expectedParams = Collections.singletonMap("refresh", refreshPolicy.getValue());
         } else {
             expectedParams = Collections.emptyMap();
         }
-        final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy);
-        final Request request = SecurityRequestConverters.createApiKey(createApiKeyRequest);
+
+        final Request request = SecurityRequestConverters.grantApiKey(grantApiKeyRequest);
         assertEquals(HttpPost.METHOD_NAME, request.getMethod());
-        assertEquals("/_security/api_key", request.getEndpoint());
+        assertEquals("/_security/api_key/grant", request.getEndpoint());
         assertEquals(expectedParams, request.getParameters());
-        assertToXContentBody(createApiKeyRequest, request.getEntity());
+        assertToXContentBody(grantApiKeyRequest, request.getEntity());
     }
 
     public void testGetApiKey() throws IOException {

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

@@ -57,6 +57,7 @@ import org.elasticsearch.client.security.GetSslCertificatesResponse;
 import org.elasticsearch.client.security.GetUserPrivilegesResponse;
 import org.elasticsearch.client.security.GetUsersRequest;
 import org.elasticsearch.client.security.GetUsersResponse;
+import org.elasticsearch.client.security.GrantApiKeyRequest;
 import org.elasticsearch.client.security.HasPrivilegesRequest;
 import org.elasticsearch.client.security.HasPrivilegesResponse;
 import org.elasticsearch.client.security.InvalidateApiKeyRequest;
@@ -86,6 +87,7 @@ 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.client.security.user.privileges.UserIndicesPrivileges;
+import org.elasticsearch.common.CheckedConsumer;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.common.settings.Settings;
@@ -94,6 +96,8 @@ import org.elasticsearch.common.util.concurrent.ThreadContext;
 import org.elasticsearch.common.util.set.Sets;
 import org.hamcrest.Matchers;
 
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.Files;
@@ -114,11 +118,7 @@ import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
-import javax.crypto.SecretKeyFactory;
-import javax.crypto.spec.PBEKeySpec;
-
 import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
-
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.containsString;
@@ -126,10 +126,12 @@ import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.emptyIterable;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
 import static org.hamcrest.Matchers.hasItem;
 import static org.hamcrest.Matchers.in;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.iterableWithSize;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
 import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.notNullValue;
 import static org.hamcrest.Matchers.nullValue;
@@ -140,13 +142,13 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
     protected Settings restAdminSettings() {
         String token = basicAuthHeaderValue("admin_user", new SecureString("admin-password".toCharArray()));
         return Settings.builder()
-                .put(ThreadContext.PREFIX + ".Authorization", token)
-                .build();
+            .put(ThreadContext.PREFIX + ".Authorization", token)
+            .build();
     }
 
     public void testGetUsers() throws Exception {
         final RestHighLevelClient client = highLevelClient();
-        String[] usernames = new String[] {"user1", "user2", "user3"};
+        String[] usernames = new String[]{"user1", "user2", "user3"};
         addUser(client, usernames[0], randomAlphaOfLengthBetween(14, 18));
         addUser(client, usernames[1], randomAlphaOfLengthBetween(14, 18));
         addUser(client, usernames[2], randomAlphaOfLengthBetween(14, 18));
@@ -242,7 +244,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
 
         {
             //tag::put-user-password-request
-            char[] password = new char[]{'t', 'e', 's', 't', '-', 'u','s','e','r','-','p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
+            char[] password = new char[]{'t', 'e', 's', 't', '-', 'u', 's', 'e', 'r', '-', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
             User user = new User("example", Collections.singletonList("superuser"));
             PutUserRequest request = PutUserRequest.withPassword(user, password, true, RefreshPolicy.NONE);
             //end::put-user-password-request
@@ -261,7 +263,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
             byte[] salt = new byte[32];
             // no need for secure random in a test; it could block and would not be reproducible anyway
             random().nextBytes(salt);
-            char[] password = new char[]{'t', 'e', 's', 't', '-', 'u','s','e','r','-','p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
+            char[] password = new char[]{'t', 'e', 's', 't', '-', 'u', 's', 'e', 'r', '-', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
             User user = new User("example2", Collections.singletonList("superuser"));
 
             //tag::put-user-hash-request
@@ -557,7 +559,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
 
     public void testEnableUser() throws Exception {
         RestHighLevelClient client = highLevelClient();
-        char[] password = new char[]{'t', 'e', 's', 't', '-', 'u','s','e','r','-','p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
+        char[] password = new char[]{'t', 'e', 's', 't', '-', 'u', 's', 'e', 'r', '-', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
         User enable_user = new User("enable_user", Collections.singletonList("superuser"));
         PutUserRequest putUserRequest = PutUserRequest.withPassword(enable_user, password, true, RefreshPolicy.IMMEDIATE);
         PutUserResponse putUserResponse = client.security().putUser(putUserRequest, RequestOptions.DEFAULT);
@@ -602,7 +604,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
 
     public void testDisableUser() throws Exception {
         RestHighLevelClient client = highLevelClient();
-        char[] password = new char[]{'t', 'e', 's', 't', '-', 'u','s','e','r','-','p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
+        char[] password = new char[]{'t', 'e', 's', 't', '-', 'u', 's', 'e', 'r', '-', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
         User disable_user = new User("disable_user", Collections.singletonList("superuser"));
         PutUserRequest putUserRequest = PutUserRequest.withPassword(disable_user, password, true, RefreshPolicy.IMMEDIATE);
         PutUserResponse putUserResponse = client.security().putUser(putUserRequest, RequestOptions.DEFAULT);
@@ -1174,9 +1176,9 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
 
     public void testChangePassword() throws Exception {
         RestHighLevelClient client = highLevelClient();
-        char[] password = new char[]{'t', 'e', 's', 't', '-', 'u','s','e','r','-','p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
+        char[] password = new char[]{'t', 'e', 's', 't', '-', 'u', 's', 'e', 'r', '-', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
         char[] newPassword =
-            new char[]{'n', 'e', 'w', '-', 't', 'e', 's', 't', '-', 'u','s','e','r','-','p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
+            new char[]{'n', 'e', 'w', '-', 't', 'e', 's', 't', '-', 'u', 's', 'e', 'r', '-', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
         User user = new User("change_password_user", Collections.singletonList("superuser"), Collections.emptyMap(), null, null);
         PutUserRequest putUserRequest = PutUserRequest.withPassword(user, password, true, RefreshPolicy.NONE);
         PutUserResponse putUserResponse = client.security().putUser(putUserRequest, RequestOptions.DEFAULT);
@@ -1396,13 +1398,13 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
             // Setup user
             User token_user = new User("token_user", Collections.singletonList("kibana_user"));
             PutUserRequest putUserRequest = PutUserRequest.withPassword(token_user, "test-user-password".toCharArray(), true,
-                    RefreshPolicy.IMMEDIATE);
+                RefreshPolicy.IMMEDIATE);
             PutUserResponse putUserResponse = client.security().putUser(putUserRequest, RequestOptions.DEFAULT);
             assertTrue(putUserResponse.isCreated());
         }
         {
             // tag::create-token-password-request
-            final char[] password = new char[]{'t', 'e', 's', 't', '-', 'u','s','e','r','-','p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
+            final char[] password = new char[]{'t', 'e', 's', 't', '-', 'u', 's', 'e', 'r', '-', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
             CreateTokenRequest createTokenRequest = CreateTokenRequest.passwordGrant("token_user", password);
             // end::create-token-password-request
 
@@ -1952,7 +1954,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
         RestHighLevelClient client = highLevelClient();
 
         List<Role> roles = Collections.singletonList(Role.builder().name("r1").clusterPrivileges(ClusterPrivilegeName.ALL)
-                .indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build());
+            .indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build());
         final TimeValue expiration = TimeValue.timeValueHours(24);
         final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
         {
@@ -2011,11 +2013,108 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
         }
     }
 
+    public void testGrantApiKey() throws Exception {
+        RestHighLevelClient client = highLevelClient();
+
+        final String username = "grant_apikey_user";
+        final String passwordString = randomAlphaOfLengthBetween(14, 18);
+        final char[] password = passwordString.toCharArray();
+
+        addUser(client, username, passwordString);
+
+        List<Role> roles = Collections.singletonList(Role.builder().name("r1").clusterPrivileges(ClusterPrivilegeName.ALL)
+            .indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build());
+
+
+        final Instant start = Instant.now();
+        CheckedConsumer<CreateApiKeyResponse, IOException> apiKeyVerifier = (created) -> {
+            final GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(created.getId(), false);
+            final GetApiKeyResponse getApiKeyResponse = client.security().getApiKey(getApiKeyRequest, RequestOptions.DEFAULT);
+            assertThat(getApiKeyResponse.getApiKeyInfos(), iterableWithSize(1));
+            final ApiKey apiKeyInfo = getApiKeyResponse.getApiKeyInfos().get(0);
+            assertThat(apiKeyInfo.getUsername(), equalTo(username));
+            assertThat(apiKeyInfo.getId(), equalTo(created.getId()));
+            assertThat(apiKeyInfo.getName(), equalTo(created.getName()));
+            assertThat(apiKeyInfo.getExpiration(), equalTo(created.getExpiration()));
+            assertThat(apiKeyInfo.isInvalidated(), equalTo(false));
+            assertThat(apiKeyInfo.getCreation(), greaterThanOrEqualTo(start));
+            assertThat(apiKeyInfo.getCreation(), lessThanOrEqualTo(Instant.now()));
+        };
+
+        final TimeValue expiration = TimeValue.timeValueHours(24);
+        final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
+        {
+            final String name = randomAlphaOfLength(5);
+            // tag::grant-api-key-request
+            CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy);
+            GrantApiKeyRequest.Grant grant = GrantApiKeyRequest.Grant.passwordGrant(username, password);
+            GrantApiKeyRequest grantApiKeyRequest = new GrantApiKeyRequest(grant, createApiKeyRequest);
+            // end::grant-api-key-request
+
+            // tag::grant-api-key-execute
+            CreateApiKeyResponse apiKeyResponse = client.security().grantApiKey(grantApiKeyRequest, RequestOptions.DEFAULT);
+            // end::grant-api-key-execute
+
+            // tag::grant-api-key-response
+            SecureString apiKey = apiKeyResponse.getKey(); // <1>
+            Instant apiKeyExpiration = apiKeyResponse.getExpiration(); // <2>
+            // end::grant-api-key-response
+            assertThat(apiKeyResponse.getName(), equalTo(name));
+            assertNotNull(apiKey);
+            assertNotNull(apiKeyExpiration);
+
+            apiKeyVerifier.accept(apiKeyResponse);
+        }
+
+        {
+            final String name = randomAlphaOfLength(5);
+            final CreateTokenRequest tokenRequest = CreateTokenRequest.passwordGrant(username, password);
+            final CreateTokenResponse token = client.security().createToken(tokenRequest, RequestOptions.DEFAULT);
+
+            CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy);
+            GrantApiKeyRequest.Grant grant = GrantApiKeyRequest.Grant.accessTokenGrant(token.getAccessToken());
+            GrantApiKeyRequest grantApiKeyRequest = new GrantApiKeyRequest(grant, createApiKeyRequest);
+
+            ActionListener<CreateApiKeyResponse> listener;
+            // tag::grant-api-key-execute-listener
+            listener = new ActionListener<CreateApiKeyResponse>() {
+                @Override
+                public void onResponse(CreateApiKeyResponse createApiKeyResponse) {
+                    // <1>
+                }
+
+                @Override
+                public void onFailure(Exception e) {
+                    // <2>
+                }
+            };
+            // end::grant-api-key-execute-listener
+
+            // Avoid unused variable warning
+            assertNotNull(listener);
+
+            // Replace the empty listener by a blocking listener in test
+            final PlainActionFuture<CreateApiKeyResponse> future = new PlainActionFuture<>();
+            listener = future;
+
+            // tag::grant-api-key-execute-async
+            client.security().grantApiKeyAsync(grantApiKeyRequest, RequestOptions.DEFAULT, listener); // <1>
+            // end::grant-api-key-execute-async
+
+            assertNotNull(future.get(30, TimeUnit.SECONDS));
+            assertThat(future.get().getName(), equalTo(name));
+            assertNotNull(future.get().getKey());
+            assertNotNull(future.get().getExpiration());
+
+            apiKeyVerifier.accept(future.get());
+        }
+    }
+
     public void testGetApiKey() throws Exception {
         RestHighLevelClient client = highLevelClient();
 
         List<Role> roles = Collections.singletonList(Role.builder().name("r1").clusterPrivileges(ClusterPrivilegeName.ALL)
-                .indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build());
+            .indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build());
         final TimeValue expiration = TimeValue.timeValueHours(24);
         final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
         // Create API Keys
@@ -2025,7 +2124,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
         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");
         {
             // tag::get-api-key-id-request
             GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId(), false);
@@ -2165,7 +2264,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
         RestHighLevelClient client = highLevelClient();
 
         List<Role> roles = Collections.singletonList(Role.builder().name("r1").clusterPrivileges(ClusterPrivilegeName.ALL)
-                .indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build());
+            .indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build());
         final TimeValue expiration = TimeValue.timeValueHours(24);
         final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
         // Create API Keys
@@ -2181,7 +2280,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
 
             // tag::invalidate-api-key-execute
             InvalidateApiKeyResponse invalidateApiKeyResponse = client.security().invalidateApiKey(invalidateApiKeyRequest,
-                    RequestOptions.DEFAULT);
+                RequestOptions.DEFAULT);
             // end::invalidate-api-key-execute
 
             final List<ElasticsearchException> errors = invalidateApiKeyResponse.getErrors();
@@ -2224,7 +2323,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
             // end::invalidate-api-key-name-request
 
             InvalidateApiKeyResponse invalidateApiKeyResponse = client.security().invalidateApiKey(invalidateApiKeyRequest,
-                    RequestOptions.DEFAULT);
+                RequestOptions.DEFAULT);
 
             final List<ElasticsearchException> errors = invalidateApiKeyResponse.getErrors();
             final List<String> invalidatedApiKeyIds = invalidateApiKeyResponse.getInvalidatedApiKeys();
@@ -2247,7 +2346,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
             // end::invalidate-realm-api-keys-request
 
             InvalidateApiKeyResponse invalidateApiKeyResponse = client.security().invalidateApiKey(invalidateApiKeyRequest,
-                    RequestOptions.DEFAULT);
+                RequestOptions.DEFAULT);
 
             final List<ElasticsearchException> errors = invalidateApiKeyResponse.getErrors();
             final List<String> invalidatedApiKeyIds = invalidateApiKeyResponse.getInvalidatedApiKeys();
@@ -2270,7 +2369,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
             // end::invalidate-user-api-keys-request
 
             InvalidateApiKeyResponse invalidateApiKeyResponse = client.security().invalidateApiKey(invalidateApiKeyRequest,
-                    RequestOptions.DEFAULT);
+                RequestOptions.DEFAULT);
 
             final List<ElasticsearchException> errors = invalidateApiKeyResponse.getErrors();
             final List<String> invalidatedApiKeyIds = invalidateApiKeyResponse.getInvalidatedApiKeys();
@@ -2294,7 +2393,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
 
             // tag::invalidate-api-key-response
             InvalidateApiKeyResponse invalidateApiKeyResponse = client.security().invalidateApiKey(invalidateApiKeyRequest,
-                    RequestOptions.DEFAULT);
+                RequestOptions.DEFAULT);
             // end::invalidate-api-key-response
 
             final List<ElasticsearchException> errors = invalidateApiKeyResponse.getErrors();
@@ -2382,7 +2481,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
         {
             //tag::delegate-pki-request
             DelegatePkiAuthenticationRequest request = new DelegatePkiAuthenticationRequest(
-                    Arrays.asList(clientCertificate, intermediateCA));
+                Arrays.asList(clientCertificate, intermediateCA));
             //end::delegate-pki-request
             //tag::delegate-pki-execute
             DelegatePkiAuthenticationResponse response = client.security().delegatePkiAuthentication(request, RequestOptions.DEFAULT);
@@ -2406,7 +2505,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
 
         {
             DelegatePkiAuthenticationRequest request = new DelegatePkiAuthenticationRequest(
-                    Arrays.asList(clientCertificate, intermediateCA));
+                Arrays.asList(clientCertificate, intermediateCA));
             ActionListener<DelegatePkiAuthenticationResponse> listener;
 
             //tag::delegate-pki-execute-listener

+ 133 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/security/GrantApiKeyRequestTests.java

@@ -0,0 +1,133 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.client.security;
+
+import org.elasticsearch.client.security.user.privileges.IndicesPrivileges;
+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.test.ESTestCase;
+import org.elasticsearch.test.EqualsHashCodeTestUtils;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class GrantApiKeyRequestTests extends ESTestCase {
+
+    public void testToXContent() throws IOException {
+        final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest("api-key", List.of(), null, null);
+        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);
+        assertThat(output, equalTo(
+            "{" +
+                "\"grant_type\":\"password\"," +
+                "\"username\":\"kamala.khan\"," +
+                "\"password\":\"JerseyGirl!\"," +
+                "\"api_key\":{\"name\":\"api-key\",\"role_descriptors\":{}}" +
+                "}"));
+    }
+
+    public void testEqualsHashCode() {
+        final String name = randomAlphaOfLength(5);
+        List<Role> roles = randomList(1, 3, () ->
+            Role.builder()
+                .name(randomAlphaOfLengthBetween(3, 8))
+                .clusterPrivileges(randomSubsetOf(randomIntBetween(1, 3), ClusterPrivilegeName.ALL_ARRAY))
+                .indicesPrivileges(
+                    IndicesPrivileges
+                        .builder()
+                        .indices(randomAlphaOfLengthBetween(4, 12))
+                        .privileges(randomSubsetOf(randomIntBetween(1, 3), IndexPrivilegeName.ALL_ARRAY))
+                        .build()
+                ).build()
+        );
+        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 GrantApiKeyRequest.Grant grant = randomBoolean()
+            ? GrantApiKeyRequest.Grant.passwordGrant(randomAlphaOfLength(8), randomAlphaOfLengthBetween(6, 12).toCharArray())
+            : GrantApiKeyRequest.Grant.accessTokenGrant(randomAlphaOfLength(24));
+
+        final GrantApiKeyRequest grantApiKeyRequest = new GrantApiKeyRequest(grant, createApiKeyRequest);
+        EqualsHashCodeTestUtils.checkEqualsAndHashCode(
+            grantApiKeyRequest,
+            original -> new GrantApiKeyRequest(clone(original.getGrant()), clone(original.getApiKeyRequest())),
+            GrantApiKeyRequestTests::mutateTestItem);
+    }
+
+    private GrantApiKeyRequest.Grant clone(GrantApiKeyRequest.Grant grant) {
+        switch (grant.getGrantType()) {
+            case "password":
+                return GrantApiKeyRequest.Grant.passwordGrant(grant.getUsername(), grant.getPassword());
+            case "access_token":
+                return GrantApiKeyRequest.Grant.accessTokenGrant(grant.getAccessToken());
+            default:
+                throw new IllegalArgumentException("Cannot clone grant: " + Strings.toString(grant));
+        }
+    }
+
+    private CreateApiKeyRequest clone(CreateApiKeyRequest apiKeyRequest) {
+        return new CreateApiKeyRequest(
+            apiKeyRequest.getName(),
+            apiKeyRequest.getRoles().stream().map(r -> Role.builder().clone(r).build()).collect(Collectors.toUnmodifiableList()),
+            apiKeyRequest.getExpiration(),
+            apiKeyRequest.getRefreshPolicy()
+        );
+    }
+
+    private static GrantApiKeyRequest mutateTestItem(GrantApiKeyRequest original) {
+        switch (randomIntBetween(0, 3)) {
+            case 0:
+                return new GrantApiKeyRequest(original.getGrant().getGrantType().equals("password")
+                    ? GrantApiKeyRequest.Grant.accessTokenGrant(randomAlphaOfLength(24))
+                    : GrantApiKeyRequest.Grant.passwordGrant(randomAlphaOfLength(8), randomAlphaOfLengthBetween(6, 12).toCharArray()),
+                    original.getApiKeyRequest());
+            case 1:
+                return new GrantApiKeyRequest(original.getGrant(),
+                    new CreateApiKeyRequest(
+                        randomAlphaOfLengthBetween(10, 15),
+                        original.getApiKeyRequest().getRoles(),
+                        original.getApiKeyRequest().getExpiration(),
+                        original.getApiKeyRequest().getRefreshPolicy()
+                    )
+                );
+            case 2:
+                return new GrantApiKeyRequest(original.getGrant(),
+                    new CreateApiKeyRequest(
+                        original.getApiKeyRequest().getName(),
+                        List.of(), // No role limits
+                        original.getApiKeyRequest().getExpiration(),
+                        original.getApiKeyRequest().getRefreshPolicy()
+                    )
+                );
+            case 3:
+            default:
+                return new GrantApiKeyRequest(original.getGrant(),
+                    new CreateApiKeyRequest(
+                        original.getApiKeyRequest().getName(),
+                        original.getApiKeyRequest().getRoles(),
+                        TimeValue.timeValueMinutes(randomIntBetween(10, 120)),
+                        original.getApiKeyRequest().getRefreshPolicy()
+                    )
+                );
+        }
+    }
+}