Browse Source

[HLRC] Add support for put privileges API (#35679)

This commit adds support for API to create or update
application privileges in high-level rest client.
Yogesh Gaikwad 6 years ago
parent
commit
32c4f99238

+ 34 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java

@@ -52,6 +52,8 @@ import org.elasticsearch.client.security.HasPrivilegesRequest;
 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 final class SecurityClient {
             options, GetPrivilegesResponse::fromXContent, listener, emptySet());
     }
 
+    /**
+     * Create or update application privileges.
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-privileges.html">
+     * the docs</a> 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.<br>
+     * See <a href=
+     * "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-privileges.html">
+     * the docs</a> 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<PutPrivilegesResponse> listener) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::putPrivileges, options,
+                PutPrivilegesResponse::fromXContent, listener, emptySet());
+    }
+
     /**
      * Removes application privilege(s)
      * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delete-privilege.html">

+ 14 - 5
client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java

@@ -28,17 +28,18 @@ 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.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 @@ final class SecurityRequestConverters {
         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")

+ 98 - 0
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<String, List<ApplicationPrivilege>> privileges;
+    private final RefreshPolicy refreshPolicy;
+
+    public PutPrivilegesRequest(final List<ApplicationPrivilege> 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<String, List<ApplicationPrivilege>> 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<String, List<ApplicationPrivilege>> 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();
+    }
+
+}

+ 105 - 0
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<String, Map<String, Boolean>> applicationPrivilegesCreatedOrUpdated;
+
+    public PutPrivilegesResponse(final Map<String, Map<String, Boolean>> 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<String, Map<String, Boolean>> applicationPrivilegesCreatedOrUpdated = new HashMap<>();
+        XContentParser.Token token = parser.currentToken();
+        if (token == null) {
+            token = parser.nextToken();
+        }
+        final Map<String, Object> appNameToPrivStatus = parser.map();
+        for (Entry<String, Object> entry : appNameToPrivStatus.entrySet()) {
+            if (entry.getValue() instanceof Map) {
+                final Map<String, Boolean> privilegeToStatus = applicationPrivilegesCreatedOrUpdated.computeIfAbsent(entry.getKey(),
+                        (a) -> new HashMap<>());
+                final Map<String, Object> createdOrUpdated = (Map<String, Object>) entry.getValue();
+                for (String privilegeName : createdOrUpdated.keySet()) {
+                    if (createdOrUpdated.get(privilegeName) instanceof Map) {
+                        final Map<String, Object> statusMap = (Map<String, Object>) 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);
+    }
+}

+ 15 - 1
client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/ApplicationPrivilege.java

@@ -24,6 +24,8 @@ import org.elasticsearch.common.ParseField;
 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 @@ import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optiona
  * 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 final class ApplicationPrivilege {
         }
     }
 
+    @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();
+    }
+
 }

+ 27 - 2
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.DisableUserRequest;
 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.RoleMapperExpress
 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 class SecurityRequestConvertersTests extends ESTestCase {
         assertNull(request.getEntity());
     }
 
+    public void testPutPrivileges() throws Exception {
+        int noOfApplicationPrivileges = randomIntBetween(2, 4);
+        final List<ApplicationPrivilege> 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<String, String> 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<String> privileges = randomSubsetOf(randomIntBetween(1, 3), "read", "write", "all");

+ 116 - 49
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java

@@ -29,7 +29,6 @@ import org.elasticsearch.action.support.PlainActionFuture;
 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.HasPrivilegesRequest;
 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.client.security.user.privileges.IndicesPrivileges;
 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 javax.crypto.spec.PBEKeySpec;
 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 class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
             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<ApplicationPrivilege> 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 class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
         }
     }
 
+    public void testPutPrivileges() throws Exception {
+        RestHighLevelClient client = highLevelClient();
+
+        {
+            // tag::put-privileges-request
+            final List<ApplicationPrivilege> 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<ApplicationPrivilege> 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<PutPrivilegesResponse> listener = new ActionListener<PutPrivilegesResponse>() {
+                @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<PutPrivilegesResponse> 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<ApplicationPrivilege> 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

+ 170 - 0
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<ApplicationPrivilege> 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<ApplicationPrivilege> 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<ApplicationPrivilege> 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<RefreshPolicy> policies = Sets.newHashSet(RefreshPolicy.values());
+        policies.remove(original.getRefreshPolicy());
+        switch (randomIntBetween(0, 1)) {
+        case 0:
+            final List<ApplicationPrivilege> 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));
+        }
+    }
+}

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

+ 20 - 12
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 @@ import static org.hamcrest.Matchers.equalTo;
 
 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 class ApplicationPrivilegeTests extends ESTestCase {
         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() {

+ 39 - 0
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.

+ 2 - 0
docs/java-rest/high-level/supported-apis.asciidoc

@@ -398,6 +398,7 @@ The Java High Level REST Client supports the following Security APIs:
 * <<java-rest-high-security-create-token>>
 * <<{upid}-invalidate-token>>
 * <<{upid}-get-privileges>>
+* <<{upid}-put-privileges>>
 * <<{upid}-delete-privileges>>
 
 include::security/put-user.asciidoc[]
@@ -419,6 +420,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