Skip to content

Commit 4b8c300

Browse files
committed
HLRest: add xpack put user API
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, the request and response classes were moved to the x-pack protocol project and licensed under the Apache 2 license. The PutUserRequest previously performed some validation of the username that really is more fitting for the server validation. This validation has been removed to reduce the amount of logic on the client side and also reduce the number of classes that would need to be moved to the protocol project. The removed validation now happens in the transport action. See elastic#29827
1 parent a525c36 commit 4b8c300

File tree

48 files changed

+732
-173
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+732
-173
lines changed

client/rest-high-level/build.gradle

+21
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,27 @@ forbiddenApisMain {
7171
signaturesURLs += [file('src/main/resources/forbidden/rest-high-level-signatures.txt').toURI().toURL()]
7272
}
7373

74+
integTestRunner {
75+
systemProperty 'tests.rest.cluster.username', System.getProperty('tests.rest.cluster.username', 'test_user')
76+
systemProperty 'tests.rest.cluster.password', System.getProperty('tests.rest.cluster.password', 'test-password')
77+
}
78+
7479
integTestCluster {
7580
setting 'xpack.license.self_generated.type', 'trial'
81+
setting 'xpack.security.enabled', 'true'
82+
setupCommand 'setupDummyUser',
83+
'bin/elasticsearch-users',
84+
'useradd', System.getProperty('tests.rest.cluster.username', 'test_user'),
85+
'-p', System.getProperty('tests.rest.cluster.password', 'test-password'),
86+
'-r', 'superuser'
87+
waitCondition = { node, ant ->
88+
File tmpFile = new File(node.cwd, 'wait.success')
89+
ant.get(src: "http://${node.httpUri()}/_cluster/health?wait_for_nodes=>=${numNodes}&wait_for_status=yellow",
90+
dest: tmpFile.toString(),
91+
username: System.getProperty('tests.rest.cluster.username', 'test_user'),
92+
password: System.getProperty('tests.rest.cluster.password', 'test-password'),
93+
ignoreerrors: true,
94+
retries: 10)
95+
return tmpFile.exists()
96+
}
7697
}

client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java

+10
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
import org.elasticsearch.protocol.xpack.XPackInfoRequest;
110110
import org.elasticsearch.protocol.xpack.watcher.PutWatchRequest;
111111
import org.elasticsearch.protocol.xpack.XPackUsageRequest;
112+
import org.elasticsearch.protocol.xpack.security.PutUserRequest;
112113
import org.elasticsearch.rest.action.search.RestSearchAction;
113114
import org.elasticsearch.script.mustache.MultiSearchTemplateRequest;
114115
import org.elasticsearch.script.mustache.SearchTemplateRequest;
@@ -1139,6 +1140,15 @@ static Request xpackUsage(XPackUsageRequest usageRequest) {
11391140
return request;
11401141
}
11411142

1143+
static Request putUser(PutUserRequest putUserRequest) throws IOException {
1144+
String endpoint = new EndpointBuilder().addPathPartAsIs("_xpack/security/user").addPathPart(putUserRequest.username()).build();
1145+
Request request = new Request(HttpPut.METHOD_NAME, endpoint);
1146+
request.setEntity(createEntity(putUserRequest, REQUEST_BODY_CONTENT_TYPE));
1147+
Params params = new Params(request);
1148+
params.withRefreshPolicy(putUserRequest.getRefreshPolicy());
1149+
return request;
1150+
}
1151+
11421152
private static HttpEntity createEntity(ToXContent toXContent, XContentType xContentType) throws IOException {
11431153
BytesRef source = XContentHelper.toXContent(toXContent, xContentType, false).toBytesRef();
11441154
return new ByteArrayEntity(source.bytes, source.offset, source.length, createContentType(xContentType));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.client;
21+
22+
import org.elasticsearch.action.ActionListener;
23+
import org.elasticsearch.common.xcontent.XContentParser;
24+
import org.elasticsearch.protocol.xpack.security.PutUserRequest;
25+
import org.elasticsearch.protocol.xpack.security.PutUserResponse;
26+
27+
import java.io.IOException;
28+
29+
import static java.util.Collections.emptySet;
30+
31+
/**
32+
* A wrapper for the {@link RestHighLevelClient} that provides methods for accessing the Security APIs.
33+
* <p>
34+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api.html">Security APIs on elastic.co</a>
35+
*/
36+
public final class SecurityClient {
37+
38+
private final RestHighLevelClient restHighLevelClient;
39+
40+
SecurityClient(RestHighLevelClient restHighLevelClient) {
41+
this.restHighLevelClient = restHighLevelClient;
42+
}
43+
44+
/**
45+
* Create/update a user in the native realm synchronously.
46+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html">
47+
* the docs</a> for more.
48+
* @param request the request with the user's information
49+
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
50+
* @return the response from the put user call
51+
* @throws IOException in case there is a problem sending the request or parsing back the response
52+
*/
53+
public PutUserResponse putUser(PutUserRequest request, RequestOptions options) throws IOException {
54+
return restHighLevelClient.performRequestAndParseEntity(request, RequestConverters::putUser, options,
55+
SecurityClient::parsePutUserResponse, emptySet());
56+
}
57+
58+
/**
59+
* Asynchronously create/update a user in the native realm.
60+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html">
61+
* the docs</a> for more.
62+
* @param request the request with the user's information
63+
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
64+
* @param listener the listener to be notified upon request completion
65+
*/
66+
public void putUserAsync(PutUserRequest request, RequestOptions options, ActionListener<PutUserResponse> listener) {
67+
restHighLevelClient.performRequestAsyncAndParseEntity(request, RequestConverters::putUser, options,
68+
SecurityClient::parsePutUserResponse, listener, emptySet());
69+
}
70+
71+
/**
72+
* Parses the rest response from the put user API. The rest API wraps the XContent of the
73+
* {@link PutUserResponse} in a field, so this method unwraps this value.
74+
*
75+
* @param parser the XContent parser for the response body
76+
* @return the {@link PutUserResponse} parsed
77+
* @throws IOException in case there is a problem parsing the response
78+
*/
79+
private static PutUserResponse parsePutUserResponse(XContentParser parser) throws IOException {
80+
XContentParser.Token token;
81+
String currentFieldName = null;
82+
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
83+
if (token == XContentParser.Token.FIELD_NAME) {
84+
currentFieldName = parser.currentName();
85+
} else if ("user".equals(currentFieldName)) {
86+
return PutUserResponse.fromXContent(parser);
87+
}
88+
}
89+
throw new IOException("Failed to parse [put_user_response]");
90+
}
91+
}

client/rest-high-level/src/main/java/org/elasticsearch/client/XPackClient.java

+11
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,12 @@ public final class XPackClient {
4242

4343
private final RestHighLevelClient restHighLevelClient;
4444
private final WatcherClient watcherClient;
45+
private final SecurityClient securityClient;
4546

4647
XPackClient(RestHighLevelClient restHighLevelClient) {
4748
this.restHighLevelClient = restHighLevelClient;
4849
this.watcherClient = new WatcherClient(restHighLevelClient);
50+
this.securityClient = new SecurityClient(restHighLevelClient);
4951
}
5052

5153
public WatcherClient watcher() {
@@ -100,4 +102,13 @@ public void usageAsync(XPackUsageRequest request, RequestOptions options, Action
100102
restHighLevelClient.performRequestAsyncAndParseEntity(request, RequestConverters::xpackUsage, options,
101103
XPackUsageResponse::fromXContent, listener, emptySet());
102104
}
105+
106+
/**
107+
* Provides an {@link SecurityClient} which can be used to access the Security APIs.
108+
*
109+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api.html">Security APIs on elastic.co</a>
110+
*/
111+
public SecurityClient security() {
112+
return securityClient;
113+
}
103114
}

client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java

+15
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.elasticsearch.action.support.PlainActionFuture;
2626
import org.elasticsearch.common.bytes.BytesReference;
2727
import org.elasticsearch.common.settings.Settings;
28+
import org.elasticsearch.common.util.concurrent.ThreadContext;
2829
import org.elasticsearch.common.xcontent.XContentBuilder;
2930
import org.elasticsearch.common.xcontent.XContentType;
3031
import org.elasticsearch.ingest.Pipeline;
@@ -33,7 +34,10 @@
3334
import org.junit.Before;
3435

3536
import java.io.IOException;
37+
import java.nio.charset.StandardCharsets;
38+
import java.util.Base64;
3639
import java.util.Collections;
40+
import java.util.Objects;
3741

3842
public abstract class ESRestHighLevelClientTestCase extends ESRestTestCase {
3943

@@ -136,4 +140,15 @@ protected static void clusterUpdateSettings(Settings persistentSettings,
136140
request.transientSettings(transientSettings);
137141
assertOK(client().performRequest(RequestConverters.clusterPutSettings(request)));
138142
}
143+
144+
@Override
145+
protected Settings restClientSettings() {
146+
final String user = Objects.requireNonNull(System.getProperty("tests.rest.cluster.username"));
147+
final String pass = Objects.requireNonNull(System.getProperty("tests.rest.cluster.password"));
148+
final String token = "Basic " + Base64.getEncoder().encodeToString((user + ":" + pass).getBytes(StandardCharsets.UTF_8));
149+
return Settings.builder()
150+
.put(super.restClientSettings())
151+
.put(ThreadContext.PREFIX + ".Authorization", token)
152+
.build();
153+
}
139154
}

client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java

+33
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126
import org.elasticsearch.index.rankeval.RatedRequest;
127127
import org.elasticsearch.index.rankeval.RestRankEvalAction;
128128
import org.elasticsearch.protocol.xpack.XPackInfoRequest;
129+
import org.elasticsearch.protocol.xpack.security.PutUserRequest;
129130
import org.elasticsearch.protocol.xpack.watcher.PutWatchRequest;
130131
import org.elasticsearch.repositories.fs.FsRepository;
131132
import org.elasticsearch.rest.action.search.RestSearchAction;
@@ -2580,6 +2581,38 @@ public void testXPackPutWatch() throws Exception {
25802581
assertThat(bos.toString("UTF-8"), is(body));
25812582
}
25822583

2584+
public void testPutUser() throws IOException {
2585+
PutUserRequest putUserRequest = new PutUserRequest();
2586+
putUserRequest.username(randomAlphaOfLengthBetween(4, 12));
2587+
if (randomBoolean()) {
2588+
putUserRequest.password(randomAlphaOfLengthBetween(8, 12).toCharArray());
2589+
}
2590+
putUserRequest.roles(generateRandomStringArray(randomIntBetween(2, 8), randomIntBetween(8, 16), false, true));
2591+
putUserRequest.email(randomBoolean() ? null : randomAlphaOfLengthBetween(12, 24));
2592+
putUserRequest.fullName(randomBoolean() ? null : randomAlphaOfLengthBetween(7, 14));
2593+
putUserRequest.enabled(randomBoolean());
2594+
if (randomBoolean()) {
2595+
Map<String, Object> metadata = new HashMap<>();
2596+
for (int i = 0; i < randomIntBetween(1, 10); i++) {
2597+
metadata.put(String.valueOf(i), randomAlphaOfLengthBetween(1, 12));
2598+
}
2599+
putUserRequest.metadata(metadata);
2600+
}
2601+
putUserRequest.setRefreshPolicy(randomFrom(WriteRequest.RefreshPolicy.values()));
2602+
final Map<String, String> expectedParams;
2603+
if (putUserRequest.getRefreshPolicy() != WriteRequest.RefreshPolicy.NONE) {
2604+
expectedParams = Collections.singletonMap("refresh", putUserRequest.getRefreshPolicy().getValue());
2605+
} else {
2606+
expectedParams = Collections.emptyMap();
2607+
}
2608+
2609+
Request request = RequestConverters.putUser(putUserRequest);
2610+
assertEquals(HttpPut.METHOD_NAME, request.getMethod());
2611+
assertEquals("/_xpack/security/user/" + putUserRequest.username(), request.getEndpoint());
2612+
assertEquals(expectedParams, request.getParameters());
2613+
assertToXContentBody(putUserRequest, request.getEntity());
2614+
}
2615+
25832616
/**
25842617
* Randomize the {@link FetchSourceContext} request parameters.
25852618
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.client.documentation;
21+
22+
import org.elasticsearch.action.ActionListener;
23+
import org.elasticsearch.action.LatchedActionListener;
24+
import org.elasticsearch.client.ESRestHighLevelClientTestCase;
25+
import org.elasticsearch.client.RequestOptions;
26+
import org.elasticsearch.client.RestHighLevelClient;
27+
import org.elasticsearch.protocol.xpack.security.PutUserRequest;
28+
import org.elasticsearch.protocol.xpack.security.PutUserResponse;
29+
30+
import java.util.concurrent.CountDownLatch;
31+
import java.util.concurrent.TimeUnit;
32+
33+
public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
34+
35+
public void testPutUser() throws Exception {
36+
RestHighLevelClient client = highLevelClient();
37+
38+
{
39+
//tag::x-pack-put-user-execute
40+
PutUserRequest request = new PutUserRequest();
41+
request.username("example");
42+
request.roles("superuser");
43+
request.password(new char[] { 'p', 'a', 's', 's', 'w', 'o', 'r', 'd' } );
44+
PutUserResponse response = client.xpack().security().putUser(request, RequestOptions.DEFAULT);
45+
//end::x-pack-put-user-execute
46+
47+
//tag::x-pack-put-user-response
48+
boolean isCreated = response.created(); // <1>
49+
//end::x-pack-put-user-response
50+
}
51+
52+
{
53+
PutUserRequest request = new PutUserRequest();
54+
request.username("example2");
55+
request.roles("superuser");
56+
request.password(new char[] { 'p', 'a', 's', 's', 'w', 'o', 'r', 'd' } );
57+
// tag::x-pack-put-user-execute-listener
58+
ActionListener<PutUserResponse> listener = new ActionListener<PutUserResponse>() {
59+
@Override
60+
public void onResponse(PutUserResponse response) {
61+
// <1>
62+
}
63+
64+
@Override
65+
public void onFailure(Exception e) {
66+
// <2>
67+
}
68+
};
69+
// end::x-pack-put-user-execute-listener
70+
71+
// Replace the empty listener by a blocking listener in test
72+
final CountDownLatch latch = new CountDownLatch(1);
73+
listener = new LatchedActionListener<>(listener, latch);
74+
75+
// tag::x-pack-put-user-execute-async
76+
client.xpack().security().putUserAsync(request, RequestOptions.DEFAULT, listener); // <1>
77+
// end::x-pack-put-user-execute-async
78+
79+
assertTrue(latch.await(30L, TimeUnit.SECONDS));
80+
}
81+
}
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
[[java-rest-high-x-pack-security-put-user]]
2+
=== X-Pack Put User API
3+
4+
[[java-rest-high-x-pack-security-put-user-execution]]
5+
==== Execution
6+
7+
Creating and updating a user can be performed using the `security().putUser()`
8+
method:
9+
10+
["source","java",subs="attributes,callouts,macros"]
11+
--------------------------------------------------
12+
include-tagged::{doc-tests}/SecurityDocumentationIT.java[x-pack-put-user-execute]
13+
--------------------------------------------------
14+
15+
[[java-rest-high-x-pack-security-put-user-response]]
16+
==== Response
17+
18+
The returned `PutUserResponse` contains a single field, `created`. This field
19+
serves as an indication if a user was created or if an existing entry was updated.
20+
21+
["source","java",subs="attributes,callouts,macros"]
22+
--------------------------------------------------
23+
include-tagged::{doc-tests}/SecurityDocumentationIT.java[x-pack-put-user-response]
24+
--------------------------------------------------
25+
<1> `created` is a boolean indicating whether the user was created or updated
26+
27+
[[java-rest-high-x-pack-security-put-user-async]]
28+
==== Asynchronous Execution
29+
30+
This request can be executed asynchronously:
31+
32+
["source","java",subs="attributes,callouts,macros"]
33+
--------------------------------------------------
34+
include-tagged::{doc-tests}/WatcherDocumentationIT.java[x-pack-put-user-execute-async]
35+
--------------------------------------------------
36+
<1> The `PutUserResponse` to execute and the `ActionListener` to use when
37+
the execution completes
38+
39+
The asynchronous method does not block and returns immediately. Once the request
40+
has completed the `ActionListener` is called back using the `onResponse` method
41+
if the execution successfully completed or using the `onFailure` method if
42+
it failed.
43+
44+
A typical listener for a `PutUserResponse` looks like:
45+
46+
["source","java",subs="attributes,callouts,macros"]
47+
--------------------------------------------------
48+
include-tagged::{doc-tests}/SecurityDocumentationIT.java[x-pack-put-user-execute-listener]
49+
--------------------------------------------------
50+
<1> Called when the execution is successfully completed. The response is
51+
provided as an argument
52+
<2> Called in case of failure. The raised exception is provided as an argument

0 commit comments

Comments
 (0)