Skip to content

Commit

Permalink
Enroll Kibana API uses Service Accounts (#76370)
Browse files Browse the repository at this point in the history
This commit changes the Enroll Kibana API to create and return
a token for this service account, instead of setting and returning the
password of the kibana_system built-in user. Both the token name and
value are returned in the response of the API.
  • Loading branch information
jkakavas authored Aug 17, 2021
1 parent 031d9bb commit a596848
Show file tree
Hide file tree
Showing 17 changed files with 434 additions and 291 deletions.
8 changes: 8 additions & 0 deletions client/rest-high-level/qa/ssl-enabled/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
* Side Public License, v 1.
*/

/*
* We need this separate project as tests related to the enrollment process require
* test clusters with a specific TLS setup which is also not FIPS 140-2 compliant
* (as it uses PKCS#12 keystores). In order to not disable the entire rest-high-level
* project when running in fips mode, we moved enrollment tests in this subproject.
*
*/

import org.elasticsearch.gradle.internal.test.RestIntegTestTask
import org.elasticsearch.gradle.internal.info.BuildParams

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.startsWith;

public class EnrollmentIT extends ESRestHighLevelClientTestCase {
private static Path httpTrustStore;
Expand Down Expand Up @@ -76,7 +77,7 @@ public void testEnrollKibana() throws Exception {
assertThat(kibanaResponse, notNullValue());
assertThat(kibanaResponse.getHttpCa()
, endsWith("brcNC5xq6YE7C4/06nH7F6le4kE4Uo6c9fpkl4ehOxQxndNLn462tFF+8VBA8IftJ1PPWzqGxLsCTzM6p6w8sa+XhgNYglLfkRjirc="));
assertNotNull(kibanaResponse.getPassword());
assertThat(kibanaResponse.getPassword().toString().length(), equalTo(14));
assertNotNull(kibanaResponse.getTokenValue());
assertNotNull(kibanaResponse.getTokenName(), startsWith("enroll-process-token-"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* 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.documentation;

import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.LatchedActionListener;
import org.elasticsearch.client.ESRestHighLevelClientTestCase;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.security.KibanaEnrollmentResponse;
import org.elasticsearch.client.security.NodeEnrollmentResponse;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.core.PathUtils;
import org.junit.BeforeClass;

import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import static org.hamcrest.Matchers.startsWith;

public class EnrollmentDocumentationIT extends ESRestHighLevelClientTestCase {
static Path HTTP_TRUSTSTORE;

@BeforeClass
public static void getResources() throws Exception {
HTTP_TRUSTSTORE = PathUtils.get(EnrollmentDocumentationIT.class.getResource("/httpCa.p12").toURI());
}

@Override
protected String getProtocol() {
return "https";
}

@Override
protected Settings restClientSettings() {
String token = basicAuthHeaderValue("admin_user", new SecureString("admin-password".toCharArray()));

return Settings.builder()
.put(ThreadContext.PREFIX + ".Authorization", token)
.put(TRUSTSTORE_PATH, HTTP_TRUSTSTORE)
.put(TRUSTSTORE_PASSWORD, "password")
.build();
}

public void testNodeEnrollment() throws Exception {
RestHighLevelClient client = highLevelClient();

{
// tag::node-enrollment-execute
NodeEnrollmentResponse response = client.security().enrollNode(RequestOptions.DEFAULT);
// end::node-enrollment-execute

// tag::node-enrollment-response
String httpCaKey = response.getHttpCaKey(); // <1>
String httpCaCert = response.getHttpCaCert(); // <2>
String transportKey = response.getTransportKey(); // <3>
String transportCert = response.getTransportCert(); // <4>
List<String> nodesAddresses = response.getNodesAddresses(); // <5>
// end::node-enrollment-response
}

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

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

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

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

public void testKibanaEnrollment() throws Exception {
RestHighLevelClient client = highLevelClient();

{
// tag::kibana-enrollment-execute
KibanaEnrollmentResponse response = client.security().enrollKibana(RequestOptions.DEFAULT);
// end::kibana-enrollment-execute

// tag::kibana-enrollment-response
String tokenName = response.getTokenName(); // <1>
SecureString tokenValue = response.getTokenValue(); // <2>
String httoCa = response.getHttpCa(); // <3>
// end::kibana-enrollment-response
assertNotNull(tokenValue);
assertThat(tokenName, startsWith("enroll-process-token-"));
}

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

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

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

// tag::kibana-enrollment-execute-async
client.security().enrollKibanaAsync(RequestOptions.DEFAULT, listener);
// end::kibana-enrollment-execute-async
assertTrue(latch.await(30L, TimeUnit.SECONDS));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,51 @@
import java.io.IOException;
import java.util.Objects;

import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;

public final class KibanaEnrollmentResponse {

private SecureString password;
private String tokenName;
private SecureString tokenValue;
private String httpCa;

public KibanaEnrollmentResponse(SecureString password, String httpCa) {
this.password = password;
public KibanaEnrollmentResponse(String tokenName, SecureString tokenValue, String httpCa) {
this.tokenName = tokenName;
this.tokenValue = tokenValue;
this.httpCa = httpCa;
}

public SecureString getPassword() { return password; }
public String getTokenName() { return tokenName; }

public SecureString getTokenValue() { return tokenValue; }

public String getHttpCa() {
return httpCa;
}

private static final ParseField PASSWORD = new ParseField("password");
private static final ParseField TOKEN = new ParseField("token");
private static final ParseField TOKEN_NAME = new ParseField("name");
private static final ParseField TOKEN_VALUE = new ParseField("value");
private static final ParseField HTTP_CA = new ParseField("http_ca");

@SuppressWarnings("unchecked")
static final ConstructingObjectParser<Token, Void> TOKEN_PARSER = new ConstructingObjectParser<>(
KibanaEnrollmentResponse.class.getName(), true,
a -> new Token((String) a[0], (String) a[1])
);

private static final ConstructingObjectParser<KibanaEnrollmentResponse, Void> PARSER =
new ConstructingObjectParser<>(
KibanaEnrollmentResponse.class.getName(), true,
a -> new KibanaEnrollmentResponse(new SecureString(((String) a[0]).toCharArray()), (String) a[1]));
a -> {
final Token token = (Token) a[0];
return new KibanaEnrollmentResponse(token.name, new SecureString(token.value.toCharArray()), (String) a[1]);
});

static {
PARSER.declareString(ConstructingObjectParser.constructorArg(), PASSWORD);
PARSER.declareString(ConstructingObjectParser.constructorArg(), HTTP_CA);
TOKEN_PARSER.declareString(constructorArg(), TOKEN_NAME);
TOKEN_PARSER.declareString(constructorArg(), TOKEN_VALUE);
PARSER.declareObject(constructorArg(), TOKEN_PARSER, TOKEN);
PARSER.declareString(constructorArg(), HTTP_CA);
}

public static KibanaEnrollmentResponse fromXContent(XContentParser parser) throws IOException {
Expand All @@ -54,10 +71,20 @@ public static KibanaEnrollmentResponse fromXContent(XContentParser parser) throw
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
KibanaEnrollmentResponse that = (KibanaEnrollmentResponse) o;
return password.equals(that.password) && httpCa.equals(that.httpCa);
return tokenName.equals(that.tokenName) && tokenValue.equals(that.tokenValue) && httpCa.equals(that.httpCa);
}

@Override public int hashCode() {
return Objects.hash(password, httpCa);
return Objects.hash(tokenName, tokenValue, httpCa);
}

private static class Token {
private final String name;
private final String value;

Token(String name, String value) {
this.name = name;
this.value = value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@
import org.elasticsearch.client.security.InvalidateApiKeyResponse;
import org.elasticsearch.client.security.InvalidateTokenRequest;
import org.elasticsearch.client.security.InvalidateTokenResponse;
import org.elasticsearch.client.security.NodeEnrollmentResponse;
import org.elasticsearch.client.security.PutPrivilegesRequest;
import org.elasticsearch.client.security.PutPrivilegesResponse;
import org.elasticsearch.client.security.PutRoleMappingRequest;
Expand Down Expand Up @@ -103,7 +102,6 @@
import org.elasticsearch.client.security.user.privileges.Role.ClusterPrivilegeName;
import org.elasticsearch.client.security.user.privileges.Role.IndexPrivilegeName;
import org.elasticsearch.client.security.user.privileges.UserIndicesPrivileges;
import org.elasticsearch.client.security.KibanaEnrollmentResponse;
import org.elasticsearch.core.CheckedConsumer;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.SecureString;
Expand Down Expand Up @@ -3008,90 +3006,6 @@ public void onFailure(Exception e) {
}
}

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

{
// tag::node-enrollment-execute
NodeEnrollmentResponse response = client.security().enrollNode(RequestOptions.DEFAULT);
// end::node-enrollment-execute

// tag::node-enrollment-response
String httpCaKey = response.getHttpCaKey(); // <1>
String httpCaCert = response.getHttpCaCert(); // <2>
String transportKey = response.getTransportKey(); // <3>
String transportCert = response.getTransportCert(); // <4>
List<String> nodesAddresses = response.getNodesAddresses(); // <5>
// end::node-enrollment-response
}

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

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

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

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

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

{
// tag::kibana-enrollment-execute
KibanaEnrollmentResponse response = client.security().enrollKibana(RequestOptions.DEFAULT);
// end::kibana-enrollment-execute

// tag::kibana-enrollment-response
SecureString password = response.getPassword(); // <1>
String httoCa = response.getHttpCa(); // <2>
// end::kibana-enrollment-response
assertThat(password.length(), equalTo(14));
}

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

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

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

// tag::kibana-enrollment-execute-async
client.security().enrollKibanaAsync(RequestOptions.DEFAULT, listener);
// end::kibana-enrollment-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
Loading

0 comments on commit a596848

Please sign in to comment.