Skip to content

Commit 32c4f99

Browse files
authored
[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.
1 parent 902d6f5 commit 32c4f99

File tree

12 files changed

+726
-69
lines changed

12 files changed

+726
-69
lines changed

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
import org.elasticsearch.client.security.HasPrivilegesResponse;
5353
import org.elasticsearch.client.security.InvalidateTokenRequest;
5454
import org.elasticsearch.client.security.InvalidateTokenResponse;
55+
import org.elasticsearch.client.security.PutPrivilegesRequest;
56+
import org.elasticsearch.client.security.PutPrivilegesResponse;
5557
import org.elasticsearch.client.security.PutRoleMappingRequest;
5658
import org.elasticsearch.client.security.PutRoleMappingResponse;
5759
import org.elasticsearch.client.security.PutUserRequest;
@@ -603,6 +605,38 @@ public void getPrivilegesAsync(final GetPrivilegesRequest request, final Request
603605
options, GetPrivilegesResponse::fromXContent, listener, emptySet());
604606
}
605607

608+
/**
609+
* Create or update application privileges.
610+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-privileges.html">
611+
* the docs</a> for more.
612+
*
613+
* @param request the request to create or update application privileges
614+
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
615+
* @return the response from the create or update application privileges call
616+
* @throws IOException in case there is a problem sending the request or parsing back the response
617+
*/
618+
public PutPrivilegesResponse putPrivileges(final PutPrivilegesRequest request, final RequestOptions options) throws IOException {
619+
return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::putPrivileges, options,
620+
PutPrivilegesResponse::fromXContent, emptySet());
621+
}
622+
623+
/**
624+
* Asynchronously create or update application privileges.<br>
625+
* See <a href=
626+
* "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-privileges.html">
627+
* the docs</a> for more.
628+
*
629+
* @param request the request to create or update application privileges
630+
* @param options the request options (e.g. headers), use
631+
* {@link RequestOptions#DEFAULT} if nothing needs to be customized
632+
* @param listener the listener to be notified upon request completion
633+
*/
634+
public void putPrivilegesAsync(final PutPrivilegesRequest request, final RequestOptions options,
635+
final ActionListener<PutPrivilegesResponse> listener) {
636+
restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::putPrivileges, options,
637+
PutPrivilegesResponse::fromXContent, listener, emptySet());
638+
}
639+
606640
/**
607641
* Removes application privilege(s)
608642
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delete-privilege.html">

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,18 @@
2828
import org.elasticsearch.client.security.ClearRolesCacheRequest;
2929
import org.elasticsearch.client.security.CreateTokenRequest;
3030
import org.elasticsearch.client.security.DeletePrivilegesRequest;
31-
import org.elasticsearch.client.security.GetPrivilegesRequest;
3231
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
3332
import org.elasticsearch.client.security.DeleteRoleRequest;
3433
import org.elasticsearch.client.security.DeleteUserRequest;
35-
import org.elasticsearch.client.security.InvalidateTokenRequest;
36-
import org.elasticsearch.client.security.GetRolesRequest;
37-
import org.elasticsearch.client.security.PutRoleMappingRequest;
38-
import org.elasticsearch.client.security.HasPrivilegesRequest;
3934
import org.elasticsearch.client.security.DisableUserRequest;
4035
import org.elasticsearch.client.security.EnableUserRequest;
36+
import org.elasticsearch.client.security.GetPrivilegesRequest;
4137
import org.elasticsearch.client.security.GetRoleMappingsRequest;
38+
import org.elasticsearch.client.security.GetRolesRequest;
39+
import org.elasticsearch.client.security.HasPrivilegesRequest;
40+
import org.elasticsearch.client.security.InvalidateTokenRequest;
41+
import org.elasticsearch.client.security.PutPrivilegesRequest;
42+
import org.elasticsearch.client.security.PutRoleMappingRequest;
4243
import org.elasticsearch.client.security.PutUserRequest;
4344
import org.elasticsearch.client.security.SetUserEnabledRequest;
4445
import org.elasticsearch.common.Strings;
@@ -213,6 +214,14 @@ static Request getPrivileges(GetPrivilegesRequest getPrivilegesRequest) {
213214
return new Request(HttpGet.METHOD_NAME, endpoint);
214215
}
215216

217+
static Request putPrivileges(final PutPrivilegesRequest putPrivilegesRequest) throws IOException {
218+
Request request = new Request(HttpPut.METHOD_NAME, "/_xpack/security/privilege");
219+
request.setEntity(createEntity(putPrivilegesRequest, REQUEST_BODY_CONTENT_TYPE));
220+
RequestConverters.Params params = new RequestConverters.Params(request);
221+
params.withRefreshPolicy(putPrivilegesRequest.getRefreshPolicy());
222+
return request;
223+
}
224+
216225
static Request deletePrivileges(DeletePrivilegesRequest deletePrivilegeRequest) {
217226
String endpoint = new RequestConverters.EndpointBuilder()
218227
.addPathPartAsIs("_xpack/security/privilege")
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.client.security;
21+
22+
import org.elasticsearch.client.Validatable;
23+
import org.elasticsearch.client.security.user.privileges.ApplicationPrivilege;
24+
import org.elasticsearch.common.Nullable;
25+
import org.elasticsearch.common.xcontent.ToXContentObject;
26+
import org.elasticsearch.common.xcontent.XContentBuilder;
27+
28+
import java.io.IOException;
29+
import java.util.Collections;
30+
import java.util.List;
31+
import java.util.Map;
32+
import java.util.Map.Entry;
33+
import java.util.Objects;
34+
import java.util.TreeMap;
35+
import java.util.stream.Collectors;
36+
37+
/**
38+
* Request object for creating/updating application privileges.
39+
*/
40+
public final class PutPrivilegesRequest implements Validatable, ToXContentObject {
41+
42+
private final Map<String, List<ApplicationPrivilege>> privileges;
43+
private final RefreshPolicy refreshPolicy;
44+
45+
public PutPrivilegesRequest(final List<ApplicationPrivilege> privileges, @Nullable final RefreshPolicy refreshPolicy) {
46+
if (privileges == null || privileges.isEmpty()) {
47+
throw new IllegalArgumentException("privileges are required");
48+
}
49+
this.privileges = Collections.unmodifiableMap(privileges.stream()
50+
.collect(Collectors.groupingBy(ApplicationPrivilege::getApplication, TreeMap::new, Collectors.toList())));
51+
this.refreshPolicy = refreshPolicy == null ? RefreshPolicy.IMMEDIATE : refreshPolicy;
52+
}
53+
54+
/**
55+
* @return a map of application name to list of
56+
* {@link ApplicationPrivilege}s
57+
*/
58+
public Map<String, List<ApplicationPrivilege>> getPrivileges() {
59+
return privileges;
60+
}
61+
62+
public RefreshPolicy getRefreshPolicy() {
63+
return refreshPolicy;
64+
}
65+
66+
@Override
67+
public int hashCode() {
68+
return Objects.hash(privileges, refreshPolicy);
69+
}
70+
71+
@Override
72+
public boolean equals(Object o) {
73+
if (this == o) {
74+
return true;
75+
}
76+
if (o == null || (this.getClass() != o.getClass())) {
77+
return false;
78+
}
79+
final PutPrivilegesRequest that = (PutPrivilegesRequest) o;
80+
return privileges.equals(that.privileges) && (refreshPolicy == that.refreshPolicy);
81+
}
82+
83+
@Override
84+
public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException {
85+
builder.startObject();
86+
for (Entry<String, List<ApplicationPrivilege>> entry : privileges.entrySet()) {
87+
builder.field(entry.getKey());
88+
builder.startObject();
89+
for (ApplicationPrivilege applicationPrivilege : entry.getValue()) {
90+
builder.field(applicationPrivilege.getName());
91+
applicationPrivilege.toXContent(builder, params);
92+
}
93+
builder.endObject();
94+
}
95+
return builder.endObject();
96+
}
97+
98+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.client.security;
21+
22+
import org.elasticsearch.common.ParsingException;
23+
import org.elasticsearch.common.Strings;
24+
import org.elasticsearch.common.xcontent.XContentParser;
25+
26+
import java.io.IOException;
27+
import java.util.Collections;
28+
import java.util.HashMap;
29+
import java.util.Map;
30+
import java.util.Map.Entry;
31+
32+
/**
33+
* Response when creating/updating one or more application privileges to the
34+
* security index.
35+
*/
36+
public final class PutPrivilegesResponse {
37+
38+
/*
39+
* Map of application name to a map of privilege name to boolean denoting
40+
* created or update status.
41+
*/
42+
private final Map<String, Map<String, Boolean>> applicationPrivilegesCreatedOrUpdated;
43+
44+
public PutPrivilegesResponse(final Map<String, Map<String, Boolean>> applicationPrivilegesCreatedOrUpdated) {
45+
this.applicationPrivilegesCreatedOrUpdated = Collections.unmodifiableMap(applicationPrivilegesCreatedOrUpdated);
46+
}
47+
48+
/**
49+
* Get response status for the request to create or update application
50+
* privileges.
51+
*
52+
* @param applicationName application name as specified in the request
53+
* @param privilegeName privilege name as specified in the request
54+
* @return {@code true} if the privilege was created, {@code false} if the
55+
* privilege was updated
56+
* @throws IllegalArgumentException thrown for unknown application name or
57+
* privilege name.
58+
*/
59+
public boolean wasCreated(final String applicationName, final String privilegeName) {
60+
if (Strings.hasText(applicationName) == false) {
61+
throw new IllegalArgumentException("application name is required");
62+
}
63+
if (Strings.hasText(privilegeName) == false) {
64+
throw new IllegalArgumentException("privilege name is required");
65+
}
66+
if (applicationPrivilegesCreatedOrUpdated.get(applicationName) == null
67+
|| applicationPrivilegesCreatedOrUpdated.get(applicationName).get(privilegeName) == null) {
68+
throw new IllegalArgumentException("application name or privilege name not found in the response");
69+
}
70+
return applicationPrivilegesCreatedOrUpdated.get(applicationName).get(privilegeName);
71+
}
72+
73+
@SuppressWarnings("unchecked")
74+
public static PutPrivilegesResponse fromXContent(final XContentParser parser) throws IOException {
75+
final Map<String, Map<String, Boolean>> applicationPrivilegesCreatedOrUpdated = new HashMap<>();
76+
XContentParser.Token token = parser.currentToken();
77+
if (token == null) {
78+
token = parser.nextToken();
79+
}
80+
final Map<String, Object> appNameToPrivStatus = parser.map();
81+
for (Entry<String, Object> entry : appNameToPrivStatus.entrySet()) {
82+
if (entry.getValue() instanceof Map) {
83+
final Map<String, Boolean> privilegeToStatus = applicationPrivilegesCreatedOrUpdated.computeIfAbsent(entry.getKey(),
84+
(a) -> new HashMap<>());
85+
final Map<String, Object> createdOrUpdated = (Map<String, Object>) entry.getValue();
86+
for (String privilegeName : createdOrUpdated.keySet()) {
87+
if (createdOrUpdated.get(privilegeName) instanceof Map) {
88+
final Map<String, Object> statusMap = (Map<String, Object>) createdOrUpdated.get(privilegeName);
89+
final Object status = statusMap.get("created");
90+
if (status instanceof Boolean) {
91+
privilegeToStatus.put(privilegeName, (Boolean) status);
92+
} else {
93+
throw new ParsingException(parser.getTokenLocation(), "Failed to parse object, unexpected structure");
94+
}
95+
} else {
96+
throw new ParsingException(parser.getTokenLocation(), "Failed to parse object, unexpected structure");
97+
}
98+
}
99+
} else {
100+
throw new ParsingException(parser.getTokenLocation(), "Failed to parse object, unexpected structure");
101+
}
102+
}
103+
return new PutPrivilegesResponse(applicationPrivilegesCreatedOrUpdated);
104+
}
105+
}

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import org.elasticsearch.common.Strings;
2525
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
2626
import org.elasticsearch.common.xcontent.ObjectParser;
27+
import org.elasticsearch.common.xcontent.ToXContentObject;
28+
import org.elasticsearch.common.xcontent.XContentBuilder;
2729
import org.elasticsearch.common.xcontent.XContentParser;
2830

2931
import java.io.IOException;
@@ -44,7 +46,7 @@
4446
* actions and metadata are completely managed by the client and can contain arbitrary
4547
* string values.
4648
*/
47-
public final class ApplicationPrivilege {
49+
public final class ApplicationPrivilege implements ToXContentObject {
4850

4951
private static final ParseField APPLICATION = new ParseField("application");
5052
private static final ParseField NAME = new ParseField("name");
@@ -171,4 +173,16 @@ public ApplicationPrivilege build() {
171173
}
172174
}
173175

176+
@Override
177+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
178+
builder.startObject()
179+
.field(APPLICATION.getPreferredName(), application)
180+
.field(NAME.getPreferredName(), name)
181+
.field(ACTIONS.getPreferredName(), actions);
182+
if (metadata != null && metadata.isEmpty() == false) {
183+
builder.field(METADATA.getPreferredName(), metadata);
184+
}
185+
return builder.endObject();
186+
}
187+
174188
}

client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@
1919

2020
package org.elasticsearch.client;
2121

22-
import org.apache.http.client.methods.HttpGet;
2322
import org.apache.http.client.methods.HttpDelete;
23+
import org.apache.http.client.methods.HttpGet;
2424
import org.apache.http.client.methods.HttpPost;
2525
import org.apache.http.client.methods.HttpPut;
26+
import org.elasticsearch.client.security.ChangePasswordRequest;
2627
import org.elasticsearch.client.security.CreateTokenRequest;
2728
import org.elasticsearch.client.security.DeletePrivilegesRequest;
2829
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
@@ -32,19 +33,22 @@
3233
import org.elasticsearch.client.security.EnableUserRequest;
3334
import org.elasticsearch.client.security.GetPrivilegesRequest;
3435
import org.elasticsearch.client.security.GetRoleMappingsRequest;
35-
import org.elasticsearch.client.security.ChangePasswordRequest;
3636
import org.elasticsearch.client.security.GetRolesRequest;
37+
import org.elasticsearch.client.security.PutPrivilegesRequest;
3738
import org.elasticsearch.client.security.PutRoleMappingRequest;
3839
import org.elasticsearch.client.security.PutUserRequest;
3940
import org.elasticsearch.client.security.RefreshPolicy;
4041
import org.elasticsearch.client.security.support.expressiondsl.RoleMapperExpression;
4142
import org.elasticsearch.client.security.support.expressiondsl.expressions.AnyRoleMapperExpression;
4243
import org.elasticsearch.client.security.support.expressiondsl.fields.FieldRoleMapperExpression;
4344
import org.elasticsearch.client.security.user.User;
45+
import org.elasticsearch.client.security.user.privileges.ApplicationPrivilege;
4446
import org.elasticsearch.common.Strings;
47+
import org.elasticsearch.common.util.set.Sets;
4548
import org.elasticsearch.test.ESTestCase;
4649

4750
import java.io.IOException;
51+
import java.util.ArrayList;
4852
import java.util.Arrays;
4953
import java.util.Collections;
5054
import java.util.HashMap;
@@ -318,6 +322,27 @@ public void testGetAllPrivileges() throws Exception {
318322
assertNull(request.getEntity());
319323
}
320324

325+
public void testPutPrivileges() throws Exception {
326+
int noOfApplicationPrivileges = randomIntBetween(2, 4);
327+
final List<ApplicationPrivilege> privileges = new ArrayList<>();
328+
for (int count = 0; count < noOfApplicationPrivileges; count++) {
329+
privileges.add(ApplicationPrivilege.builder()
330+
.application(randomAlphaOfLength(4))
331+
.privilege(randomAlphaOfLengthBetween(3, 5))
332+
.actions(Sets.newHashSet(generateRandomStringArray(3, 5, false, false)))
333+
.metadata(Collections.singletonMap("k1", "v1"))
334+
.build());
335+
}
336+
final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
337+
final Map<String, String> expectedParams = getExpectedParamsFromRefreshPolicy(refreshPolicy);
338+
final PutPrivilegesRequest putPrivilegesRequest = new PutPrivilegesRequest(privileges, refreshPolicy);
339+
final Request request = SecurityRequestConverters.putPrivileges(putPrivilegesRequest);
340+
assertEquals(HttpPut.METHOD_NAME, request.getMethod());
341+
assertEquals("/_xpack/security/privilege", request.getEndpoint());
342+
assertEquals(expectedParams, request.getParameters());
343+
assertToXContentBody(putPrivilegesRequest, request.getEntity());
344+
}
345+
321346
public void testDeletePrivileges() {
322347
final String application = randomAlphaOfLengthBetween(1, 12);
323348
final List<String> privileges = randomSubsetOf(randomIntBetween(1, 3), "read", "write", "all");

0 commit comments

Comments
 (0)