HLRC: Add delete user action (#35294)

* HLRC: Add delete user action

It adds delete user action to the high level rest client.

Relates #29827
This commit is contained in:
Ignacio Vera 2018-11-29 07:52:56 +01:00 committed by GitHub
parent e0a678f0c4
commit 93ed8b7d61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 407 additions and 3 deletions

View File

@ -35,6 +35,8 @@ import org.elasticsearch.client.security.DeleteRoleMappingRequest;
import org.elasticsearch.client.security.DeleteRoleMappingResponse;
import org.elasticsearch.client.security.DeleteRoleRequest;
import org.elasticsearch.client.security.DeleteRoleResponse;
import org.elasticsearch.client.security.DeleteUserRequest;
import org.elasticsearch.client.security.DeleteUserResponse;
import org.elasticsearch.client.security.DisableUserRequest;
import org.elasticsearch.client.security.EmptyResponse;
import org.elasticsearch.client.security.EnableUserRequest;
@ -102,6 +104,33 @@ public final class SecurityClient {
PutUserResponse::fromXContent, listener, emptySet());
}
/**
* Removes user from the native realm synchronously.
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delete-user.html">
* the docs</a> for more.
* @param request the request with the user to delete
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
* @return the response from the delete user call
* @throws IOException in case there is a problem sending the request or parsing back the response
*/
public DeleteUserResponse deleteUser(DeleteUserRequest request, RequestOptions options) throws IOException {
return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::deleteUser, options,
DeleteUserResponse::fromXContent, singleton(404));
}
/**
* Asynchronously deletes a user in the native realm.
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delete-user.html">
* the docs</a> for more.
* @param request the request with the user to delete
* @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 deleteUserAsync(DeleteUserRequest request, RequestOptions options, ActionListener<DeleteUserResponse> listener) {
restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::deleteUser, options,
DeleteUserResponse::fromXContent, listener, singleton(404));
}
/**
* Create/Update a role mapping.
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-role-mapping.html">

View File

@ -31,6 +31,7 @@ import org.elasticsearch.client.security.DeletePrivilegesRequest;
import org.elasticsearch.client.security.GetPrivilegesRequest;
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
import org.elasticsearch.client.security.DeleteRoleRequest;
import org.elasticsearch.client.security.DeleteUserRequest;
import org.elasticsearch.client.security.InvalidateTokenRequest;
import org.elasticsearch.client.security.GetRolesRequest;
import org.elasticsearch.client.security.PutRoleMappingRequest;
@ -76,6 +77,17 @@ final class SecurityRequestConverters {
return request;
}
static Request deleteUser(DeleteUserRequest deleteUserRequest) {
String endpoint = new RequestConverters.EndpointBuilder()
.addPathPartAsIs("_xpack","security", "user")
.addPathPart(deleteUserRequest.getName())
.build();
Request request = new Request(HttpDelete.METHOD_NAME, endpoint);
RequestConverters.Params params = new RequestConverters.Params(request);
params.withRefreshPolicy(deleteUserRequest.getRefreshPolicy());
return request;
}
static Request putRoleMapping(final PutRoleMappingRequest putRoleMappingRequest) throws IOException {
final String endpoint = new RequestConverters.EndpointBuilder()
.addPathPartAsIs("_xpack/security/role_mapping")

View File

@ -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.client.Validatable;
import java.util.Objects;
/**
* A request to delete a user from the native realm.
*/
public final class DeleteUserRequest implements Validatable {
private final String name;
private final RefreshPolicy refreshPolicy;
public DeleteUserRequest(String name) {
this(name, RefreshPolicy.IMMEDIATE);
}
public DeleteUserRequest(String name, RefreshPolicy refreshPolicy) {
this.name = Objects.requireNonNull(name, "user name is required");
this.refreshPolicy = Objects.requireNonNull(refreshPolicy, "refresh policy is required");
}
public String getName() {
return name;
}
public RefreshPolicy getRefreshPolicy() {
return refreshPolicy;
}
@Override
public int hashCode() {
return Objects.hash(name, refreshPolicy);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final DeleteUserRequest other = (DeleteUserRequest) obj;
return (refreshPolicy == other.refreshPolicy) && Objects.equals(name, other.name);
}
}

View File

@ -0,0 +1,50 @@
/*
* 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.core.AcknowledgedResponse;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
/**
* Response for a user being deleted from the native realm
*/
public final class DeleteUserResponse extends AcknowledgedResponse {
private static final String PARSE_FIELD_NAME = "found";
private static final ConstructingObjectParser<DeleteUserResponse, Void> PARSER = AcknowledgedResponse
.generateParser("delete_user_response", DeleteUserResponse::new, PARSE_FIELD_NAME);
public DeleteUserResponse(boolean acknowledged) {
super(acknowledged);
}
public static DeleteUserResponse fromXContent(final XContentParser parser) throws IOException {
return PARSER.parse(parser, null);
}
@Override
protected String getFieldName() {
return PARSE_FIELD_NAME;
}
}

View File

@ -22,6 +22,8 @@ package org.elasticsearch.client;
import org.apache.http.client.methods.HttpDelete;
import org.elasticsearch.ElasticsearchStatusException;
import org.elasticsearch.client.security.AuthenticateResponse;
import org.elasticsearch.client.security.DeleteUserRequest;
import org.elasticsearch.client.security.DeleteUserResponse;
import org.elasticsearch.client.security.PutUserRequest;
import org.elasticsearch.client.security.PutUserResponse;
import org.elasticsearch.client.security.RefreshPolicy;
@ -74,14 +76,22 @@ public class SecurityIT extends ESRestHighLevelClientTestCase {
assertThat(authenticateResponse.enabled(), is(true));
// delete user
final Request deleteUserRequest = new Request(HttpDelete.METHOD_NAME,
"/_xpack/security/user/" + putUserRequest.getUser().getUsername());
highLevelClient().getLowLevelClient().performRequest(deleteUserRequest);
final DeleteUserRequest deleteUserRequest =
new DeleteUserRequest(putUserRequest.getUser().getUsername(), putUserRequest.getRefreshPolicy());
final DeleteUserResponse deleteUserResponse =
execute(deleteUserRequest, securityClient::deleteUser, securityClient::deleteUserAsync);
assertThat(deleteUserResponse.isAcknowledged(), is(true));
// authentication no longer works
ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> execute(securityClient::authenticate,
securityClient::authenticateAsync, authorizationRequestOptions(basicAuthHeader)));
assertThat(e.getMessage(), containsString("unable to authenticate user [" + putUserRequest.getUser().getUsername() + "]"));
// delete non-existing user
final DeleteUserResponse deleteUserResponse2 =
execute(deleteUserRequest, securityClient::deleteUser, securityClient::deleteUserAsync);
assertThat(deleteUserResponse2.isAcknowledged(), is(false));
}
private static User randomUser() {

View File

@ -27,6 +27,7 @@ import org.elasticsearch.client.security.CreateTokenRequest;
import org.elasticsearch.client.security.DeletePrivilegesRequest;
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
import org.elasticsearch.client.security.DeleteRoleRequest;
import org.elasticsearch.client.security.DeleteUserRequest;
import org.elasticsearch.client.security.DisableUserRequest;
import org.elasticsearch.client.security.EnableUserRequest;
import org.elasticsearch.client.security.GetPrivilegesRequest;
@ -80,6 +81,18 @@ public class SecurityRequestConvertersTests extends ESTestCase {
assertToXContentBody(putUserRequest, request.getEntity());
}
public void testDeleteUser() {
final String name = randomAlphaOfLengthBetween(4, 12);
final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
final Map<String, String> expectedParams = getExpectedParamsFromRefreshPolicy(refreshPolicy);
DeleteUserRequest deleteUserRequest = new DeleteUserRequest(name, refreshPolicy);
Request request = SecurityRequestConverters.deleteUser(deleteUserRequest);
assertEquals(HttpDelete.METHOD_NAME, request.getMethod());
assertEquals("/_xpack/security/user/" + name, request.getEndpoint());
assertEquals(expectedParams, request.getParameters());
assertNull(request.getEntity());
}
public void testPutRoleMapping() throws IOException {
final String username = randomAlphaOfLengthBetween(4, 7);
final String rolename = randomAlphaOfLengthBetween(4, 7);

View File

@ -45,6 +45,8 @@ import org.elasticsearch.client.security.DeleteRoleMappingRequest;
import org.elasticsearch.client.security.DeleteRoleMappingResponse;
import org.elasticsearch.client.security.DeleteRoleRequest;
import org.elasticsearch.client.security.DeleteRoleResponse;
import org.elasticsearch.client.security.DeleteUserRequest;
import org.elasticsearch.client.security.DeleteUserResponse;
import org.elasticsearch.client.security.DisableUserRequest;
import org.elasticsearch.client.security.EmptyResponse;
import org.elasticsearch.client.security.EnableUserRequest;
@ -185,6 +187,67 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
}
}
public void testDeleteUser() throws Exception {
RestHighLevelClient client = highLevelClient();
addUser(client, "testUser", "testPassword");
{
// tag::delete-user-request
DeleteUserRequest deleteUserRequest = new DeleteUserRequest(
"testUser"); // <1>
// end::delete-user-request
// tag::delete-user-execute
DeleteUserResponse deleteUserResponse = client.security().deleteUser(deleteUserRequest, RequestOptions.DEFAULT);
// end::delete-user-execute
// tag::delete-user-response
boolean found = deleteUserResponse.isAcknowledged(); // <1>
// end::delete-user-response
assertTrue(found);
// check if deleting the already deleted user again will give us a different response
deleteUserResponse = client.security().deleteUser(deleteUserRequest, RequestOptions.DEFAULT);
assertFalse(deleteUserResponse.isAcknowledged());
}
{
DeleteUserRequest deleteUserRequest = new DeleteUserRequest("testUser", RefreshPolicy.IMMEDIATE);
ActionListener<DeleteUserResponse> listener;
//tag::delete-user-execute-listener
listener = new ActionListener<DeleteUserResponse>() {
@Override
public void onResponse(DeleteUserResponse deleteUserResponse) {
// <1>
}
@Override
public void onFailure(Exception e) {
// <2>
}
};
//end::delete-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::delete-user-execute-async
client.security().deleteUserAsync(deleteUserRequest, RequestOptions.DEFAULT, listener); // <1>
//end::delete-user-execute-async
assertTrue(latch.await(30L, TimeUnit.SECONDS));
}
}
private void addUser(RestHighLevelClient client, String userName, String password) throws IOException {
User user = new User(userName, Collections.singletonList(userName));
PutUserRequest request = new PutUserRequest(user, password.toCharArray(), true, RefreshPolicy.NONE);
PutUserResponse response = client.security().putUser(request, RequestOptions.DEFAULT);
assertTrue(response.isCreated());
}
public void testPutRoleMapping() throws Exception {
final RestHighLevelClient client = highLevelClient();

View File

@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.client.security;
import org.elasticsearch.common.bytes.BytesReference;

View File

@ -0,0 +1,78 @@
/*
* 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.test.ESTestCase;
import org.elasticsearch.test.EqualsHashCodeTestUtils;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.equalTo;
public class DeleteUserRequestTests extends ESTestCase {
public void testDeleteUserRequest() {
final String name = randomAlphaOfLength(10);
final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
final DeleteUserRequest deleteUserRequest = new DeleteUserRequest(name, refreshPolicy);
assertThat(deleteUserRequest.getName(), equalTo(name));
assertThat(deleteUserRequest.getRefreshPolicy(), equalTo(refreshPolicy));
}
public void testDeleteUserRequestThrowsExceptionForNullName() {
final NullPointerException ile =
expectThrows(NullPointerException.class, () -> new DeleteUserRequest(null, randomFrom(RefreshPolicy.values())));
assertThat(ile.getMessage(), equalTo("user name is required"));
}
public void testDeleteUserRequestThrowsExceptionForNullRefreshPolicy() {
final NullPointerException ile =
expectThrows(NullPointerException.class, () -> new DeleteUserRequest(randomAlphaOfLength(10), null));
assertThat(ile.getMessage(), equalTo("refresh policy is required"));
}
public void testEqualsHashCode() {
final String name = randomAlphaOfLength(10);
final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
final DeleteUserRequest deleteUserRequest = new DeleteUserRequest(name, refreshPolicy);
assertNotNull(deleteUserRequest);
EqualsHashCodeTestUtils.checkEqualsAndHashCode(deleteUserRequest, (original) -> {
return new DeleteUserRequest(original.getName(), original.getRefreshPolicy());
});
EqualsHashCodeTestUtils.checkEqualsAndHashCode(deleteUserRequest, (original) -> {
return new DeleteUserRequest(original.getName(), original.getRefreshPolicy());
}, DeleteUserRequestTests::mutateTestItem);
}
private static DeleteUserRequest mutateTestItem(DeleteUserRequest original) {
if (randomBoolean()) {
return new DeleteUserRequest(randomAlphaOfLength(10), original.getRefreshPolicy());
} else {
List<RefreshPolicy> values = Arrays.stream(RefreshPolicy.values()).filter(rp -> rp != original.getRefreshPolicy()).collect(
Collectors.toList());
return new DeleteUserRequest(original.getName(), randomFrom(values));
}
}
}

View File

@ -0,0 +1,43 @@
/*
* 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.bytes.BytesReference;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.test.ESTestCase;
import java.io.IOException;
public class DeleteUserResponseTests extends ESTestCase {
public void testParsingWithMissingField() throws IOException {
XContentType contentType = randomFrom(XContentType.values());
XContentBuilder builder = XContentFactory.contentBuilder(contentType).startObject().endObject();
BytesReference bytes = BytesReference.bytes(builder);
XContentParser parser = XContentFactory.xContent(contentType)
.createParser(NamedXContentRegistry.EMPTY, null, bytes.streamInput());
parser.nextToken();
expectThrows(IllegalArgumentException.class, () -> DeleteUserResponse.fromXContent(parser));
}
}

View File

@ -0,0 +1,32 @@
--
:api: delete-user
:request: DeleteUserRequest
:response: DeleteUserResponse
--
[id="{upid}-{api}"]
=== Delete User API
[id="{upid}-{api}-request"]
==== Delete User Request
A user can be deleted as follows:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests-file}[{api}-request]
--------------------------------------------------
[id="{upid}-{api}-response"]
==== Delete Response
The returned +{response}+ allows to retrieve information about the executed
operation as follows:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests-file}[{api}-response]
--------------------------------------------------
<1> whether the given user was found
include::../execution.asciidoc[]

View File

@ -373,6 +373,7 @@ include::rollup/get_rollup_index_caps.asciidoc[]
The Java High Level REST Client supports the following Security APIs:
* <<java-rest-high-security-put-user>>
* <<{upid}-delete-user>>
* <<java-rest-high-security-enable-user>>
* <<java-rest-high-security-disable-user>>
* <<java-rest-high-security-change-password>>
@ -392,6 +393,7 @@ The Java High Level REST Client supports the following Security APIs:
* <<{upid}-delete-privileges>>
include::security/put-user.asciidoc[]
include::security/delete-user.asciidoc[]
include::security/enable-user.asciidoc[]
include::security/disable-user.asciidoc[]
include::security/change-password.asciidoc[]