Browse Source

HLRC: add change password API support (#33509)

This change adds support for the change password APIs to the high
level rest client.

Relates #29827
Ioannis Kakavas 7 years ago
parent
commit
300896d401

+ 13 - 11
client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java

@@ -480,9 +480,9 @@ public class RestHighLevelClient implements Closeable {
      * @param listener the listener to be notified upon request completion
      */
     public final void updateByQueryAsync(UpdateByQueryRequest updateByQueryRequest, RequestOptions options,
-                                   ActionListener<BulkByScrollResponse> listener) {
+                                         ActionListener<BulkByScrollResponse> listener) {
         performRequestAsyncAndParseEntity(
-                updateByQueryRequest, RequestConverters::updateByQuery, options, BulkByScrollResponse::fromXContent, listener, emptySet()
+            updateByQueryRequest, RequestConverters::updateByQuery, options, BulkByScrollResponse::fromXContent, listener, emptySet()
         );
     }
 
@@ -512,36 +512,38 @@ public class RestHighLevelClient implements Closeable {
     public final void deleteByQueryAsync(DeleteByQueryRequest deleteByQueryRequest, RequestOptions options,
                                          ActionListener<BulkByScrollResponse> listener) {
         performRequestAsyncAndParseEntity(
-                deleteByQueryRequest, RequestConverters::deleteByQuery, options, BulkByScrollResponse::fromXContent, listener, emptySet()
+            deleteByQueryRequest, RequestConverters::deleteByQuery, options, BulkByScrollResponse::fromXContent, listener, emptySet()
         );
     }
 
     /**
      * Executes a reindex rethrottling request.
      * See the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html#docs-reindex-rethrottle">
-     *     Reindex rethrottling API on elastic.co</a>
+     * Reindex rethrottling API on elastic.co</a>
+     *
      * @param rethrottleRequest the request
-     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @param options           the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
      * @return the response
      * @throws IOException in case there is a problem sending the request or parsing back the response
      */
     public final ListTasksResponse reindexRethrottle(RethrottleRequest rethrottleRequest, RequestOptions options) throws IOException {
         return performRequestAndParseEntity(rethrottleRequest, RequestConverters::rethrottle, options, ListTasksResponse::fromXContent,
-                emptySet());
+            emptySet());
     }
 
     /**
      * Executes a reindex rethrottling request.
      * See the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html#docs-reindex-rethrottle">
-     *     Reindex rethrottling API on elastic.co</a>
+     * Reindex rethrottling API on elastic.co</a>
+     *
      * @param rethrottleRequest the request
-     * @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
+     * @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
      */
     public final void reindexRethrottleAsync(RethrottleRequest rethrottleRequest, RequestOptions options,
-            ActionListener<ListTasksResponse> listener) {
+                                             ActionListener<ListTasksResponse> listener) {
         performRequestAsyncAndParseEntity(rethrottleRequest, RequestConverters::rethrottle, options, ListTasksResponse::fromXContent,
-                listener, emptySet());
+            listener, emptySet());
     }
 
     /**

+ 45 - 8
client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java

@@ -25,6 +25,7 @@ import org.elasticsearch.client.security.EnableUserRequest;
 import org.elasticsearch.client.security.PutUserRequest;
 import org.elasticsearch.client.security.PutUserResponse;
 import org.elasticsearch.client.security.EmptyResponse;
+import org.elasticsearch.client.security.ChangePasswordRequest;
 
 import java.io.IOException;
 
@@ -47,6 +48,7 @@ public final class SecurityClient {
      * Create/update a user in the native realm synchronously.
      * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html">
      * the docs</a> for more.
+     *
      * @param request the request with the user's information
      * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
      * @return the response from the put user call
@@ -61,8 +63,9 @@ public final class SecurityClient {
      * Asynchronously create/update a user in the native realm.
      * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html">
      * the docs</a> for more.
-     * @param request the request with the user's information
-     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     *
+     * @param request  the request with the user's information
+     * @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
      */
     public void putUserAsync(PutUserRequest request, RequestOptions options, ActionListener<PutUserResponse> listener) {
@@ -74,6 +77,7 @@ public final class SecurityClient {
      * Enable a native realm or built-in user synchronously.
      * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-enable-user.html">
      * the docs</a> for more.
+     *
      * @param request the request with the user to enable
      * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
      * @return the response from the enable user call
@@ -88,12 +92,13 @@ public final class SecurityClient {
      * Enable a native realm or built-in user asynchronously.
      * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-enable-user.html">
      * the docs</a> for more.
-     * @param request the request with the user to enable
-     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     *
+     * @param request  the request with the user to enable
+     * @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
      */
     public void enableUserAsync(EnableUserRequest request, RequestOptions options,
-                                    ActionListener<EmptyResponse> listener) {
+                                ActionListener<EmptyResponse> listener) {
         restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::enableUser, options,
             EmptyResponse::fromXContent, listener, emptySet());
     }
@@ -102,6 +107,7 @@ public final class SecurityClient {
      * Disable a native realm or built-in user synchronously.
      * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-disable-user.html">
      * the docs</a> for more.
+     *
      * @param request the request with the user to disable
      * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
      * @return the response from the enable user call
@@ -116,13 +122,44 @@ public final class SecurityClient {
      * Disable a native realm or built-in user asynchronously.
      * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-disable-user.html">
      * the docs</a> for more.
-     * @param request the request with the user to disable
-     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     *
+     * @param request  the request with the user to disable
+     * @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
      */
     public void disableUserAsync(DisableUserRequest request, RequestOptions options,
-                                ActionListener<EmptyResponse> listener) {
+                                 ActionListener<EmptyResponse> listener) {
         restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::disableUser, options,
             EmptyResponse::fromXContent, listener, emptySet());
     }
+
+    /**
+     * Change the password of a user of a native realm or built-in user synchronously.
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-change-password.html">
+     * the docs</a> for more.
+     *
+     * @param request the request with the user's new password
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @return the response from the change user password call
+     * @throws IOException in case there is a problem sending the request or parsing back the response
+     */
+    public EmptyResponse changePassword(ChangePasswordRequest request, RequestOptions options) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::changePassword, options,
+            EmptyResponse::fromXContent, emptySet());
+    }
+
+    /**
+     * Change the password of a user of a native realm or built-in user asynchronously.
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-change-password.html">
+     * the docs</a> for more.
+     *
+     * @param request  the request with the user's new password
+     * @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
+     */
+    public void changePasswordAsync(ChangePasswordRequest request, RequestOptions options,
+                                    ActionListener<EmptyResponse> listener) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::changePassword, options,
+            EmptyResponse::fromXContent, listener, emptySet());
+    }
 }

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

@@ -19,9 +19,11 @@
 
 package org.elasticsearch.client;
 
+import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.methods.HttpPut;
 import org.elasticsearch.client.security.DisableUserRequest;
 import org.elasticsearch.client.security.EnableUserRequest;
+import org.elasticsearch.client.security.ChangePasswordRequest;
 import org.elasticsearch.client.security.PutUserRequest;
 import org.elasticsearch.client.security.SetUserEnabledRequest;
 
@@ -34,6 +36,19 @@ final class SecurityRequestConverters {
 
     private SecurityRequestConverters() {}
 
+    static Request changePassword(ChangePasswordRequest changePasswordRequest) throws IOException {
+        String endpoint = new RequestConverters.EndpointBuilder()
+            .addPathPartAsIs("_xpack/security/user")
+            .addPathPart(changePasswordRequest.getUsername())
+            .addPathPartAsIs("_password")
+            .build();
+        Request request = new Request(HttpPost.METHOD_NAME, endpoint);
+        request.setEntity(createEntity(changePasswordRequest, REQUEST_BODY_CONTENT_TYPE));
+        RequestConverters.Params params = new RequestConverters.Params(request);
+        params.withRefreshPolicy(changePasswordRequest.getRefreshPolicy());
+        return request;
+    }
+
     static Request putUser(PutUserRequest putUserRequest) throws IOException {
         String endpoint = new RequestConverters.EndpointBuilder()
             .addPathPartAsIs("_xpack/security/user")

+ 76 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/security/ChangePasswordRequest.java

@@ -0,0 +1,76 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.security;
+
+import org.elasticsearch.client.Validatable;
+import org.elasticsearch.common.CharArrays;
+import org.elasticsearch.common.Nullable;
+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 object to change the password of a user of a native realm or a built-in user.
+ */
+public final class ChangePasswordRequest implements Validatable, ToXContentObject {
+
+    private final String username;
+    private final char[] password;
+    private final RefreshPolicy refreshPolicy;
+
+    /**
+     * @param username      The username of the user whose password should be changed or null for the current user.
+     * @param password      The new password. The password array is not cleared by the {@link ChangePasswordRequest} object so the
+     *                      calling code must clear it after receiving the response.
+     * @param refreshPolicy The refresh policy for the request.
+     */
+    public ChangePasswordRequest(@Nullable String username, char[] password, RefreshPolicy refreshPolicy) {
+        this.username = username;
+        this.password = Objects.requireNonNull(password, "password is required");
+        this.refreshPolicy = refreshPolicy == null ? RefreshPolicy.getDefault() : refreshPolicy;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public char[] getPassword() {
+        return password;
+    }
+
+    public RefreshPolicy getRefreshPolicy() {
+        return refreshPolicy;
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        byte[] charBytes = CharArrays.toUtf8Bytes(password);
+        try {
+            return builder.startObject()
+                .field("password").utf8Value(charBytes, 0, charBytes.length)
+                .endObject();
+        } finally {
+            Arrays.fill(charBytes, (byte) 0);
+        }
+    }
+}

+ 28 - 1
client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java

@@ -19,9 +19,11 @@
 
 package org.elasticsearch.client;
 
+import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.methods.HttpPut;
 import org.elasticsearch.client.security.DisableUserRequest;
 import org.elasticsearch.client.security.EnableUserRequest;
+import org.elasticsearch.client.security.ChangePasswordRequest;
 import org.elasticsearch.client.security.PutUserRequest;
 import org.elasticsearch.client.security.RefreshPolicy;
 import org.elasticsearch.test.ESTestCase;
@@ -91,9 +93,34 @@ public class SecurityRequestConvertersTests extends ESTestCase {
 
     private static Map<String, String> getExpectedParamsFromRefreshPolicy(RefreshPolicy refreshPolicy) {
         if (refreshPolicy != RefreshPolicy.NONE) {
-             return Collections.singletonMap("refresh", refreshPolicy.getValue());
+            return Collections.singletonMap("refresh", refreshPolicy.getValue());
         } else {
             return Collections.emptyMap();
         }
     }
+
+    public void testChangePassword() throws IOException {
+        final String username = randomAlphaOfLengthBetween(4, 12);
+        final char[] password = randomAlphaOfLengthBetween(8, 12).toCharArray();
+        final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
+        final Map<String, String> expectedParams = getExpectedParamsFromRefreshPolicy(refreshPolicy);
+        ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(username, password, refreshPolicy);
+        Request request = SecurityRequestConverters.changePassword(changePasswordRequest);
+        assertEquals(HttpPost.METHOD_NAME, request.getMethod());
+        assertEquals("/_xpack/security/user/" + changePasswordRequest.getUsername() + "/_password", request.getEndpoint());
+        assertEquals(expectedParams, request.getParameters());
+        assertToXContentBody(changePasswordRequest, request.getEntity());
+    }
+
+    public void testSelfChangePassword() throws IOException {
+        final char[] password = randomAlphaOfLengthBetween(8, 12).toCharArray();
+        final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
+        final Map<String, String> expectedParams = getExpectedParamsFromRefreshPolicy(refreshPolicy);
+        ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(null, password, refreshPolicy);
+        Request request = SecurityRequestConverters.changePassword(changePasswordRequest);
+        assertEquals(HttpPost.METHOD_NAME, request.getMethod());
+        assertEquals("/_xpack/security/user/_password", request.getEndpoint());
+        assertEquals(expectedParams, request.getParameters());
+        assertToXContentBody(changePasswordRequest, request.getEntity());
+    }
 }

+ 47 - 2
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java

@@ -24,6 +24,7 @@ import org.elasticsearch.action.LatchedActionListener;
 import org.elasticsearch.client.ESRestHighLevelClientTestCase;
 import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.RestHighLevelClient;
+import org.elasticsearch.client.security.ChangePasswordRequest;
 import org.elasticsearch.client.security.DisableUserRequest;
 import org.elasticsearch.client.security.EnableUserRequest;
 import org.elasticsearch.client.security.PutUserRequest;
@@ -42,7 +43,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
 
         {
             //tag::put-user-execute
-            char[] password = new char[] { 'p', 'a', 's', 's', 'w', 'o', 'r', 'd' };
+            char[] password = new char[]{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
             PutUserRequest request =
                 new PutUserRequest("example", password, Collections.singletonList("superuser"), null, null, true, null, RefreshPolicy.NONE);
             PutUserResponse response = client.security().putUser(request, RequestOptions.DEFAULT);
@@ -56,7 +57,7 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
         }
 
         {
-            char[] password = new char[] { 'p', 'a', 's', 's', 'w', 'o', 'r', 'd' };
+            char[] password = new char[]{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
             PutUserRequest request = new PutUserRequest("example2", password, Collections.singletonList("superuser"), null, null, true,
                 null, RefreshPolicy.NONE);
             // tag::put-user-execute-listener
@@ -173,4 +174,48 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
             assertTrue(latch.await(30L, TimeUnit.SECONDS));
         }
     }
+
+    public void testChangePassword() throws Exception {
+        RestHighLevelClient client = highLevelClient();
+        char[] password = new char[]{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
+        char[] newPassword = new char[]{'n', 'e', 'w', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
+        PutUserRequest putUserRequest = new PutUserRequest("change_password_user", password, Collections.singletonList("superuser"),
+            null, null, true, null, RefreshPolicy.NONE);
+        PutUserResponse putUserResponse = client.security().putUser(putUserRequest, RequestOptions.DEFAULT);
+        assertTrue(putUserResponse.isCreated());
+        {
+            //tag::change-password-execute
+            ChangePasswordRequest request = new ChangePasswordRequest("change_password_user", newPassword, RefreshPolicy.NONE);
+            EmptyResponse response = client.security().changePassword(request, RequestOptions.DEFAULT);
+            //end::change-password-execute
+
+            assertNotNull(response);
+        }
+        {
+            //tag::change-password-execute-listener
+            ChangePasswordRequest request = new ChangePasswordRequest("change_password_user", password, RefreshPolicy.NONE);
+            ActionListener<EmptyResponse> listener = new ActionListener<EmptyResponse>() {
+                @Override
+                public void onResponse(EmptyResponse emptyResponse) {
+                    // <1>
+                }
+
+                @Override
+                public void onFailure(Exception e) {
+                    // <2>
+                }
+            };
+            //end::change-password-execute-listener
+
+            // Replace the empty listener by a blocking listener in test
+            final CountDownLatch latch = new CountDownLatch(1);
+            listener = new LatchedActionListener<>(listener, latch);
+
+            //tag::change-password-execute-async
+            client.security().changePasswordAsync(request, RequestOptions.DEFAULT, listener); // <1>
+            //end::change-password-execute-async
+
+            assertTrue(latch.await(30L, TimeUnit.SECONDS));
+        }
+    }
 }

+ 46 - 0
docs/java-rest/high-level/security/change-password.asciidoc

@@ -0,0 +1,46 @@
+[[java-rest-high-security-put-user]]
+=== Change Password User API
+
+[[java-rest-high-security-put-user-execution]]
+==== Execution
+
+A user's password can be changed using the `security().changePassword()`
+method:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SecurityDocumentationIT.java[change-password-execute]
+--------------------------------------------------
+
+[[java-rest-high-change-password-response]]
+==== Response
+
+The returned `EmptyResponse` does not contain any fields. The return of this
+response indicates a successful request.
+
+[[java-rest-high-x-pack-security-put-user-async]]
+==== Asynchronous Execution
+
+This request can be executed asynchronously:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SecurityDocumentationIT.java[change-password-execute-async]
+--------------------------------------------------
+<1> The `ChangePassword` request to execute and the `ActionListener` to use when
+the execution completes.
+
+The asynchronous method does not block and returns immediately. Once the request
+has completed the `ActionListener` is called back using the `onResponse` method
+if the execution successfully completed or using the `onFailure` method if
+it failed.
+
+A typical listener for a `EmptyResponse` looks like:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SecurityDocumentationIT.java[change-password-execute-listener]
+--------------------------------------------------
+<1> Called when the execution is successfully completed. The response is
+provided as an argument.
+<2> Called in case of failure. The raised exception is provided as an argument.

+ 10 - 0
docs/java-rest/high-level/supported-apis.asciidoc

@@ -310,6 +310,16 @@ The Java High Level REST Client supports the following Watcher APIs:
 include::watcher/put-watch.asciidoc[]
 include::watcher/delete-watch.asciidoc[]
 
+== Security APIs
+
+The Java High Level REST Client supports the following Security APIs:
+
+* <<java-rest-high-x-pack-security-put-user>>
+* <<java-rest-high-change-password-user>>
+
+include::x-pack/security/put-user.asciidoc[]
+include::security/change-password.asciidoc[]
+
 == Graph APIs
 
 The Java High Level REST Client supports the following Graph APIs: