diff --git a/client/rest-high-level/build.gradle b/client/rest-high-level/build.gradle
index 9acfc630f94f..c608d7c91f16 100644
--- a/client/rest-high-level/build.gradle
+++ b/client/rest-high-level/build.gradle
@@ -77,6 +77,27 @@ forbiddenApisMain {
signaturesFiles += files('src/main/resources/forbidden/rest-high-level-signatures.txt')
}
+integTestRunner {
+ systemProperty 'tests.rest.cluster.username', System.getProperty('tests.rest.cluster.username', 'test_user')
+ systemProperty 'tests.rest.cluster.password', System.getProperty('tests.rest.cluster.password', 'test-password')
+}
+
integTestCluster {
setting 'xpack.license.self_generated.type', 'trial'
+ setting 'xpack.security.enabled', 'true'
+ setupCommand 'setupDummyUser',
+ 'bin/elasticsearch-users',
+ 'useradd', System.getProperty('tests.rest.cluster.username', 'test_user'),
+ '-p', System.getProperty('tests.rest.cluster.password', 'test-password'),
+ '-r', 'superuser'
+ waitCondition = { node, ant ->
+ File tmpFile = new File(node.cwd, 'wait.success')
+ ant.get(src: "http://${node.httpUri()}/_cluster/health?wait_for_nodes=>=${numNodes}&wait_for_status=yellow",
+ dest: tmpFile.toString(),
+ username: System.getProperty('tests.rest.cluster.username', 'test_user'),
+ password: System.getProperty('tests.rest.cluster.password', 'test-password'),
+ ignoreerrors: true,
+ retries: 10)
+ return tmpFile.exists()
+ }
}
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 f11d7cc15732..a959e349c151 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
@@ -217,6 +217,7 @@ public class RestHighLevelClient implements Closeable {
private final LicenseClient licenseClient = new LicenseClient(this);
private final MigrationClient migrationClient = new MigrationClient(this);
private final MachineLearningClient machineLearningClient = new MachineLearningClient(this);
+ private final SecurityClient securityClient = new SecurityClient(this);
/**
* Creates a {@link RestHighLevelClient} given the low level {@link RestClientBuilder} that allows to build the
@@ -376,6 +377,20 @@ public class RestHighLevelClient implements Closeable {
return machineLearningClient;
}
+ /**
+ * Provides methods for accessing the Elastic Licensed Security APIs that
+ * are shipped with the Elastic Stack distribution of Elasticsearch. All of
+ * these APIs will 404 if run against the OSS distribution of Elasticsearch.
+ *
+ * See the
+ * Security APIs on elastic.co for more information.
+ *
+ * @return the client wrapper for making Security API calls
+ */
+ public SecurityClient security() {
+ return securityClient;
+ }
+
/**
* Executes a bulk request using the Bulk API.
* See Bulk API on elastic.co
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
new file mode 100644
index 000000000000..19f56fbd1ec9
--- /dev/null
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java
@@ -0,0 +1,69 @@
+/*
+ * 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;
+
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.client.security.PutUserRequest;
+import org.elasticsearch.client.security.PutUserResponse;
+
+import java.io.IOException;
+
+import static java.util.Collections.emptySet;
+
+/**
+ * A wrapper for the {@link RestHighLevelClient} that provides methods for accessing the Security APIs.
+ *
+ * See Security APIs on elastic.co
+ */
+public final class SecurityClient {
+
+ private final RestHighLevelClient restHighLevelClient;
+
+ SecurityClient(RestHighLevelClient restHighLevelClient) {
+ this.restHighLevelClient = restHighLevelClient;
+ }
+
+ /**
+ * 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
+ * @throws IOException in case there is a problem sending the request or parsing back the response
+ */
+ public PutUserResponse putUser(PutUserRequest request, RequestOptions options) throws IOException {
+ return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::putUser, options,
+ PutUserResponse::fromXContent, emptySet());
+ }
+
+ /**
+ * 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 listener the listener to be notified upon request completion
+ */
+ public void putUserAsync(PutUserRequest request, RequestOptions options, ActionListener listener) {
+ restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::putUser, options,
+ PutUserResponse::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
new file mode 100644
index 000000000000..c414cdf82708
--- /dev/null
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java
@@ -0,0 +1,45 @@
+/*
+ * 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;
+
+import org.apache.http.client.methods.HttpPut;
+import org.elasticsearch.client.security.PutUserRequest;
+
+import java.io.IOException;
+
+import static org.elasticsearch.client.RequestConverters.REQUEST_BODY_CONTENT_TYPE;
+import static org.elasticsearch.client.RequestConverters.createEntity;
+
+public final class SecurityRequestConverters {
+
+ private SecurityRequestConverters() {}
+
+ static Request putUser(PutUserRequest putUserRequest) throws IOException {
+ String endpoint = new RequestConverters.EndpointBuilder()
+ .addPathPartAsIs("_xpack/security/user")
+ .addPathPart(putUserRequest.getUsername())
+ .build();
+ Request request = new Request(HttpPut.METHOD_NAME, endpoint);
+ request.setEntity(createEntity(putUserRequest, REQUEST_BODY_CONTENT_TYPE));
+ RequestConverters.Params params = new RequestConverters.Params(request);
+ params.withRefreshPolicy(putUserRequest.getRefreshPolicy());
+ return request;
+ }
+}
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutUserRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutUserRequest.java
new file mode 100644
index 000000000000..f8c30a25aed6
--- /dev/null
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutUserRequest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.client.ValidationException;
+import org.elasticsearch.common.CharArrays;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Request object to create or update a user in the native realm.
+ */
+public final class PutUserRequest implements Validatable, Closeable, ToXContentObject {
+
+ private final String username;
+ private final List roles;
+ private final String fullName;
+ private final String email;
+ private final Map metadata;
+ private final char[] password;
+ private final boolean enabled;
+ private final RefreshPolicy refreshPolicy;
+
+ public PutUserRequest(String username, char[] password, List roles, String fullName, String email, boolean enabled,
+ Map metadata, RefreshPolicy refreshPolicy) {
+ this.username = Objects.requireNonNull(username, "username is required");
+ this.password = password;
+ this.roles = Collections.unmodifiableList(Objects.requireNonNull(roles, "roles must be specified"));
+ this.fullName = fullName;
+ this.email = email;
+ this.enabled = enabled;
+ this.metadata = metadata == null ? Collections.emptyMap() : Collections.unmodifiableMap(metadata);
+ this.refreshPolicy = refreshPolicy == null ? RefreshPolicy.getDefault() : refreshPolicy;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public List getRoles() {
+ return roles;
+ }
+
+ public String getFullName() {
+ return fullName;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ public char[] getPassword() {
+ return password;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public RefreshPolicy getRefreshPolicy() {
+ return refreshPolicy;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ PutUserRequest that = (PutUserRequest) o;
+ return enabled == that.enabled &&
+ Objects.equals(username, that.username) &&
+ Objects.equals(roles, that.roles) &&
+ Objects.equals(fullName, that.fullName) &&
+ Objects.equals(email, that.email) &&
+ Objects.equals(metadata, that.metadata) &&
+ Arrays.equals(password, that.password) &&
+ refreshPolicy == that.refreshPolicy;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(username, roles, fullName, email, metadata, enabled, refreshPolicy);
+ result = 31 * result + Arrays.hashCode(password);
+ return result;
+ }
+
+ @Override
+ public void close() {
+ if (password != null) {
+ Arrays.fill(password, (char) 0);
+ }
+ }
+
+ @Override
+ public Optional validate() {
+ if (metadata != null && metadata.keySet().stream().anyMatch(s -> s.startsWith("_"))) {
+ ValidationException validationException = new ValidationException();
+ validationException.addValidationError("metadata keys may not start with [_]");
+ return Optional.of(validationException);
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject();
+ builder.field("username", username);
+ if (password != null) {
+ byte[] charBytes = CharArrays.toUtf8Bytes(password);
+ builder.field("password").utf8Value(charBytes, 0, charBytes.length);
+ }
+ if (roles != null) {
+ builder.field("roles", roles);
+ }
+ if (fullName != null) {
+ builder.field("full_name", fullName);
+ }
+ if (email != null) {
+ builder.field("email", email);
+ }
+ if (metadata != null) {
+ builder.field("metadata", metadata);
+ }
+ return builder.endObject();
+ }
+}
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutUserResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutUserResponse.java
new file mode 100644
index 000000000000..73b57fb57ecc
--- /dev/null
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutUserResponse.java
@@ -0,0 +1,71 @@
+/*
+ * 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.common.ParseField;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.Objects;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+
+/**
+ * Response when adding a user to the native realm. Returns a
+ * single boolean field for whether the user was created or updated.
+ */
+public final class PutUserResponse {
+
+ private final boolean created;
+
+ public PutUserResponse(boolean created) {
+ this.created = created;
+ }
+
+ public boolean isCreated() {
+ return created;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ PutUserResponse that = (PutUserResponse) o;
+ return created == that.created;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(created);
+ }
+
+ private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("put_user_response",
+ true, args -> new PutUserResponse((boolean) args[0]));
+
+ static {
+ PARSER.declareBoolean(constructorArg(), new ParseField("created"));
+ PARSER.declareObject((a,b) -> {}, (parser, context) -> null, new ParseField("user")); // ignore the user field!
+ }
+
+ public static PutUserResponse fromXContent(XContentParser parser) throws IOException {
+ return PARSER.parse(parser, null);
+ }
+}
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java
index d917102d43d2..f1da9af4a1e7 100644
--- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java
@@ -25,6 +25,7 @@ import org.elasticsearch.action.ingest.PutPipelineRequest;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.ingest.Pipeline;
@@ -33,7 +34,10 @@ import org.junit.AfterClass;
import org.junit.Before;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
import java.util.Collections;
+import java.util.Objects;
public abstract class ESRestHighLevelClientTestCase extends ESRestTestCase {
@@ -137,4 +141,15 @@ public abstract class ESRestHighLevelClientTestCase extends ESRestTestCase {
assertTrue(execute(
request, highLevelClient().cluster()::putSettings, highLevelClient().cluster()::putSettingsAsync).isAcknowledged());
}
+
+ @Override
+ protected Settings restClientSettings() {
+ final String user = Objects.requireNonNull(System.getProperty("tests.rest.cluster.username"));
+ final String pass = Objects.requireNonNull(System.getProperty("tests.rest.cluster.password"));
+ final String token = "Basic " + Base64.getEncoder().encodeToString((user + ":" + pass).getBytes(StandardCharsets.UTF_8));
+ return Settings.builder()
+ .put(super.restClientSettings())
+ .put(ThreadContext.PREFIX + ".Authorization", token)
+ .build();
+ }
}
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java
index 61d5866b8e1e..a6845448b829 100644
--- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java
@@ -2438,7 +2438,7 @@ public class RequestConvertersTests extends ESTestCase {
assertThat(request.getEntity(), nullValue());
}
- private static void assertToXContentBody(ToXContent expectedBody, HttpEntity actualEntity) throws IOException {
+ static void assertToXContentBody(ToXContent expectedBody, HttpEntity actualEntity) throws IOException {
BytesReference expectedBytes = XContentHelper.toXContent(expectedBody, REQUEST_BODY_CONTENT_TYPE, false);
assertEquals(XContentType.JSON.mediaTypeWithoutParameters(), actualEntity.getContentType().getValue());
assertEquals(expectedBytes, new BytesArray(EntityUtils.toByteArray(actualEntity)));
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java
index dcc6a51dabac..b6562cd44cd5 100644
--- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java
@@ -757,7 +757,8 @@ public class RestHighLevelClientTests extends ESTestCase {
apiName.startsWith("machine_learning.") == false &&
apiName.startsWith("watcher.") == false &&
apiName.startsWith("graph.") == false &&
- apiName.startsWith("migration.") == false) {
+ apiName.startsWith("migration.") == false &&
+ apiName.startsWith("security.") == false) {
apiNotFound.add(apiName);
}
}
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
new file mode 100644
index 000000000000..924fc6ddadbe
--- /dev/null
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+import org.apache.http.client.methods.HttpPut;
+import org.elasticsearch.client.security.PutUserRequest;
+import org.elasticsearch.client.security.RefreshPolicy;
+import org.elasticsearch.test.ESTestCase;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.elasticsearch.client.RequestConvertersTests.assertToXContentBody;
+
+public class SecurityRequestConvertersTests extends ESTestCase {
+
+ public void testPutUser() throws IOException {
+ final String username = randomAlphaOfLengthBetween(4, 12);
+ final char[] password = randomBoolean() ? randomAlphaOfLengthBetween(8, 12).toCharArray() : null;
+ final List roles = Arrays.asList(generateRandomStringArray(randomIntBetween(2, 8), randomIntBetween(8, 16), false, true));
+ final String email = randomBoolean() ? null : randomAlphaOfLengthBetween(12, 24);
+ final String fullName = randomBoolean() ? null : randomAlphaOfLengthBetween(7, 14);
+ final boolean enabled = randomBoolean();
+ final Map metadata;
+ if (randomBoolean()) {
+ metadata = new HashMap<>();
+ for (int i = 0; i < randomIntBetween(0, 10); i++) {
+ metadata.put(String.valueOf(i), randomAlphaOfLengthBetween(1, 12));
+ }
+ } else {
+ metadata = null;
+ }
+
+ final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
+ final Map expectedParams;
+ if (refreshPolicy != RefreshPolicy.NONE) {
+ expectedParams = Collections.singletonMap("refresh", refreshPolicy.getValue());
+ } else {
+ expectedParams = Collections.emptyMap();
+ }
+
+ PutUserRequest putUserRequest = new PutUserRequest(username, password, roles, fullName, email, enabled, metadata, refreshPolicy);
+ Request request = SecurityRequestConverters.putUser(putUserRequest);
+ assertEquals(HttpPut.METHOD_NAME, request.getMethod());
+ assertEquals("/_xpack/security/user/" + putUserRequest.getUsername(), request.getEndpoint());
+ assertEquals(expectedParams, request.getParameters());
+ assertToXContentBody(putUserRequest, 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
new file mode 100644
index 000000000000..5741b0539ba0
--- /dev/null
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java
@@ -0,0 +1,84 @@
+/*
+ * 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.documentation;
+
+import org.elasticsearch.action.ActionListener;
+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.PutUserRequest;
+import org.elasticsearch.client.security.PutUserResponse;
+import org.elasticsearch.client.security.RefreshPolicy;
+
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
+
+ public void testPutUser() throws Exception {
+ RestHighLevelClient client = highLevelClient();
+
+ {
+ //tag::x-pack-put-user-execute
+ 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);
+ //end::x-pack-put-user-execute
+
+ //tag::x-pack-put-user-response
+ boolean isCreated = response.isCreated(); // <1>
+ //end::x-pack-put-user-response
+
+ assertTrue(isCreated);
+ }
+
+ {
+ 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::x-pack-put-user-execute-listener
+ ActionListener listener = new ActionListener() {
+ @Override
+ public void onResponse(PutUserResponse response) {
+ // <1>
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ // <2>
+ }
+ };
+ // end::x-pack-put-user-execute-listener
+
+ // Replace the empty listener by a blocking listener in test
+ final CountDownLatch latch = new CountDownLatch(1);
+ listener = new LatchedActionListener<>(listener, latch);
+
+ // tag::x-pack-put-user-execute-async
+ client.security().putUserAsync(request, RequestOptions.DEFAULT, listener); // <1>
+ // end::x-pack-put-user-execute-async
+
+ assertTrue(latch.await(30L, TimeUnit.SECONDS));
+ }
+ }
+}
diff --git a/docs/java-rest/high-level/x-pack/security/put-user.asciidoc b/docs/java-rest/high-level/x-pack/security/put-user.asciidoc
new file mode 100644
index 000000000000..b6d1e0166eed
--- /dev/null
+++ b/docs/java-rest/high-level/x-pack/security/put-user.asciidoc
@@ -0,0 +1,52 @@
+[[java-rest-high-x-pack-security-put-user]]
+=== X-Pack Put User API
+
+[[java-rest-high-x-pack-security-put-user-execution]]
+==== Execution
+
+Creating and updating a user can be performed using the `security().putUser()`
+method:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SecurityDocumentationIT.java[x-pack-put-user-execute]
+--------------------------------------------------
+
+[[java-rest-high-x-pack-security-put-user-response]]
+==== Response
+
+The returned `PutUserResponse` contains a single field, `created`. This field
+serves as an indication if a user was created or if an existing entry was updated.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SecurityDocumentationIT.java[x-pack-put-user-response]
+--------------------------------------------------
+<1> `created` is a boolean indicating whether the user was created or updated
+
+[[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[x-pack-put-user-execute-async]
+--------------------------------------------------
+<1> The `PutUserResponse` 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 `PutUserResponse` looks like:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SecurityDocumentationIT.java[x-pack-put-user-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/x-pack/docs/en/rest-api/security/create-users.asciidoc b/x-pack/docs/en/rest-api/security/create-users.asciidoc
index 91171b0e57eb..789e8c7e80db 100644
--- a/x-pack/docs/en/rest-api/security/create-users.asciidoc
+++ b/x-pack/docs/en/rest-api/security/create-users.asciidoc
@@ -91,8 +91,9 @@ created or updated.
--------------------------------------------------
{
"user": {
- "created" : true <1>
- }
+ "created" : true
+ },
+ "created": true <1>
}
--------------------------------------------------
// TESTRESPONSE
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/rolemapping/PutRoleMappingRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/rolemapping/PutRoleMappingRequest.java
index 0f876f644531..19a84525c301 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/rolemapping/PutRoleMappingRequest.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/rolemapping/PutRoleMappingRequest.java
@@ -151,4 +151,4 @@ public class PutRoleMappingRequest extends ActionRequest
enabled
);
}
-}
\ No newline at end of file
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequest.java
index e704259396a3..f39c56825168 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequest.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequest.java
@@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+
package org.elasticsearch.xpack.core.security.action.user;
import org.elasticsearch.action.ActionRequest;
@@ -31,7 +32,6 @@ public class PutUserRequest extends ActionRequest implements UserRequest, WriteR
private String email;
private Map metadata;
private char[] passwordHash;
- private char[] password;
private boolean enabled = true;
private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE;
@@ -50,9 +50,6 @@ public class PutUserRequest extends ActionRequest implements UserRequest, WriteR
if (metadata != null && metadata.keySet().stream().anyMatch(s -> s.startsWith("_"))) {
validationException = addValidationError("metadata keys may not start with [_]", validationException);
}
- if (password != null && passwordHash != null) {
- validationException = addValidationError("only one of [password, passwordHash] can be provided", validationException);
- }
// we do not check for a password hash here since it is possible that the user exists and we don't want to update the password
return validationException;
}
@@ -85,10 +82,6 @@ public class PutUserRequest extends ActionRequest implements UserRequest, WriteR
this.enabled = enabled;
}
- public void password(@Nullable char[] password) {
- this.password = password;
- }
-
/**
* Should this request trigger a refresh ({@linkplain RefreshPolicy#IMMEDIATE}, the default), wait for a refresh (
* {@linkplain RefreshPolicy#WAIT_UNTIL}), or proceed ignore refreshes entirely ({@linkplain RefreshPolicy#NONE}).
@@ -138,11 +131,6 @@ public class PutUserRequest extends ActionRequest implements UserRequest, WriteR
return new String[] { username };
}
- @Nullable
- public char[] password() {
- return password;
- }
-
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
@@ -161,9 +149,6 @@ public class PutUserRequest extends ActionRequest implements UserRequest, WriteR
super.writeTo(out);
out.writeString(username);
writeCharArrayToStream(out, passwordHash);
- if (password != null) {
- throw new IllegalStateException("password cannot be serialized. it is only used for HL rest");
- }
out.writeStringArray(roles);
out.writeOptionalString(fullName);
out.writeOptionalString(email);
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserResponse.java
index 30d15e5a3fdc..4d0e5fdfa4b4 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserResponse.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserResponse.java
@@ -3,12 +3,13 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+
package org.elasticsearch.xpack.core.security.action.user;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
-import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.ToXContentFragment;
import org.elasticsearch.common.xcontent.XContentBuilder;
import java.io.IOException;
@@ -17,7 +18,7 @@ import java.io.IOException;
* Response when adding a user to the security index. Returns a
* single boolean field for whether the user was created or updated.
*/
-public class PutUserResponse extends ActionResponse implements ToXContentObject {
+public class PutUserResponse extends ActionResponse implements ToXContentFragment {
private boolean created;
@@ -32,12 +33,6 @@ public class PutUserResponse extends ActionResponse implements ToXContentObject
return created;
}
- @Override
- public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
- builder.startObject().field("created", created).endObject();
- return builder;
- }
-
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
@@ -49,4 +44,9 @@ public class PutUserResponse extends ActionResponse implements ToXContentObject
super.readFrom(in);
this.created = in.readBoolean();
}
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ return builder.field("created", created);
+ }
}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/user/UserTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/user/UserTests.java
index d652575bb9fc..02813f53b8c7 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/user/UserTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/user/UserTests.java
@@ -6,7 +6,6 @@
package org.elasticsearch.xpack.core.security.user;
import org.elasticsearch.test.ESTestCase;
-import org.elasticsearch.xpack.core.security.user.User;
import java.util.Collections;
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportPutUserAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportPutUserAction.java
index a2715896da68..ed23b84fbf57 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportPutUserAction.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportPutUserAction.java
@@ -93,10 +93,6 @@ public class TransportPutUserAction extends HandledTransportAction requestBuilder.execute(new RestBuilderListener(channel) {
@Override
public RestResponse buildResponse(PutUserResponse putUserResponse, XContentBuilder builder) throws Exception {
- return new BytesRestResponse(RestStatus.OK,
- builder.startObject()
- .field("user", putUserResponse)
- .endObject());
+ builder.startObject()
+ .startObject("user"); // TODO in 7.0 remove wrapping of response in the user object and just return the object
+ putUserResponse.toXContent(builder, request);
+ builder.endObject();
+
+ putUserResponse.toXContent(builder, request);
+ return new BytesRestResponse(RestStatus.OK, builder.endObject());
}
});
}
diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/PutUserRequestTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/PutUserRequestTests.java
index 952448db4869..e4ebe3da93a0 100644
--- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/PutUserRequestTests.java
+++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/PutUserRequestTests.java
@@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+
package org.elasticsearch.xpack.security.action.user;
import org.elasticsearch.action.ActionRequestValidationException;