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 a033ee61f79dc..811b9c9053ee8 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 @@ -52,6 +52,8 @@ import org.elasticsearch.client.security.HasPrivilegesResponse; import org.elasticsearch.client.security.InvalidateTokenRequest; import org.elasticsearch.client.security.InvalidateTokenResponse; +import org.elasticsearch.client.security.PutPrivilegesRequest; +import org.elasticsearch.client.security.PutPrivilegesResponse; import org.elasticsearch.client.security.PutRoleMappingRequest; import org.elasticsearch.client.security.PutRoleMappingResponse; import org.elasticsearch.client.security.PutUserRequest; @@ -603,6 +605,38 @@ public void getPrivilegesAsync(final GetPrivilegesRequest request, final Request options, GetPrivilegesResponse::fromXContent, listener, emptySet()); } + /** + * Create or update application privileges. + * See + * the docs for more. + * + * @param request the request to create or update application privileges + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the response from the create or update application privileges call + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public PutPrivilegesResponse putPrivileges(final PutPrivilegesRequest request, final RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::putPrivileges, options, + PutPrivilegesResponse::fromXContent, emptySet()); + } + + /** + * Asynchronously create or update application privileges.
+ * See + * the docs for more. + * + * @param request the request to create or update application privileges + * @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 putPrivilegesAsync(final PutPrivilegesRequest request, final RequestOptions options, + final ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::putPrivileges, options, + PutPrivilegesResponse::fromXContent, listener, emptySet()); + } + /** * Removes application privilege(s) * 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 6485899acf947..d893c20c3b2fd 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 @@ -28,17 +28,18 @@ import org.elasticsearch.client.security.ClearRolesCacheRequest; import org.elasticsearch.client.security.CreateTokenRequest; 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; -import org.elasticsearch.client.security.HasPrivilegesRequest; import org.elasticsearch.client.security.DisableUserRequest; import org.elasticsearch.client.security.EnableUserRequest; +import org.elasticsearch.client.security.GetPrivilegesRequest; import org.elasticsearch.client.security.GetRoleMappingsRequest; +import org.elasticsearch.client.security.GetRolesRequest; +import org.elasticsearch.client.security.HasPrivilegesRequest; +import org.elasticsearch.client.security.InvalidateTokenRequest; +import org.elasticsearch.client.security.PutPrivilegesRequest; +import org.elasticsearch.client.security.PutRoleMappingRequest; import org.elasticsearch.client.security.PutUserRequest; import org.elasticsearch.client.security.SetUserEnabledRequest; import org.elasticsearch.common.Strings; @@ -213,6 +214,14 @@ static Request getPrivileges(GetPrivilegesRequest getPrivilegesRequest) { return new Request(HttpGet.METHOD_NAME, endpoint); } + static Request putPrivileges(final PutPrivilegesRequest putPrivilegesRequest) throws IOException { + Request request = new Request(HttpPut.METHOD_NAME, "/_xpack/security/privilege"); + request.setEntity(createEntity(putPrivilegesRequest, REQUEST_BODY_CONTENT_TYPE)); + RequestConverters.Params params = new RequestConverters.Params(request); + params.withRefreshPolicy(putPrivilegesRequest.getRefreshPolicy()); + return request; + } + static Request deletePrivileges(DeletePrivilegesRequest deletePrivilegeRequest) { String endpoint = new RequestConverters.EndpointBuilder() .addPathPartAsIs("_xpack/security/privilege") diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutPrivilegesRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutPrivilegesRequest.java new file mode 100644 index 0000000000000..3e20405046a0c --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutPrivilegesRequest.java @@ -0,0 +1,98 @@ +/* + * 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.client.security.user.privileges.ApplicationPrivilege; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.TreeMap; +import java.util.stream.Collectors; + +/** + * Request object for creating/updating application privileges. + */ +public final class PutPrivilegesRequest implements Validatable, ToXContentObject { + + private final Map> privileges; + private final RefreshPolicy refreshPolicy; + + public PutPrivilegesRequest(final List privileges, @Nullable final RefreshPolicy refreshPolicy) { + if (privileges == null || privileges.isEmpty()) { + throw new IllegalArgumentException("privileges are required"); + } + this.privileges = Collections.unmodifiableMap(privileges.stream() + .collect(Collectors.groupingBy(ApplicationPrivilege::getApplication, TreeMap::new, Collectors.toList()))); + this.refreshPolicy = refreshPolicy == null ? RefreshPolicy.IMMEDIATE : refreshPolicy; + } + + /** + * @return a map of application name to list of + * {@link ApplicationPrivilege}s + */ + public Map> getPrivileges() { + return privileges; + } + + public RefreshPolicy getRefreshPolicy() { + return refreshPolicy; + } + + @Override + public int hashCode() { + return Objects.hash(privileges, refreshPolicy); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || (this.getClass() != o.getClass())) { + return false; + } + final PutPrivilegesRequest that = (PutPrivilegesRequest) o; + return privileges.equals(that.privileges) && (refreshPolicy == that.refreshPolicy); + } + + @Override + public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { + builder.startObject(); + for (Entry> entry : privileges.entrySet()) { + builder.field(entry.getKey()); + builder.startObject(); + for (ApplicationPrivilege applicationPrivilege : entry.getValue()) { + builder.field(applicationPrivilege.getName()); + applicationPrivilege.toXContent(builder, params); + } + builder.endObject(); + } + return builder.endObject(); + } + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutPrivilegesResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutPrivilegesResponse.java new file mode 100644 index 0000000000000..fadf6d155dc69 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutPrivilegesResponse.java @@ -0,0 +1,105 @@ +/* + * 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.ParsingException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Response when creating/updating one or more application privileges to the + * security index. + */ +public final class PutPrivilegesResponse { + + /* + * Map of application name to a map of privilege name to boolean denoting + * created or update status. + */ + private final Map> applicationPrivilegesCreatedOrUpdated; + + public PutPrivilegesResponse(final Map> applicationPrivilegesCreatedOrUpdated) { + this.applicationPrivilegesCreatedOrUpdated = Collections.unmodifiableMap(applicationPrivilegesCreatedOrUpdated); + } + + /** + * Get response status for the request to create or update application + * privileges. + * + * @param applicationName application name as specified in the request + * @param privilegeName privilege name as specified in the request + * @return {@code true} if the privilege was created, {@code false} if the + * privilege was updated + * @throws IllegalArgumentException thrown for unknown application name or + * privilege name. + */ + public boolean wasCreated(final String applicationName, final String privilegeName) { + if (Strings.hasText(applicationName) == false) { + throw new IllegalArgumentException("application name is required"); + } + if (Strings.hasText(privilegeName) == false) { + throw new IllegalArgumentException("privilege name is required"); + } + if (applicationPrivilegesCreatedOrUpdated.get(applicationName) == null + || applicationPrivilegesCreatedOrUpdated.get(applicationName).get(privilegeName) == null) { + throw new IllegalArgumentException("application name or privilege name not found in the response"); + } + return applicationPrivilegesCreatedOrUpdated.get(applicationName).get(privilegeName); + } + + @SuppressWarnings("unchecked") + public static PutPrivilegesResponse fromXContent(final XContentParser parser) throws IOException { + final Map> applicationPrivilegesCreatedOrUpdated = new HashMap<>(); + XContentParser.Token token = parser.currentToken(); + if (token == null) { + token = parser.nextToken(); + } + final Map appNameToPrivStatus = parser.map(); + for (Entry entry : appNameToPrivStatus.entrySet()) { + if (entry.getValue() instanceof Map) { + final Map privilegeToStatus = applicationPrivilegesCreatedOrUpdated.computeIfAbsent(entry.getKey(), + (a) -> new HashMap<>()); + final Map createdOrUpdated = (Map) entry.getValue(); + for (String privilegeName : createdOrUpdated.keySet()) { + if (createdOrUpdated.get(privilegeName) instanceof Map) { + final Map statusMap = (Map) createdOrUpdated.get(privilegeName); + final Object status = statusMap.get("created"); + if (status instanceof Boolean) { + privilegeToStatus.put(privilegeName, (Boolean) status); + } else { + throw new ParsingException(parser.getTokenLocation(), "Failed to parse object, unexpected structure"); + } + } else { + throw new ParsingException(parser.getTokenLocation(), "Failed to parse object, unexpected structure"); + } + } + } else { + throw new ParsingException(parser.getTokenLocation(), "Failed to parse object, unexpected structure"); + } + } + return new PutPrivilegesResponse(applicationPrivilegesCreatedOrUpdated); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/ApplicationPrivilege.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/ApplicationPrivilege.java index 1f5a4f08191cc..fc21dfc0b2816 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/ApplicationPrivilege.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/ApplicationPrivilege.java @@ -24,6 +24,8 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; @@ -44,7 +46,7 @@ * actions and metadata are completely managed by the client and can contain arbitrary * string values. */ -public final class ApplicationPrivilege { +public final class ApplicationPrivilege implements ToXContentObject { private static final ParseField APPLICATION = new ParseField("application"); private static final ParseField NAME = new ParseField("name"); @@ -171,4 +173,16 @@ public ApplicationPrivilege build() { } } + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject() + .field(APPLICATION.getPreferredName(), application) + .field(NAME.getPreferredName(), name) + .field(ACTIONS.getPreferredName(), actions); + if (metadata != null && metadata.isEmpty() == false) { + builder.field(METADATA.getPreferredName(), metadata); + } + return builder.endObject(); + } + } 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 110e0cc56c986..f5015c1203dea 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 @@ -19,10 +19,11 @@ 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.CreateTokenRequest; import org.elasticsearch.client.security.DeletePrivilegesRequest; import org.elasticsearch.client.security.DeleteRoleMappingRequest; @@ -32,8 +33,8 @@ import org.elasticsearch.client.security.EnableUserRequest; import org.elasticsearch.client.security.GetPrivilegesRequest; import org.elasticsearch.client.security.GetRoleMappingsRequest; -import org.elasticsearch.client.security.ChangePasswordRequest; import org.elasticsearch.client.security.GetRolesRequest; +import org.elasticsearch.client.security.PutPrivilegesRequest; import org.elasticsearch.client.security.PutRoleMappingRequest; import org.elasticsearch.client.security.PutUserRequest; import org.elasticsearch.client.security.RefreshPolicy; @@ -41,10 +42,13 @@ 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.user.privileges.ApplicationPrivilege; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.test.ESTestCase; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -318,6 +322,27 @@ public void testGetAllPrivileges() throws Exception { assertNull(request.getEntity()); } + public void testPutPrivileges() throws Exception { + int noOfApplicationPrivileges = randomIntBetween(2, 4); + final List privileges = new ArrayList<>(); + for (int count = 0; count < noOfApplicationPrivileges; count++) { + privileges.add(ApplicationPrivilege.builder() + .application(randomAlphaOfLength(4)) + .privilege(randomAlphaOfLengthBetween(3, 5)) + .actions(Sets.newHashSet(generateRandomStringArray(3, 5, false, false))) + .metadata(Collections.singletonMap("k1", "v1")) + .build()); + } + final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); + final Map expectedParams = getExpectedParamsFromRefreshPolicy(refreshPolicy); + final PutPrivilegesRequest putPrivilegesRequest = new PutPrivilegesRequest(privileges, refreshPolicy); + final Request request = SecurityRequestConverters.putPrivileges(putPrivilegesRequest); + assertEquals(HttpPut.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/security/privilege", request.getEndpoint()); + assertEquals(expectedParams, request.getParameters()); + assertToXContentBody(putPrivilegesRequest, request.getEntity()); + } + public void testDeletePrivileges() { final String application = randomAlphaOfLengthBetween(1, 12); final List privileges = randomSubsetOf(randomIntBetween(1, 3), "read", "write", "all"); 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 79258b314510c..480f7abb131e1 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 @@ -29,7 +29,6 @@ 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; @@ -62,6 +61,8 @@ import org.elasticsearch.client.security.HasPrivilegesResponse; import org.elasticsearch.client.security.InvalidateTokenRequest; import org.elasticsearch.client.security.InvalidateTokenResponse; +import org.elasticsearch.client.security.PutPrivilegesRequest; +import org.elasticsearch.client.security.PutPrivilegesResponse; import org.elasticsearch.client.security.PutRoleMappingRequest; import org.elasticsearch.client.security.PutRoleMappingResponse; import org.elasticsearch.client.security.PutUserRequest; @@ -78,7 +79,6 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.rest.RestStatus; import org.hamcrest.Matchers; import javax.crypto.SecretKeyFactory; @@ -86,6 +86,7 @@ import java.io.IOException; import java.security.SecureRandom; import java.util.Base64; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -1205,36 +1206,23 @@ public void testGetPrivileges() throws Exception { new ApplicationPrivilege("testapp2", "all", Arrays.asList("action:login", "data:write/*", "manage:*"), null); { - //TODO Replace this with a call to PutPrivileges once it is implemented - final Request createPrivilegeRequest = new Request("POST", "/_xpack/security/privilege"); - createPrivilegeRequest.setJsonEntity("{" + - " \"testapp\": {" + - " \"read\": {" + - " \"actions\": [ \"action:login\", \"data:read/*\" ]" + - " }," + - " \"write\": {" + - " \"actions\": [ \"action:login\", \"data:write/*\" ]," + - " \"metadata\": { \"key1\": \"value1\" }" + - " }," + - " \"all\": {" + - " \"actions\": [ \"action:login\", \"data:write/*\" , \"manage:*\"]" + - " }" + - " }," + - " \"testapp2\": {" + - " \"read\": {" + - " \"actions\": [ \"action:login\", \"data:read/*\" ]," + - " \"metadata\": { \"key2\": \"value2\" }" + - " }," + - " \"write\": {" + - " \"actions\": [ \"action:login\", \"data:write/*\" ]" + - " }," + - " \"all\": {" + - " \"actions\": [ \"action:login\", \"data:write/*\" , \"manage:*\"]" + - " }" + - " }" + - "}"); - final Response createPrivilegeResponse = client.getLowLevelClient().performRequest(createPrivilegeRequest); - assertEquals(RestStatus.OK.getStatus(), createPrivilegeResponse.getStatusLine().getStatusCode()); + List applicationPrivileges = new ArrayList<>(); + applicationPrivileges.add(readTestappPrivilege); + applicationPrivileges.add(writeTestappPrivilege); + applicationPrivileges.add(allTestappPrivilege); + applicationPrivileges.add(readTestapp2Privilege); + applicationPrivileges.add(writeTestapp2Privilege); + applicationPrivileges.add(allTestapp2Privilege); + PutPrivilegesRequest putPrivilegesRequest = new PutPrivilegesRequest(applicationPrivileges, RefreshPolicy.IMMEDIATE); + PutPrivilegesResponse putPrivilegesResponse = client.security().putPrivileges(putPrivilegesRequest, RequestOptions.DEFAULT); + + assertNotNull(putPrivilegesResponse); + assertThat(putPrivilegesResponse.wasCreated("testapp", "write"), is(true)); + assertThat(putPrivilegesResponse.wasCreated("testapp", "read"), is(true)); + assertThat(putPrivilegesResponse.wasCreated("testapp", "all"), is(true)); + assertThat(putPrivilegesResponse.wasCreated("testapp2", "all"), is(true)); + assertThat(putPrivilegesResponse.wasCreated("testapp2", "write"), is(true)); + assertThat(putPrivilegesResponse.wasCreated("testapp2", "read"), is(true)); } { @@ -1327,26 +1315,105 @@ public void onFailure(Exception e) { } } + public void testPutPrivileges() throws Exception { + RestHighLevelClient client = highLevelClient(); + + { + // tag::put-privileges-request + final List privileges = new ArrayList<>(); + privileges.add(ApplicationPrivilege.builder() + .application("app01") + .privilege("all") + .actions(Sets.newHashSet("action:login")) + .metadata(Collections.singletonMap("k1", "v1")) + .build()); + privileges.add(ApplicationPrivilege.builder() + .application("app01") + .privilege("write") + .actions(Sets.newHashSet("action:write")) + .build()); + final PutPrivilegesRequest putPrivilegesRequest = new PutPrivilegesRequest(privileges, RefreshPolicy.IMMEDIATE); + // end::put-privileges-request + + // tag::put-privileges-execute + final PutPrivilegesResponse putPrivilegesResponse = client.security().putPrivileges(putPrivilegesRequest, + RequestOptions.DEFAULT); + // end::put-privileges-execute + + final String applicationName = "app01"; + final String privilegeName = "all"; + // tag::put-privileges-response + final boolean status = putPrivilegesResponse.wasCreated(applicationName, privilegeName); // <1> + // end::put-privileges-response + assertThat(status, is(true)); + } + + { + final List privileges = new ArrayList<>(); + privileges.add(ApplicationPrivilege.builder() + .application("app01") + .privilege("all") + .actions(Sets.newHashSet("action:login")) + .metadata(Collections.singletonMap("k1", "v1")) + .build()); + final PutPrivilegesRequest putPrivilegesRequest = new PutPrivilegesRequest(privileges, RefreshPolicy.IMMEDIATE); + + // tag::put-privileges-execute-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(PutPrivilegesResponse response) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::put-privileges-execute-listener + + // Avoid unused variable warning + assertNotNull(listener); + + // Replace the empty listener by a blocking listener in test + final PlainActionFuture future = new PlainActionFuture<>(); + listener = future; + + //tag::put-privileges-execute-async + client.security().putPrivilegesAsync(putPrivilegesRequest, RequestOptions.DEFAULT, listener); // <1> + //end::put-privileges-execute-async + + assertNotNull(future.get(30, TimeUnit.SECONDS)); + assertThat(future.get().wasCreated("app01", "all"), is(false)); + } + } + 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()); + List applicationPrivileges = new ArrayList<>(); + applicationPrivileges.add(ApplicationPrivilege.builder() + .application("testapp") + .privilege("read") + .actions("action:login", "data:read/*") + .build()); + applicationPrivileges.add(ApplicationPrivilege.builder() + .application("testapp") + .privilege("write") + .actions("action:login", "data:write/*") + .build()); + applicationPrivileges.add(ApplicationPrivilege.builder() + .application("testapp") + .privilege("all") + .actions("action:login", "data:write/*") + .build()); + PutPrivilegesRequest putPrivilegesRequest = new PutPrivilegesRequest(applicationPrivileges, RefreshPolicy.IMMEDIATE); + PutPrivilegesResponse putPrivilegesResponse = client.security().putPrivileges(putPrivilegesRequest, RequestOptions.DEFAULT); + + assertNotNull(putPrivilegesResponse); + assertThat(putPrivilegesResponse.wasCreated("testapp", "write"), is(true)); + assertThat(putPrivilegesResponse.wasCreated("testapp", "read"), is(true)); + assertThat(putPrivilegesResponse.wasCreated("testapp", "all"), is(true)); } { // tag::delete-privileges-request diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/PutPrivilegesRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/PutPrivilegesRequestTests.java new file mode 100644 index 0000000000000..8c60382eeec38 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/PutPrivilegesRequestTests.java @@ -0,0 +1,170 @@ +/* + * 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.security.user.privileges.ApplicationPrivilege; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.equalTo; + +public class PutPrivilegesRequestTests extends ESTestCase { + + public void testConstructor() { + final List privileges = randomFrom( + Arrays.asList(Collections.singletonList(ApplicationPrivilege.builder() + .application("app01") + .privilege("all") + .actions(Sets.newHashSet("action:login", "action:logout")) + .metadata(Collections.singletonMap("k1", "v1")) + .build()), + null, Collections.emptyList())); + final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); + if (privileges == null || privileges.isEmpty()) { + final IllegalArgumentException ile = expectThrows(IllegalArgumentException.class, + () -> new PutPrivilegesRequest(privileges, refreshPolicy)); + assertThat(ile.getMessage(), equalTo("privileges are required")); + } else { + final PutPrivilegesRequest putPrivilegesRequest = new PutPrivilegesRequest(privileges, refreshPolicy); + assertThat(putPrivilegesRequest.getPrivileges().values().stream().flatMap(List::stream).collect(Collectors.toList()), + equalTo(privileges)); + assertThat(putPrivilegesRequest.getRefreshPolicy(), equalTo(refreshPolicy)); + } + } + + public void testToXContent() throws IOException { + final String expected = "{\n" + + " \"app01\" : {\n" + + " \"all\" : {\n" + + " \"application\" : \"app01\",\n" + + " \"name\" : \"all\",\n" + + " \"actions\" : [\n" + + " \"action:logout\",\n" + + " \"action:login\"\n" + + " ],\n" + + " \"metadata\" : {\n" + + " \"k1\" : \"v1\"\n" + + " }\n" + + " },\n" + + " \"read\" : {\n" + + " \"application\" : \"app01\",\n" + + " \"name\" : \"read\",\n" + + " \"actions\" : [\n" + + " \"data:read\"\n" + + " ]\n" + " }\n" + + " },\n" + + " \"app02\" : {\n" + + " \"all\" : {\n" + + " \"application\" : \"app02\",\n" + + " \"name\" : \"all\",\n" + + " \"actions\" : [\n" + + " \"action:logout\",\n" + + " \"action:login\"\n" + + " ],\n" + + " \"metadata\" : {\n" + + " \"k2\" : \"v2\"\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + List privileges = new ArrayList<>(); + privileges.add(ApplicationPrivilege.builder() + .application("app01") + .privilege("all") + .actions(Sets.newHashSet("action:login", "action:logout")) + .metadata(Collections.singletonMap("k1", "v1")) + .build()); + privileges.add(ApplicationPrivilege.builder() + .application("app01") + .privilege("read") + .actions(Sets.newHashSet("data:read")) + .build()); + privileges.add(ApplicationPrivilege.builder() + .application("app02") + .privilege("all") + .actions(Sets.newHashSet("action:login", "action:logout")) + .metadata(Collections.singletonMap("k2", "v2")) + .build()); + final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); + final PutPrivilegesRequest putPrivilegesRequest = new PutPrivilegesRequest(privileges, refreshPolicy); + final XContentBuilder builder = XContentFactory.jsonBuilder().prettyPrint(); + assertThat(Strings.toString(putPrivilegesRequest.toXContent(builder, ToXContent.EMPTY_PARAMS)), equalTo(expected)); + } + + public void testEqualsHashCode() { + final List privileges = new ArrayList<>(); + privileges.add(ApplicationPrivilege.builder() + .application(randomAlphaOfLength(5)) + .privilege(randomAlphaOfLength(3)) + .actions(Sets.newHashSet(randomAlphaOfLength(5), randomAlphaOfLength(5))) + .metadata(Collections.singletonMap(randomAlphaOfLength(3), randomAlphaOfLength(3))) + .build()); + privileges.add(ApplicationPrivilege.builder() + .application(randomAlphaOfLength(5)) + .privilege(randomAlphaOfLength(3)) + .actions(Sets.newHashSet(randomAlphaOfLength(5), randomAlphaOfLength(5))) + .metadata(Collections.singletonMap(randomAlphaOfLength(3), randomAlphaOfLength(3))) + .build()); + final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); + PutPrivilegesRequest putPrivilegesRequest = new PutPrivilegesRequest(privileges, refreshPolicy); + + EqualsHashCodeTestUtils.checkEqualsAndHashCode(putPrivilegesRequest, (original) -> { + return new PutPrivilegesRequest(privileges, refreshPolicy); + }); + EqualsHashCodeTestUtils.checkEqualsAndHashCode(putPrivilegesRequest, (original) -> { + return new PutPrivilegesRequest(original.getPrivileges().values().stream().flatMap(List::stream).collect(Collectors.toList()), + original.getRefreshPolicy()); + }, PutPrivilegesRequestTests::mutateTestItem); + } + + private static PutPrivilegesRequest mutateTestItem(PutPrivilegesRequest original) { + final Set policies = Sets.newHashSet(RefreshPolicy.values()); + policies.remove(original.getRefreshPolicy()); + switch (randomIntBetween(0, 1)) { + case 0: + final List privileges = new ArrayList<>(); + privileges.add(ApplicationPrivilege.builder() + .application(randomAlphaOfLength(5)) + .privilege(randomAlphaOfLength(3)) + .actions(Sets.newHashSet(randomAlphaOfLength(6))) + .build()); + return new PutPrivilegesRequest(privileges, original.getRefreshPolicy()); + case 1: + return new PutPrivilegesRequest(original.getPrivileges().values().stream().flatMap(List::stream).collect(Collectors.toList()), + randomFrom(policies)); + default: + return new PutPrivilegesRequest(original.getPrivileges().values().stream().flatMap(List::stream).collect(Collectors.toList()), + randomFrom(policies)); + } + } +} \ No newline at end of file diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/PutPrivilegesResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/PutPrivilegesResponseTests.java new file mode 100644 index 0000000000000..9a39f456986e5 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/PutPrivilegesResponseTests.java @@ -0,0 +1,86 @@ +/* + * 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.XContentType; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +public class PutPrivilegesResponseTests extends ESTestCase { + + public void testFromXContent() throws IOException { + final String json = "{\n" + + " \"app02\": {\n" + + " \"all\": {\n" + + " \"created\": true\n" + + " }\n" + + " },\n" + + " \"app01\": {\n" + + " \"read\": {\n" + + " \"created\": false\n" + + " },\n" + + " \"write\": {\n" + + " \"created\": true\n" + + " }\n" + + " }\n" + + "}"; + + final PutPrivilegesResponse putPrivilegesResponse = PutPrivilegesResponse + .fromXContent(createParser(XContentType.JSON.xContent(), json)); + + assertThat(putPrivilegesResponse.wasCreated("app02", "all"), is(true)); + assertThat(putPrivilegesResponse.wasCreated("app01", "read"), is(false)); + assertThat(putPrivilegesResponse.wasCreated("app01", "write"), is(true)); + expectThrows(IllegalArgumentException.class, () -> putPrivilegesResponse.wasCreated("unknown-app", "unknown-priv")); + expectThrows(IllegalArgumentException.class, () -> putPrivilegesResponse.wasCreated("app01", "unknown-priv")); + } + + public void testGetStatusFailsForUnknownApplicationOrPrivilegeName() { + final PutPrivilegesResponse putPrivilegesResponse = new PutPrivilegesResponse( + Collections.singletonMap("app-1", Collections.singletonMap("priv", true))); + + final boolean invalidAppName = randomBoolean(); + final String applicationName = (invalidAppName) ? randomAlphaOfLength(4) : "app-1"; + final String privilegeName = randomAlphaOfLength(4); + + final IllegalArgumentException ile = expectThrows(IllegalArgumentException.class, + () -> putPrivilegesResponse.wasCreated(applicationName, privilegeName)); + assertThat(ile.getMessage(), equalTo("application name or privilege name not found in the response")); + } + + public void testGetStatusFailsForNullOrEmptyApplicationOrPrivilegeName() { + final PutPrivilegesResponse putPrivilegesResponse = new PutPrivilegesResponse( + Collections.singletonMap("app-1", Collections.singletonMap("priv", true))); + + final boolean nullOrEmptyAppName = randomBoolean(); + final String applicationName = (nullOrEmptyAppName) ? randomFrom(Arrays.asList("", " ", null)) : "app-1"; + final String privilegeName = randomFrom(Arrays.asList("", " ", null)); + final IllegalArgumentException ile = expectThrows(IllegalArgumentException.class, + () -> putPrivilegesResponse.wasCreated(applicationName, privilegeName)); + assertThat(ile.getMessage(), + (nullOrEmptyAppName ? equalTo("application name is required") : equalTo("privilege name is required"))); + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/user/privileges/ApplicationPrivilegeTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/user/privileges/ApplicationPrivilegeTests.java index f958cadaa7e80..b720187673023 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/user/privileges/ApplicationPrivilegeTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/user/privileges/ApplicationPrivilegeTests.java @@ -19,8 +19,12 @@ package org.elasticsearch.client.security.user.privileges; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.DeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.test.ESTestCase; @@ -36,19 +40,19 @@ public class ApplicationPrivilegeTests extends ESTestCase { - public void testFromXContent() throws IOException { + public void testFromXContentAndToXContent() throws IOException { String json = - " {" + - " \"application\": \"myapp\"," + - " \"name\": \"read\"," + - " \"actions\": [" + - " \"data:read/*\"," + - " \"action:login\"" + - " ],\n" + - " \"metadata\": {" + - " \"description\": \"Read access to myapp\"" + - " }" + - " }"; + "{\n" + + " \"application\" : \"myapp\",\n" + + " \"name\" : \"read\",\n" + + " \"actions\" : [\n" + + " \"data:read/*\",\n" + + " \"action:login\"\n" + + " ],\n" + + " \"metadata\" : {\n" + + " \"description\" : \"Read access to myapp\"\n" + + " }\n" + + "}"; final ApplicationPrivilege privilege = ApplicationPrivilege.fromXContent(XContentType.JSON.xContent().createParser( new NamedXContentRegistry(Collections.emptyList()), new DeprecationHandler() { @Override @@ -64,6 +68,10 @@ public void usedDeprecatedField(String usedName, String replacedWith) { final ApplicationPrivilege expectedPrivilege = new ApplicationPrivilege("myapp", "read", Arrays.asList("data:read/*", "action:login"), metadata); assertThat(privilege, equalTo(expectedPrivilege)); + + XContentBuilder builder = privilege.toXContent(XContentFactory.jsonBuilder().prettyPrint(), ToXContent.EMPTY_PARAMS); + String toJson = Strings.toString(builder); + assertThat(toJson, equalTo(json)); } public void testEmptyApplicationName() { diff --git a/docs/java-rest/high-level/security/put-privileges.asciidoc b/docs/java-rest/high-level/security/put-privileges.asciidoc new file mode 100644 index 0000000000000..1c0a97d2a94c3 --- /dev/null +++ b/docs/java-rest/high-level/security/put-privileges.asciidoc @@ -0,0 +1,39 @@ +-- +:api: put-privileges +:request: PutPrivilegesRequest +:response: PutPrivilegesResponse +-- + +[id="{upid}-{api}"] +=== Put Privileges API + +Application privileges can be created or updated using this API. + +[id="{upid}-{api}-request"] +==== Put Privileges Request +A +{request}+ contains list of application privileges that +need to be created or updated. Each application privilege +consists of an application name, application privilege, +set of actions and optional metadata. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request] +-------------------------------------------------- + +include::../execution.asciidoc[] + +[id="{upid}-{api}-response"] +==== Put Privileges Response + +The returned +{response}+ contains the information about the status +for each privilege present in the +{request}+. The status would be +`true` if the privilege was created, `false` if the privilege was updated. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response] +-------------------------------------------------- +<1> The response contains the status for given application name and +privilege name. The status would be `true` if the privilege was created, +`false` if the privilege was updated. diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index eb2f0b9818172..9fa7927f96937 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -390,6 +390,7 @@ The Java High Level REST Client supports the following Security APIs: * <> * <<{upid}-invalidate-token>> * <<{upid}-get-privileges>> +* <<{upid}-put-privileges>> * <<{upid}-delete-privileges>> include::security/put-user.asciidoc[] @@ -411,6 +412,7 @@ include::security/get-role-mappings.asciidoc[] include::security/delete-role-mapping.asciidoc[] include::security/create-token.asciidoc[] include::security/invalidate-token.asciidoc[] +include::security/put-privileges.asciidoc[] == Watcher APIs