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;