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 d3b38aaf9e9d2..68bb9b9a28b99 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 @@ -29,6 +29,8 @@ import org.elasticsearch.client.security.ClearRolesCacheResponse; import org.elasticsearch.client.security.CreateTokenRequest; import org.elasticsearch.client.security.CreateTokenResponse; +import org.elasticsearch.client.security.DeletePrivilegesRequest; +import org.elasticsearch.client.security.DeletePrivilegesResponse; import org.elasticsearch.client.security.DeleteRoleMappingRequest; import org.elasticsearch.client.security.DeleteRoleMappingResponse; import org.elasticsearch.client.security.DeleteRoleRequest; @@ -221,7 +223,7 @@ public void disableUserAsync(DisableUserRequest request, RequestOptions options, * See * the docs for more. * - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return the responsee from the authenticate user call */ public AuthenticateResponse authenticate(RequestOptions options) throws IOException { @@ -234,8 +236,8 @@ public AuthenticateResponse authenticate(RequestOptions options) throws IOExcept * See * the docs for more. * - * @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 + * @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 authenticateAsync(RequestOptions options, ActionListener listener) { restHighLevelClient.performRequestAsyncAndParseEntity(AuthenticateRequest.INSTANCE, AuthenticateRequest::getRequest, options, @@ -473,4 +475,32 @@ public void invalidateTokenAsync(InvalidateTokenRequest request, RequestOptions InvalidateTokenResponse::fromXContent, listener, emptySet()); } + /** + * Removes application privilege(s) + * See + * the docs for more. + * @param request the request with the application privilege 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 application privilege call + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public DeletePrivilegesResponse deletePrivileges(DeletePrivilegesRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::deletePrivileges, options, + DeletePrivilegesResponse::fromXContent, singleton(404)); + } + + /** + * Asynchronously removes an application privilege + * See + * the docs for more. + * @param request the request with the application privilege 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 deletePrivilegesAsync(DeletePrivilegesRequest request, RequestOptions options, + ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::deletePrivileges, options, + DeletePrivilegesResponse::fromXContent, listener, singleton(404)); + } + } 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 5958a763eeebc..160aa1fd82b0a 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 @@ -19,21 +19,22 @@ package org.elasticsearch.client; -import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; +import org.elasticsearch.client.security.ChangePasswordRequest; import org.elasticsearch.client.security.ClearRealmCacheRequest; import org.elasticsearch.client.security.ClearRolesCacheRequest; 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.InvalidateTokenRequest; -import org.elasticsearch.client.security.PutRoleMappingRequest; import org.elasticsearch.client.security.DisableUserRequest; import org.elasticsearch.client.security.EnableUserRequest; import org.elasticsearch.client.security.GetRoleMappingsRequest; -import org.elasticsearch.client.security.ChangePasswordRequest; +import org.elasticsearch.client.security.InvalidateTokenRequest; +import org.elasticsearch.client.security.PutRoleMappingRequest; import org.elasticsearch.client.security.PutUserRequest; import org.elasticsearch.client.security.SetUserEnabledRequest; import org.elasticsearch.common.Strings; @@ -172,4 +173,16 @@ static Request invalidateToken(InvalidateTokenRequest invalidateTokenRequest) th request.setEntity(createEntity(invalidateTokenRequest, REQUEST_BODY_CONTENT_TYPE)); return request; } + + static Request deletePrivileges(DeletePrivilegesRequest deletePrivilegeRequest) { + String endpoint = new RequestConverters.EndpointBuilder() + .addPathPartAsIs("_xpack/security/privilege") + .addPathPart(deletePrivilegeRequest.getApplication()) + .addCommaSeparatedPathParts(deletePrivilegeRequest.getPrivileges()) + .build(); + Request request = new Request(HttpDelete.METHOD_NAME, endpoint); + RequestConverters.Params params = new RequestConverters.Params(request); + params.withRefreshPolicy(deletePrivilegeRequest.getRefreshPolicy()); + return request; + } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeletePrivilegesRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeletePrivilegesRequest.java new file mode 100644 index 0000000000000..7ea416fc339c3 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeletePrivilegesRequest.java @@ -0,0 +1,76 @@ +/* + * 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.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.util.CollectionUtils; + +/** + * A request to delete application privileges + */ +public final class DeletePrivilegesRequest implements Validatable { + + private final String application; + private final String[] privileges; + private final RefreshPolicy refreshPolicy; + + /** + * Creates a new {@link DeletePrivilegesRequest} using the default {@link RefreshPolicy#getDefault()} refresh policy. + * + * @param application the name of the application for which the privileges will be deleted + * @param privileges the privileges to delete + */ + public DeletePrivilegesRequest(String application, String... privileges) { + this(application, privileges, null); + } + + /** + * Creates a new {@link DeletePrivilegesRequest}. + * + * @param application the name of the application for which the privileges will be deleted + * @param privileges the privileges to delete + * @param refreshPolicy the refresh policy {@link RefreshPolicy} for the request, defaults to {@link RefreshPolicy#getDefault()} + */ + public DeletePrivilegesRequest(String application, String[] privileges, @Nullable RefreshPolicy refreshPolicy) { + if (Strings.hasText(application) == false) { + throw new IllegalArgumentException("application name is required"); + } + if (CollectionUtils.isEmpty(privileges)) { + throw new IllegalArgumentException("privileges are required"); + } + this.application = application; + this.privileges = privileges; + this.refreshPolicy = (refreshPolicy == null) ? RefreshPolicy.getDefault() : refreshPolicy; + } + + public String getApplication() { + return application; + } + + public String[] getPrivileges() { + return privileges; + } + + public RefreshPolicy getRefreshPolicy() { + return refreshPolicy; + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeletePrivilegesResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeletePrivilegesResponse.java new file mode 100644 index 0000000000000..fd6e30df10544 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeletePrivilegesResponse.java @@ -0,0 +1,91 @@ +/* + * 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.xcontent.XContentParser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + +/** + * Response for application privileges deletion + */ +public final class DeletePrivilegesResponse { + + private final String application; + private final List privileges; + + DeletePrivilegesResponse(String application, List privileges) { + this.application = Objects.requireNonNull(application, "application is required"); + this.privileges = Objects.requireNonNull(privileges, "privileges are required"); + } + + public String getApplication() { + return application; + } + + /** + * Indicates if the given privilege was successfully found and deleted from the list of application privileges. + * + * @param privilege the privilege + * @return true if the privilege was found and deleted, false otherwise. + */ + public boolean isFound(final String privilege) { + return privileges.contains(privilege); + } + + public static DeletePrivilegesResponse fromXContent(XContentParser parser) throws IOException { + XContentParser.Token token = parser.currentToken(); + if (token == null) { + token = parser.nextToken(); + } + ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser::getTokenLocation); + token = parser.nextToken(); + ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation); + final String application = parser.currentName(); + final List foundAndDeletedPrivileges = new ArrayList<>(); + token = parser.nextToken(); + if (token == XContentParser.Token.START_OBJECT) { + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + String privilege = parser.currentName(); + token = parser.nextToken(); + if (token == XContentParser.Token.START_OBJECT) { + String currentFieldName = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.VALUE_BOOLEAN) { + if ("found".equals(currentFieldName) && parser.booleanValue()) { + foundAndDeletedPrivileges.add(privilege); + } + } + } + } + } + } + } + return new DeletePrivilegesResponse(application, foundAndDeletedPrivileges); + } +} 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 d2679906af207..199356de2902e 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 @@ -24,6 +24,7 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; 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.DisableUserRequest; @@ -241,4 +242,19 @@ public void testCreateTokenWithClientCredentialsGrant() throws Exception { assertEquals(0, request.getParameters().size()); assertToXContentBody(createTokenRequest, request.getEntity()); } + + public void testDeletePrivileges() { + final String application = randomAlphaOfLengthBetween(1, 12); + final List privileges = randomSubsetOf(randomIntBetween(1, 3), "read", "write", "all"); + final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); + final Map expectedParams = getExpectedParamsFromRefreshPolicy(refreshPolicy); + DeletePrivilegesRequest deletePrivilegesRequest = + new DeletePrivilegesRequest(application, privileges.toArray(Strings.EMPTY_ARRAY), refreshPolicy); + Request request = SecurityRequestConverters.deletePrivileges(deletePrivilegesRequest); + assertEquals(HttpDelete.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/security/privilege/" + application + "/" + Strings.collectionToCommaDelimitedString(privileges), + request.getEndpoint()); + assertEquals(expectedParams, request.getParameters()); + assertNull(request.getEntity()); + } } 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 ffa30e16c0468..71cfdd4ba5b89 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 @@ -28,6 +28,7 @@ import org.elasticsearch.client.ESRestHighLevelClientTestCase; import org.elasticsearch.client.Request; import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.Response; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.security.AuthenticateResponse; import org.elasticsearch.client.security.ChangePasswordRequest; @@ -37,6 +38,8 @@ import org.elasticsearch.client.security.ClearRolesCacheResponse; import org.elasticsearch.client.security.CreateTokenRequest; import org.elasticsearch.client.security.CreateTokenResponse; +import org.elasticsearch.client.security.DeletePrivilegesRequest; +import org.elasticsearch.client.security.DeletePrivilegesResponse; import org.elasticsearch.client.security.DeleteRoleMappingRequest; import org.elasticsearch.client.security.DeleteRoleMappingResponse; import org.elasticsearch.client.security.DeleteRoleRequest; @@ -55,13 +58,14 @@ import org.elasticsearch.client.security.PutUserRequest; import org.elasticsearch.client.security.PutUserResponse; import org.elasticsearch.client.security.RefreshPolicy; +import org.elasticsearch.client.security.support.CertificateInfo; import org.elasticsearch.client.security.support.expressiondsl.RoleMapperExpression; +import org.elasticsearch.client.security.support.expressiondsl.expressions.AnyRoleMapperExpression; import org.elasticsearch.client.security.support.expressiondsl.fields.FieldRoleMapperExpression; import org.elasticsearch.client.security.user.User; -import org.elasticsearch.client.security.support.CertificateInfo; -import org.elasticsearch.client.security.support.expressiondsl.expressions.AnyRoleMapperExpression; import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.rest.RestStatus; import org.hamcrest.Matchers; import java.io.IOException; @@ -916,4 +920,78 @@ public void onFailure(Exception e) { // See https://github.com/elastic/elasticsearch/issues/35115 } } + + public void testDeletePrivilege() throws Exception { + RestHighLevelClient client = highLevelClient(); + { + final Request createPrivilegeRequest = new Request("POST", "/_xpack/security/privilege"); + createPrivilegeRequest.setJsonEntity("{" + + " \"testapp\": {" + + " \"read\": {" + + " \"actions\": [ \"action:login\", \"data:read/*\" ]" + + " }," + + " \"write\": {" + + " \"actions\": [ \"action:login\", \"data:write/*\" ]" + + " }," + + " \"all\": {" + + " \"actions\": [ \"action:login\", \"data:write/*\" ]" + + " }" + + " }" + + "}"); + + final Response createPrivilegeResponse = client.getLowLevelClient().performRequest(createPrivilegeRequest); + assertEquals(RestStatus.OK.getStatus(), createPrivilegeResponse.getStatusLine().getStatusCode()); + } + { + // tag::delete-privileges-request + DeletePrivilegesRequest request = new DeletePrivilegesRequest( + "testapp", // <1> + "read", "write"); // <2> + // end::delete-privileges-request + + // tag::delete-privileges-execute + DeletePrivilegesResponse response = client.security().deletePrivileges(request, RequestOptions.DEFAULT); + // end::delete-privileges-execute + + // tag::delete-privileges-response + String application = response.getApplication(); // <1> + boolean found = response.isFound("read"); // <2> + // end::delete-privileges-response + assertThat(application, equalTo("testapp")); + assertTrue(response.isFound("write")); + assertTrue(found); + + // check if deleting the already deleted privileges again will give us a different response + response = client.security().deletePrivileges(request, RequestOptions.DEFAULT); + assertFalse(response.isFound("write")); + } + { + DeletePrivilegesRequest deletePrivilegesRequest = new DeletePrivilegesRequest("testapp", "all"); + + ActionListener listener; + //tag::delete-privileges-execute-listener + listener = new ActionListener() { + @Override + public void onResponse(DeletePrivilegesResponse deletePrivilegesResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + //end::delete-privileges-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-privileges-execute-async + client.security().deletePrivilegesAsync(deletePrivilegesRequest, RequestOptions.DEFAULT, listener); // <1> + //end::delete-privileges-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } } diff --git a/docs/java-rest/high-level/security/delete-privileges.asciidoc b/docs/java-rest/high-level/security/delete-privileges.asciidoc new file mode 100644 index 0000000000000..7f32d75107b97 --- /dev/null +++ b/docs/java-rest/high-level/security/delete-privileges.asciidoc @@ -0,0 +1,37 @@ +-- +:api: delete-privileges +:request: DeletePrivilegesRequest +:response: DeletePrivilegesResponse +-- + +[id="{upid}-{api}"] +=== Delete Privileges API + +This API can be used to delete application privileges. + +[id="{upid}-{api}-request"] +==== Delete Application Privileges Request + +A +{request}+ has two arguments + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request] +-------------------------------------------------- +<1> the name of application +<2> the name(s) of the privileges to delete that belong to the given application + +include::../execution.asciidoc[] + +[id="{upid}-{api}-response"] +==== Delete Application Privileges 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> the name of the application +<2> whether the given privilege was found and deleted diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 7deb214d29628..8ec6ac7a31164 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -351,12 +351,14 @@ The Java High Level REST Client supports the following Security APIs: * <> * <> * <<{upid}-invalidate-token>> +* <<{upid}-delete-privileges>> include::security/put-user.asciidoc[] include::security/enable-user.asciidoc[] include::security/disable-user.asciidoc[] include::security/change-password.asciidoc[] include::security/delete-role.asciidoc[] +include::security/delete-privileges.asciidoc[] include::security/clear-roles-cache.asciidoc[] include::security/clear-realm-cache.asciidoc[] include::security/authenticate.asciidoc[]