Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import org.elasticsearch.client.security.ClearSecurityCacheResponse;
import org.elasticsearch.client.security.CreateApiKeyRequest;
import org.elasticsearch.client.security.CreateApiKeyResponse;
import org.elasticsearch.client.security.CreateEnrollmentTokenRequest;
import org.elasticsearch.client.security.CreateEnrollmentTokenResponse;
import org.elasticsearch.client.security.CreateTokenRequest;
import org.elasticsearch.client.security.CreateTokenResponse;
import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest;
Expand Down Expand Up @@ -1133,13 +1135,36 @@ public Cancellable delegatePkiAuthenticationAsync(DelegatePkiAuthenticationReque
DelegatePkiAuthenticationResponse::fromXContent, listener, emptySet());
}

/**
* Create an Enrollment Token used to enroll a client to a secured cluster using the Enroll Client API
*
*/
public CreateEnrollmentTokenResponse createEnrollmentToken(RequestOptions options) throws IOException {
return restHighLevelClient.performRequestAndParseEntity(CreateEnrollmentTokenRequest.INSTANCE,
CreateEnrollmentTokenRequest::getRequest, options,
CreateEnrollmentTokenResponse::fromXContent, emptySet());
}

/**
* Asynchronously create an Enrollment Token used to enroll a client to a secured cluster using the Enroll Client API
*
* @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 Cancellable createEnrollmentTokenAsync(RequestOptions options, ActionListener<CreateEnrollmentTokenResponse> listener) {
return restHighLevelClient.performRequestAsyncAndParseEntity(CreateEnrollmentTokenRequest.INSTANCE,
CreateEnrollmentTokenRequest::getRequest, options,
CreateEnrollmentTokenResponse::fromXContent, listener, emptySet());
}


/**
* Allows a node to join to a cluster with security features enabled using the Enroll Node API.
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
* @return the response
* @throws IOException in case there is a problem sending the request or parsing back the response
*/

public NodeEnrollmentResponse enrollNode(RequestOptions options) throws IOException {
return restHighLevelClient.performRequestAndParseEntity(
NodeEnrollmentRequest.INSTANCE, NodeEnrollmentRequest::getRequest,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.client.security;

import org.apache.http.client.methods.HttpPut;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.Validatable;

public class CreateEnrollmentTokenRequest implements Validatable {

public CreateEnrollmentTokenRequest() {
}

public static final CreateEnrollmentTokenRequest INSTANCE = new CreateEnrollmentTokenRequest();


public Request getRequest() {
return new Request(HttpPut.METHOD_NAME, "/_security/enrollment_token");
Copy link
Contributor

@albertzaharovits albertzaharovits May 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets use POST here, please.

EDIT: but maybe this class should be removed completely until we find the need for it to be exposed, see #72186 (review)

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.client.security;

import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.XContentParser;

import java.io.IOException;
import java.util.Objects;

public class CreateEnrollmentTokenResponse {
private String enrollmentToken;

public CreateEnrollmentTokenResponse(String enrollmentToken) {
this.enrollmentToken = enrollmentToken;
}

public String getEnrollmentToken() {
return enrollmentToken;
}

private static final ParseField ENROLLMENT_TOKEN = new ParseField("enrollment_token");

@SuppressWarnings("unchecked")
private static final ConstructingObjectParser<CreateEnrollmentTokenResponse, Void> PARSER =
new ConstructingObjectParser<>(CreateEnrollmentTokenResponse.class.getName(), true,
a -> new CreateEnrollmentTokenResponse((String) a[0]));

static {
PARSER.declareString(ConstructingObjectParser.constructorArg(), ENROLLMENT_TOKEN);
}

public static CreateEnrollmentTokenResponse fromXContent(XContentParser parser) throws IOException {
return PARSER.apply(parser, null);
}

@Override public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CreateEnrollmentTokenResponse that = (CreateEnrollmentTokenResponse) o;
return enrollmentToken.equals(that.enrollmentToken);
}

@Override public int hashCode() {
return Objects.hash(enrollmentToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -326,11 +326,13 @@ public static class ClusterPrivilegeName {
public static final String MANAGE_ILM = "manage_ilm";
public static final String READ_ILM = "read_ilm";
public static final String MANAGE_ENRICH = "manage_enrich";
public static final String MANAGE_ENROLLMENT = "manage_enrollment";
public static final String ENROLL = "enroll";
public static final String[] ALL_ARRAY = new String[] { NONE, ALL, MONITOR, MONITOR_TRANSFORM_DEPRECATED, MONITOR_TRANSFORM,
MONITOR_ML, MONITOR_TEXT_STRUCTURE, MONITOR_WATCHER, MONITOR_ROLLUP, MANAGE, MANAGE_TRANSFORM_DEPRECATED, MANAGE_TRANSFORM,
MANAGE_ML, MANAGE_WATCHER, MANAGE_ROLLUP, MANAGE_INDEX_TEMPLATES, MANAGE_INGEST_PIPELINES, READ_PIPELINE,
TRANSPORT_CLIENT, MANAGE_SECURITY, MANAGE_SAML, MANAGE_OIDC, MANAGE_TOKEN, MANAGE_PIPELINE, MANAGE_AUTOSCALING, MANAGE_CCR,
READ_CCR, MANAGE_ILM, READ_ILM, MANAGE_ENRICH };
READ_CCR, MANAGE_ILM, READ_ILM, MANAGE_ENRICH, MANAGE_ENROLLMENT, ENROLL };
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.apache.http.client.methods.HttpDelete;
import org.elasticsearch.ElasticsearchStatusException;
import org.elasticsearch.client.security.AuthenticateResponse;
import org.elasticsearch.client.security.CreateEnrollmentTokenResponse;
import org.elasticsearch.client.security.DeleteRoleRequest;
import org.elasticsearch.client.security.DeleteRoleResponse;
import org.elasticsearch.client.security.DeleteUserRequest;
Expand All @@ -33,16 +34,21 @@
import org.elasticsearch.client.security.user.privileges.IndicesPrivilegesTests;
import org.elasticsearch.client.security.user.privileges.Role;
import org.elasticsearch.common.CharArrays;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;

import static org.elasticsearch.client.RequestOptions.DEFAULT;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
Expand Down Expand Up @@ -142,7 +148,7 @@ public void testPutRole() throws Exception {
assertThat(createRoleResponse.isCreated(), is(true));

final GetRolesRequest getRoleRequest = new GetRolesRequest(role.getName());
final GetRolesResponse getRoleResponse = securityClient.getRoles(getRoleRequest, RequestOptions.DEFAULT);
final GetRolesResponse getRoleResponse = securityClient.getRoles(getRoleRequest, DEFAULT);
// assert role is equal
assertThat(getRoleResponse.getRoles(), contains(role));

Expand All @@ -151,11 +157,30 @@ public void testPutRole() throws Exception {
assertThat(updateRoleResponse.isCreated(), is(false));

final DeleteRoleRequest deleteRoleRequest = new DeleteRoleRequest(role.getName());
final DeleteRoleResponse deleteRoleResponse = securityClient.deleteRole(deleteRoleRequest, RequestOptions.DEFAULT);
final DeleteRoleResponse deleteRoleResponse = securityClient.deleteRole(deleteRoleRequest, DEFAULT);
// assert role deleted
assertThat(deleteRoleResponse.isFound(), is(true));
}

@AwaitsFix(bugUrl = "Determine behavior for keystores with multiple keys")
public void testCreateEnrollmentTokenClient() throws Exception {
final SecurityClient securityClient = highLevelClient().security();
final Map<String, Object> info;
CreateEnrollmentTokenResponse clientResponse =
execute(securityClient::createEnrollmentToken, securityClient::createEnrollmentTokenAsync, DEFAULT);
assertThat(clientResponse, notNullValue());
String jsonString = new String(Base64.getDecoder().decode(clientResponse.getEnrollmentToken()), StandardCharsets.UTF_8);
try (XContentParser parser = createParser(JsonXContent.jsonXContent, jsonString)) {
info = parser.map();
assertNotEquals(info, null);
info.entrySet().stream()
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().toString()));
}
assertThat(info.get("adr"), notNullValue());
assertEquals("598a35cd831ee6bb90e79aa80d6b073cda88b41d", info.get("fgr"));
assertThat(info.get("key"), notNullValue());
}

@AwaitsFix(bugUrl = "Determine behavior for keystore with multiple keys")
public void testEnrollNode() throws Exception {
final NodeEnrollmentResponse nodeEnrollmentResponse =
Expand Down Expand Up @@ -239,7 +264,7 @@ private static String basicAuthHeader(String username, char[] password) {
}

private static RequestOptions authorizationRequestOptions(String authorizationHeader) {
final RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
final RequestOptions.Builder builder = DEFAULT.toBuilder();
builder.addHeader("Authorization", authorizationHeader);
return builder.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.elasticsearch.client.security.CreateApiKeyRequest;
import org.elasticsearch.client.security.CreateApiKeyRequestTests;
import org.elasticsearch.client.security.CreateApiKeyResponse;
import org.elasticsearch.client.security.CreateEnrollmentTokenResponse;
import org.elasticsearch.client.security.CreateTokenRequest;
import org.elasticsearch.client.security.CreateTokenResponse;
import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest;
Expand Down Expand Up @@ -2609,6 +2610,45 @@ public void onFailure(Exception e) {
}
}

@AwaitsFix(bugUrl = "Determine behavior for keystores with multiple keys")
public void testCreateEnrollmentToken() throws Exception {
RestHighLevelClient client = highLevelClient();

{
// tag::create-enrollment-token-execute
CreateEnrollmentTokenResponse response = client.security().createEnrollmentToken(RequestOptions.DEFAULT);
// end::create-enrollment-token-execute

// tag::create-enrollment-token-response
String enrollmentToken = response.getEnrollmentToken(); // <1>
// end::create-enrollment-token-response
}

{
// tag::create-enrollment-token-execute-listener
ActionListener<CreateEnrollmentTokenResponse> listener =
new ActionListener<CreateEnrollmentTokenResponse>() {
@Override
public void onResponse(CreateEnrollmentTokenResponse response) {
// <1>
}

@Override
public void onFailure(Exception e) {
// <2>
}};
// end::create-enrollment-token-execute-listener

final CountDownLatch latch = new CountDownLatch(1);
listener = new LatchedActionListener<>(listener, latch);

// tag::create-enrollment-token-execute-async
client.security().createEnrollmentTokenAsync(RequestOptions.DEFAULT, listener);
// end::create-enrollment-token-execute-async
assertTrue(latch.await(30L, TimeUnit.SECONDS));
}
}

private X509Certificate readCertForPkiDelegation(String certificateName) throws Exception {
Path path = getDataPath("/org/elasticsearch/client/security/delegate_pki/" + certificateName);
try (InputStream in = Files.newInputStream(path)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.client.security;

import org.elasticsearch.common.UUIDs;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.test.ESTestCase;

import java.io.IOException;

import static org.hamcrest.Matchers.equalTo;

public class CreateEnrollmentTokenResponseTests extends ESTestCase {

public void testFromXContent() throws IOException {
final SecureString enrollment_token = UUIDs.randomBase64UUIDSecureString();

final XContentType xContentType = randomFrom(XContentType.values());
final XContentBuilder builder = XContentFactory.contentBuilder(xContentType);
builder.startObject()
.field("enrollment_token", enrollment_token.toString());
builder.endObject();
BytesReference xContent = BytesReference.bytes(builder);

final CreateEnrollmentTokenResponse response = CreateEnrollmentTokenResponse.fromXContent(createParser(xContentType.xContent(),
xContent));
assertThat(response.getEnrollmentToken(), equalTo(enrollment_token.toString()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
--
:api: create-enrollment-token
:request: CreateEnrollmentTokenRequest
:response: CreateEnrollmentTokenResponse
--
[role="xpack"]
[id="{upid}-{api}"]
=== Create Enrollment Token API

Enrollment Tokens can be created using this API.

[id="{upid}-{api}-request"]
==== Create Enrollment Token Request

A +{request}+ contains no parameters.

["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests-file}[{api}-request]
--------------------------------------------------

include::../execution.asciidoc[]

[id="{upid}-{api}-response"]
==== Create Enrollment Token Response

The returned +{response}+ contains a string with the enrollment token with which user can enroll a new node
in an existing secured elasticsearch cluster, or a client can configure itself to
communicate with a secured elasticsearch cluster.

["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests-file}[{api}-response]
--------------------------------------------------
<1> The enrollment token contains the following information:
- IP Address and port number for the interface where the Elasticsearch node is listening for HTTP connections;
- The fingerprint of the CA certificate that is used to sign the certificate that the Elasticsearch node presents for TLS on the HTTP layer;
- An API key which allows a holder of the token to authenticate themself to the elasticsearch node;

as Base64 encoded string.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"security.create_enrollment_token":{
"documentation":{
"url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/security-create-enrollment-token.html",
"description":"Create an enrollment token to allow a new node to enroll in an existing secured elasticsearch cluster, or a client to configure itself to communicate with a secured elasticsearch cluster."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the live discussion, we can remove the "enroll client" bit here for now.

},
"stability":"stable",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be "experimental"?

"visibility":"public",
"headers":{
"accept": [ "application/json"],
"content_type": ["application/json"]
},
"url":{
"paths":[
{
"path":"/_security/enrollment_token",
"methods":[
"PUT",
"POST"
]
}
]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,8 @@ public void apply(Settings value, Settings current, Settings previous) {
FsHealthService.REFRESH_INTERVAL_SETTING,
FsHealthService.SLOW_PATH_LOGGING_THRESHOLD_SETTING,
IndexingPressure.MAX_INDEXING_BYTES,
ShardLimitValidator.SETTING_CLUSTER_MAX_SHARDS_PER_NODE_FROZEN);
ShardLimitValidator.SETTING_CLUSTER_MAX_SHARDS_PER_NODE_FROZEN,
Node.ENROLLMENT_ENABLED);

static List<SettingUpgrader<?>> BUILT_IN_SETTING_UPGRADERS = Collections.emptyList();

Expand Down
Loading