HLRest: add put user API (#32332)
This commit adds a security client to the high level rest client, which includes an implementation for the put user api. As part of these changes, a new request and response class have been added that are specific to the high level rest client. One change here is that the response was previously wrapped inside a user object. The plan is to remove this wrapping and this PR adds an unwrapped response outside of the user object so we can remove the user object later on. See #29827
This commit is contained in:
parent
7eef7f441b
commit
ea52277a1e
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
* <p>
|
||||
* See the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api.html">
|
||||
* Security APIs on elastic.co</a> 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 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html">Bulk API on elastic.co</a>
|
||||
|
|
|
@ -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.
|
||||
* <p>
|
||||
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api.html">Security APIs on elastic.co</a>
|
||||
*/
|
||||
public final class SecurityClient {
|
||||
|
||||
private final RestHighLevelClient restHighLevelClient;
|
||||
|
||||
SecurityClient(RestHighLevelClient restHighLevelClient) {
|
||||
this.restHighLevelClient = restHighLevelClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create/update a user in the native realm synchronously.
|
||||
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html">
|
||||
* the docs</a> for more.
|
||||
* @param request the request with the user's information
|
||||
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
|
||||
* @return the response from the put user call
|
||||
* @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 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html">
|
||||
* the docs</a> for more.
|
||||
* @param request the request with the user's information
|
||||
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
|
||||
* @param listener the listener to be notified upon request completion
|
||||
*/
|
||||
public void putUserAsync(PutUserRequest request, RequestOptions options, ActionListener<PutUserResponse> listener) {
|
||||
restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::putUser, options,
|
||||
PutUserResponse::fromXContent, listener, emptySet());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<String> roles;
|
||||
private final String fullName;
|
||||
private final String email;
|
||||
private final Map<String, Object> metadata;
|
||||
private final char[] password;
|
||||
private final boolean enabled;
|
||||
private final RefreshPolicy refreshPolicy;
|
||||
|
||||
public PutUserRequest(String username, char[] password, List<String> roles, String fullName, String email, boolean enabled,
|
||||
Map<String, Object> 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<String> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
public String getFullName() {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public Map<String, Object> 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<ValidationException> 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();
|
||||
}
|
||||
}
|
|
@ -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<PutUserResponse, Void> 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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> 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<String, Object> 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<String, String> 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());
|
||||
}
|
||||
}
|
|
@ -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<PutUserResponse> listener = new ActionListener<PutUserResponse>() {
|
||||
@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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -91,8 +91,9 @@ created or updated.
|
|||
--------------------------------------------------
|
||||
{
|
||||
"user": {
|
||||
"created" : true <1>
|
||||
}
|
||||
"created" : true
|
||||
},
|
||||
"created": true <1>
|
||||
}
|
||||
--------------------------------------------------
|
||||
// TESTRESPONSE
|
||||
|
|
|
@ -151,4 +151,4 @@ public class PutRoleMappingRequest extends ActionRequest
|
|||
enabled
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, Object> 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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -93,10 +93,6 @@ public class TransportPutUserAction extends HandledTransportAction<PutUserReques
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (request.password() != null) {
|
||||
validationException = addValidationError("password should never be passed to the transport action", validationException);
|
||||
}
|
||||
return validationException;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,10 +58,13 @@ public class RestPutUserAction extends SecurityBaseRestHandler implements RestRe
|
|||
return channel -> requestBuilder.execute(new RestBuilderListener<PutUserResponse>(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());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue