Pārlūkot izejas kodu

HLRC: Add delete user action (#35294)

* HLRC: Add delete user action

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

Relates #29827
Ignacio Vera 6 gadi atpakaļ
vecāks
revīzija
93ed8b7d61

+ 29 - 0
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 <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">

+ 12 - 0
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")

+ 71 - 0
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);
+    }
+}

+ 50 - 0
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<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;
+    }
+}

+ 13 - 3
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() {

+ 13 - 0
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<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);

+ 63 - 0
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<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();
 

+ 1 - 0
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;

+ 78 - 0
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<RefreshPolicy> values = Arrays.stream(RefreshPolicy.values()).filter(rp -> rp != original.getRefreshPolicy()).collect(
+                    Collectors.toList());
+            return new DeleteUserRequest(original.getName(), randomFrom(values));
+        }
+    }
+}

+ 43 - 0
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));
+    }
+}

+ 32 - 0
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[]

+ 2 - 0
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:
 
 * <<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[]