diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java index 176436ce458a7..888f8441832f6 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java @@ -480,9 +480,9 @@ public final BulkByScrollResponse updateByQuery(UpdateByQueryRequest updateByQue * @param listener the listener to be notified upon request completion */ public final void updateByQueryAsync(UpdateByQueryRequest updateByQueryRequest, RequestOptions options, - ActionListener listener) { + ActionListener listener) { performRequestAsyncAndParseEntity( - updateByQueryRequest, RequestConverters::updateByQuery, options, BulkByScrollResponse::fromXContent, listener, emptySet() + updateByQueryRequest, RequestConverters::updateByQuery, options, BulkByScrollResponse::fromXContent, listener, emptySet() ); } @@ -512,36 +512,38 @@ public final BulkByScrollResponse deleteByQuery(DeleteByQueryRequest deleteByQue public final void deleteByQueryAsync(DeleteByQueryRequest deleteByQueryRequest, RequestOptions options, ActionListener 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 - * Reindex rethrottling API on elastic.co + * Reindex rethrottling API on elastic.co + * * @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 - * Reindex rethrottling API on elastic.co + * Reindex rethrottling API on elastic.co + * * @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 listener) { + ActionListener listener) { performRequestAsyncAndParseEntity(rethrottleRequest, RequestConverters::rethrottle, options, ListTasksResponse::fromXContent, - listener, emptySet()); + listener, emptySet()); } /** diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java index a4bc34004c247..7192d82f474a8 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java @@ -25,6 +25,7 @@ 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 * the docs 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 PutUserResponse putUser(PutUserRequest request, RequestOptions options) t * Asynchronously create/update a user in the native realm. * See * the docs 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 listener) { @@ -74,6 +77,7 @@ public void putUserAsync(PutUserRequest request, RequestOptions options, ActionL * Enable a native realm or built-in user synchronously. * See * the docs 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 EmptyResponse enableUser(EnableUserRequest request, RequestOptions option * Enable a native realm or built-in user asynchronously. * See * the docs 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 listener) { + ActionListener listener) { restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::enableUser, options, EmptyResponse::fromXContent, listener, emptySet()); } @@ -102,6 +107,7 @@ public void enableUserAsync(EnableUserRequest request, RequestOptions options, * Disable a native realm or built-in user synchronously. * See * the docs 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 EmptyResponse disableUser(DisableUserRequest request, RequestOptions opti * Disable a native realm or built-in user asynchronously. * See * the docs 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 listener) { + ActionListener 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 + * the docs 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 + * the docs 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 listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::changePassword, options, + EmptyResponse::fromXContent, listener, emptySet()); + } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java index 8533e0f1b4cd4..3157abe63375e 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java +++ b/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") diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/ChangePasswordRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/ChangePasswordRequest.java new file mode 100644 index 0000000000000..ffae034d246ed --- /dev/null +++ b/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); + } + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java index 3670379cd9fee..0741c6f72d90a 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java @@ -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 void testDisableUser() { private static Map 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 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 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()); + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java index 103b031fc0e03..778ec7b5707cd 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java @@ -24,6 +24,7 @@ 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 void testPutUser() throws Exception { { //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 void testPutUser() throws Exception { } { - 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 void onFailure(Exception e) { 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 listener = new ActionListener() { + @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)); + } + } } diff --git a/docs/java-rest/high-level/security/change-password.asciidoc b/docs/java-rest/high-level/security/change-password.asciidoc new file mode 100644 index 0000000000000..59ee5f23af9bd --- /dev/null +++ b/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. diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 8e3e8f5812bd4..2e43127f4bc55 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -306,6 +306,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: + +* <> +* <> + +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: