Skip to content

Commit 528d029

Browse files
authored
Add grant-api-key to HLRC (#68190)
This adds support for "Grant API Key" to the Java High Level Rest Client. This API was added in Elasticsearch 7.7 but did not have explicit support in the HLRC.
1 parent 5f3542a commit 528d029

File tree

7 files changed

+501
-30
lines changed

7 files changed

+501
-30
lines changed

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import org.elasticsearch.client.security.GetUserPrivilegesResponse;
5353
import org.elasticsearch.client.security.GetUsersRequest;
5454
import org.elasticsearch.client.security.GetUsersResponse;
55+
import org.elasticsearch.client.security.GrantApiKeyRequest;
5556
import org.elasticsearch.client.security.HasPrivilegesRequest;
5657
import org.elasticsearch.client.security.HasPrivilegesResponse;
5758
import org.elasticsearch.client.security.InvalidateApiKeyRequest;
@@ -1064,6 +1065,37 @@ public Cancellable invalidateApiKeyAsync(final InvalidateApiKeyRequest request,
10641065
InvalidateApiKeyResponse::fromXContent, listener, emptySet());
10651066
}
10661067

1068+
/**
1069+
* Create an API Key on behalf of another user.<br>
1070+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-grant-api-key.html">
1071+
* the docs</a> for more.
1072+
*
1073+
* @param request the request to grant an API key
1074+
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
1075+
* @return the response from the create API key call
1076+
* @throws IOException in case there is a problem sending the request or parsing back the response
1077+
*/
1078+
public CreateApiKeyResponse grantApiKey(final GrantApiKeyRequest request, final RequestOptions options) throws IOException {
1079+
return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::grantApiKey, options,
1080+
CreateApiKeyResponse::fromXContent, emptySet());
1081+
}
1082+
1083+
/**
1084+
* Asynchronously creates an API key on behalf of another user.<br>
1085+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-grant-api-key.html">
1086+
* the docs</a> for more.
1087+
*
1088+
* @param request the request to grant an API key
1089+
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
1090+
* @param listener the listener to be notified upon request completion
1091+
* @return cancellable that may be used to cancel the request
1092+
*/
1093+
public Cancellable grantApiKeyAsync(final GrantApiKeyRequest request, final RequestOptions options,
1094+
final ActionListener<CreateApiKeyResponse> listener) {
1095+
return restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::grantApiKey, options,
1096+
CreateApiKeyResponse::fromXContent, listener, emptySet());
1097+
}
1098+
10671099
/**
10681100
* Get an Elasticsearch access token from an {@code X509Certificate} chain. The certificate chain is that of the client from a mutually
10691101
* authenticated TLS session, and it is validated by the PKI realms with {@code delegation.enabled} toggled to {@code true}.<br>

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.elasticsearch.client.security.GetRoleMappingsRequest;
3232
import org.elasticsearch.client.security.GetRolesRequest;
3333
import org.elasticsearch.client.security.GetUsersRequest;
34+
import org.elasticsearch.client.security.GrantApiKeyRequest;
3435
import org.elasticsearch.client.security.HasPrivilegesRequest;
3536
import org.elasticsearch.client.security.InvalidateApiKeyRequest;
3637
import org.elasticsearch.client.security.InvalidateTokenRequest;
@@ -296,6 +297,15 @@ static Request createApiKey(final CreateApiKeyRequest createApiKeyRequest) throw
296297
return request;
297298
}
298299

300+
static Request grantApiKey(final GrantApiKeyRequest grantApiKeyRequest) throws IOException {
301+
final Request request = new Request(HttpPost.METHOD_NAME, "/_security/api_key/grant");
302+
request.setEntity(createEntity(grantApiKeyRequest, REQUEST_BODY_CONTENT_TYPE));
303+
final RequestConverters.Params params = new RequestConverters.Params();
304+
params.withRefreshPolicy(grantApiKeyRequest.getApiKeyRequest().getRefreshPolicy());
305+
request.addParameters(params.asMap());
306+
return request;
307+
}
308+
299309
static Request getApiKey(final GetApiKeyRequest getApiKeyRequest) throws IOException {
300310
final Request request = new Request(HttpGet.METHOD_NAME, "/_security/api_key");
301311
if (Strings.hasText(getApiKeyRequest.getId())) {
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.client.security;
10+
11+
import org.elasticsearch.client.Validatable;
12+
import org.elasticsearch.common.CharArrays;
13+
import org.elasticsearch.common.xcontent.ToXContentFragment;
14+
import org.elasticsearch.common.xcontent.ToXContentObject;
15+
import org.elasticsearch.common.xcontent.XContentBuilder;
16+
17+
import java.io.IOException;
18+
import java.util.Arrays;
19+
import java.util.Objects;
20+
21+
/**
22+
* Request to create a new API Key on behalf of another user.
23+
*/
24+
public final class GrantApiKeyRequest implements Validatable, ToXContentObject {
25+
26+
private final Grant grant;
27+
private final CreateApiKeyRequest apiKeyRequest;
28+
29+
public static class Grant implements ToXContentFragment {
30+
private final String grantType;
31+
private final String username;
32+
private final char[] password;
33+
private final String accessToken;
34+
35+
private Grant(String grantType, String username, char[] password, String accessToken) {
36+
this.grantType = Objects.requireNonNull(grantType, "Grant type may not be null");
37+
this.username = username;
38+
this.password = password;
39+
this.accessToken = accessToken;
40+
}
41+
42+
public static Grant passwordGrant(String username, char[] password) {
43+
return new Grant(
44+
"password",
45+
Objects.requireNonNull(username, "Username may not be null"),
46+
Objects.requireNonNull(password, "Password may not be null"),
47+
null);
48+
}
49+
50+
public static Grant accessTokenGrant(String accessToken) {
51+
return new Grant(
52+
"access_token",
53+
null,
54+
null,
55+
Objects.requireNonNull(accessToken, "Access token may not be null")
56+
);
57+
}
58+
59+
public String getGrantType() {
60+
return grantType;
61+
}
62+
63+
public String getUsername() {
64+
return username;
65+
}
66+
67+
public char[] getPassword() {
68+
return password;
69+
}
70+
71+
public String getAccessToken() {
72+
return accessToken;
73+
}
74+
75+
@Override
76+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
77+
builder.field("grant_type", grantType);
78+
if (username != null) {
79+
builder.field("username", username);
80+
}
81+
if (password != null) {
82+
byte[] passwordBytes = CharArrays.toUtf8Bytes(password);
83+
try {
84+
builder.field("password").utf8Value(passwordBytes, 0, passwordBytes.length);
85+
} finally {
86+
Arrays.fill(passwordBytes, (byte) 0);
87+
}
88+
}
89+
if (accessToken != null) {
90+
builder.field("access_token", accessToken);
91+
}
92+
return builder;
93+
}
94+
95+
@Override
96+
public boolean equals(Object o) {
97+
if (this == o) {
98+
return true;
99+
}
100+
if (o == null || getClass() != o.getClass()) {
101+
return false;
102+
}
103+
Grant grant = (Grant) o;
104+
return grantType.equals(grant.grantType)
105+
&& Objects.equals(username, grant.username)
106+
&& Arrays.equals(password, grant.password)
107+
&& Objects.equals(accessToken, grant.accessToken);
108+
}
109+
110+
@Override
111+
public int hashCode() {
112+
int result = Objects.hash(grantType, username, accessToken);
113+
result = 31 * result + Arrays.hashCode(password);
114+
return result;
115+
}
116+
}
117+
118+
public GrantApiKeyRequest(Grant grant, CreateApiKeyRequest apiKeyRequest) {
119+
this.grant = Objects.requireNonNull(grant, "Grant may not be null");
120+
this.apiKeyRequest = Objects.requireNonNull(apiKeyRequest, "Create API key request may not be null");
121+
}
122+
123+
public Grant getGrant() {
124+
return grant;
125+
}
126+
127+
public CreateApiKeyRequest getApiKeyRequest() {
128+
return apiKeyRequest;
129+
}
130+
131+
@Override
132+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
133+
builder.startObject();
134+
grant.toXContent(builder, params);
135+
builder.field("api_key", apiKeyRequest);
136+
return builder.endObject();
137+
}
138+
139+
@Override
140+
public boolean equals(Object o) {
141+
if (this == o) {
142+
return true;
143+
}
144+
if (o == null || getClass() != o.getClass()) {
145+
return false;
146+
}
147+
final GrantApiKeyRequest that = (GrantApiKeyRequest) o;
148+
return Objects.equals(this.grant, that.grant)
149+
&& Objects.equals(this.apiKeyRequest, that.apiKeyRequest);
150+
}
151+
152+
@Override
153+
public int hashCode() {
154+
return Objects.hash(grant, apiKeyRequest);
155+
}
156+
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,17 @@ public static final class Builder {
212212
private Builder() {
213213
}
214214

215+
public Builder clone(Role role) {
216+
return this
217+
.name(role.name)
218+
.clusterPrivileges(role.clusterPrivileges)
219+
.globalApplicationPrivileges(role.globalPrivileges)
220+
.indicesPrivileges(role.indicesPrivileges)
221+
.applicationResourcePrivileges(role.applicationPrivileges)
222+
.runAsPrivilege(role.runAsPrivilege)
223+
.metadata(role.metadata);
224+
}
225+
215226
public Builder name(String name) {
216227
if (Strings.hasText(name) == false){
217228
throw new IllegalArgumentException("role name must be provided");

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

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.elasticsearch.client.security.GetRoleMappingsRequest;
2828
import org.elasticsearch.client.security.GetRolesRequest;
2929
import org.elasticsearch.client.security.GetUsersRequest;
30+
import org.elasticsearch.client.security.GrantApiKeyRequest;
3031
import org.elasticsearch.client.security.InvalidateApiKeyRequest;
3132
import org.elasticsearch.client.security.PutPrivilegesRequest;
3233
import org.elasticsearch.client.security.PutRoleMappingRequest;
@@ -425,23 +426,52 @@ public void testPutRole() throws IOException {
425426
}
426427

427428
public void testCreateApiKey() throws IOException {
429+
final CreateApiKeyRequest createApiKeyRequest = buildCreateApiKeyRequest();
430+
431+
final Map<String, String> expectedParams;
432+
final RefreshPolicy refreshPolicy = createApiKeyRequest.getRefreshPolicy();
433+
if (refreshPolicy != RefreshPolicy.NONE) {
434+
expectedParams = Collections.singletonMap("refresh", refreshPolicy.getValue());
435+
} else {
436+
expectedParams = Collections.emptyMap();
437+
}
438+
439+
final Request request = SecurityRequestConverters.createApiKey(createApiKeyRequest);
440+
assertEquals(HttpPost.METHOD_NAME, request.getMethod());
441+
assertEquals("/_security/api_key", request.getEndpoint());
442+
assertEquals(expectedParams, request.getParameters());
443+
assertToXContentBody(createApiKeyRequest, request.getEntity());
444+
}
445+
446+
private CreateApiKeyRequest buildCreateApiKeyRequest() {
428447
final String name = randomAlphaOfLengthBetween(4, 7);
429448
final List<Role> roles = Collections.singletonList(Role.builder().name("r1").clusterPrivileges(ClusterPrivilegeName.ALL)
430449
.indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build());
431450
final TimeValue expiration = randomBoolean() ? null : TimeValue.timeValueHours(24);
432451
final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values());
452+
final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy);
453+
return createApiKeyRequest;
454+
}
455+
456+
public void testGrantApiKey() throws IOException {
457+
final CreateApiKeyRequest createApiKeyRequest = buildCreateApiKeyRequest();
458+
final GrantApiKeyRequest grantApiKeyRequest = new GrantApiKeyRequest(randomBoolean()
459+
? GrantApiKeyRequest.Grant.accessTokenGrant(randomAlphaOfLength(24))
460+
: GrantApiKeyRequest.Grant.passwordGrant(randomAlphaOfLengthBetween(4, 12), randomAlphaOfLengthBetween(14, 18).toCharArray()),
461+
createApiKeyRequest);
433462
final Map<String, String> expectedParams;
463+
final RefreshPolicy refreshPolicy = createApiKeyRequest.getRefreshPolicy();
434464
if (refreshPolicy != RefreshPolicy.NONE) {
435465
expectedParams = Collections.singletonMap("refresh", refreshPolicy.getValue());
436466
} else {
437467
expectedParams = Collections.emptyMap();
438468
}
439-
final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy);
440-
final Request request = SecurityRequestConverters.createApiKey(createApiKeyRequest);
469+
470+
final Request request = SecurityRequestConverters.grantApiKey(grantApiKeyRequest);
441471
assertEquals(HttpPost.METHOD_NAME, request.getMethod());
442-
assertEquals("/_security/api_key", request.getEndpoint());
472+
assertEquals("/_security/api_key/grant", request.getEndpoint());
443473
assertEquals(expectedParams, request.getParameters());
444-
assertToXContentBody(createApiKeyRequest, request.getEntity());
474+
assertToXContentBody(grantApiKeyRequest, request.getEntity());
445475
}
446476

447477
public void testGetApiKey() throws IOException {

0 commit comments

Comments
 (0)