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 index ab7b3e33905..a033ee61f79 100644 --- 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 @@ -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 + * the docs 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 + * the docs 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 listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::deleteUser, options, + DeleteUserResponse::fromXContent, listener, singleton(404)); + } + /** * Create/Update a role mapping. * See 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 index a1123de7251..6485899acf9 100644 --- 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 @@ -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") diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserRequest.java new file mode 100644 index 00000000000..6995cfdfbca --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserRequest.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.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); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserResponse.java new file mode 100644 index 00000000000..31306dd7478 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserResponse.java @@ -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 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; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java index 5a5091fe758..27b1d31e6d7 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java @@ -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() { 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 index dcfa9210094..110e0cc56c9 100644 --- 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 @@ -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 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); 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 index d44e91652ec..79258b31451 100644 --- 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 @@ -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 listener; + //tag::delete-user-execute-listener + listener = new ActionListener() { + @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(); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteRoleResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteRoleResponseTests.java index a2c08fcf881..ea16c9fad33 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteRoleResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteRoleResponseTests.java @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + package org.elasticsearch.client.security; import org.elasticsearch.common.bytes.BytesReference; diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteUserRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteUserRequestTests.java new file mode 100644 index 00000000000..a317d41f21b --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteUserRequestTests.java @@ -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 values = Arrays.stream(RefreshPolicy.values()).filter(rp -> rp != original.getRefreshPolicy()).collect( + Collectors.toList()); + return new DeleteUserRequest(original.getName(), randomFrom(values)); + } + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteUserResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteUserResponseTests.java new file mode 100644 index 00000000000..e27ebad6d01 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteUserResponseTests.java @@ -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)); + } +} diff --git a/docs/java-rest/high-level/security/delete-user.asciidoc b/docs/java-rest/high-level/security/delete-user.asciidoc new file mode 100644 index 00000000000..52573bb29c7 --- /dev/null +++ b/docs/java-rest/high-level/security/delete-user.asciidoc @@ -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[] \ No newline at end of file diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 096a9e59382..bbb84434cbb 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -373,6 +373,7 @@ include::rollup/get_rollup_index_caps.asciidoc[] The Java High Level REST Client supports the following Security APIs: * <> +* <<{upid}-delete-user>> * <> * <> * <> @@ -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[]