diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetElasticPasswordTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetElasticPasswordTool.java index 0e43611c3ea63..819f7c0a4cf77 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetElasticPasswordTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetElasticPasswordTool.java @@ -31,6 +31,8 @@ import java.util.List; import java.util.function.Function; +import static org.elasticsearch.xpack.security.tool.CommandLineHttpClient.createURL; + public class ResetElasticPasswordTool extends BaseRunAsSuperuserCommand { private final Function clientFunction; @@ -92,7 +94,7 @@ protected void executeCommand(Terminal terminal, OptionSet options, Environment username, password, () -> requestBodySupplier(elasticPassword), - this::responseBuilder + CommandLineHttpClient::responseBuilder ); final int responseStatus = httpResponse.getHttpStatus(); if (httpResponse.getHttpStatus() != HttpURLConnection.HTTP_OK) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java index 9ccaa1c537777..e5e4ebdfee7a0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java @@ -418,7 +418,7 @@ void checkClusterHealth(Terminal terminal) throws Exception { terminal.errorPrintln("Failed to determine the health of the cluster running at " + url); terminal.errorPrintln("Unexpected response code [" + httpResponse.getHttpStatus() + "] from calling GET " + route.toString()); - final String cause = getErrorCause(httpResponse); + final String cause = CommandLineHttpClient.getErrorCause(httpResponse); if (cause != null) { terminal.errorPrintln("Cause: " + cause); } @@ -477,7 +477,7 @@ private void changeUserPassword(String user, SecureString password, Terminal ter terminal.errorPrintln(""); terminal.errorPrintln( "Unexpected response code [" + httpResponse.getHttpStatus() + "] from calling PUT " + route.toString()); - String cause = getErrorCause(httpResponse); + String cause = CommandLineHttpClient.getErrorCause(httpResponse); if (cause != null) { terminal.errorPrintln("Cause: " + cause); terminal.errorPrintln(""); @@ -564,32 +564,6 @@ private URL createURL(URL url, String path, String query) throws MalformedURLExc } } - private String getErrorCause(HttpResponse httpResponse) { - final Object error = httpResponse.getResponseBody().get("error"); - if (error == null) { - return null; - } - if (error instanceof Map) { - Object reason = ((Map) error).get("reason"); - if (reason != null) { - return reason.toString(); - } - final Object root = ((Map) error).get("root_cause"); - if (root != null && root instanceof Map) { - reason = ((Map) root).get("reason"); - if (reason != null) { - return reason.toString(); - } - final Object type = ((Map) root).get("type"); - if (type != null) { - return (String) type; - } - } - return String.valueOf(((Map) error).get("type")); - } - return error.toString(); - } - private byte[] toByteArray(InputStream is) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] internalBuffer = new byte[1024]; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/EnrollmentToken.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/EnrollmentToken.java new file mode 100644 index 0000000000000..c62fcaeb01b69 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/EnrollmentToken.java @@ -0,0 +1,64 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.enrollment; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.json.JsonXContent; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Objects; + +public class EnrollmentToken { + private final String apiKey; + private final String fingerprint; + private final String version; + private final List boundAddress; + + public String getApiKey() { return apiKey; } + public String getFingerprint() { return fingerprint; } + public String getVersion() { return version; } + public List getBoundAddress() { return boundAddress; } + + /** + * Create an EnrollmentToken + * + * @param apiKey API Key credential in the form apiKeyId:ApiKeySecret to be used for enroll calls + * @param fingerprint hex encoded SHA256 fingerprint of the HTTP CA cert + * @param version node version number + * @param boundAddress IP Addresses and port numbers for the interfaces where the Elasticsearch node is listening on + */ + public EnrollmentToken(String apiKey, String fingerprint, String version, List boundAddress) { + this.apiKey = Objects.requireNonNull(apiKey); + this.fingerprint = Objects.requireNonNull(fingerprint); + this.version = Objects.requireNonNull(version); + this.boundAddress = Objects.requireNonNull(boundAddress); + } + + public String getRaw() throws Exception { + final XContentBuilder builder = JsonXContent.contentBuilder(); + builder.startObject(); + builder.field("ver", version); + builder.startArray("adr"); + for (String bound_address : boundAddress) { + builder.value(bound_address); + } + builder.endArray(); + builder.field("fgr", fingerprint); + builder.field("key", apiKey); + builder.endObject(); + return Strings.toString(builder); + } + + public String getEncoded() throws Exception { + final String jsonString = getRaw(); + return Base64.getUrlEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/CreateEnrollmentToken.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenGenerator.java similarity index 86% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/CreateEnrollmentToken.java rename to x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenGenerator.java index e046f71be09d6..71bff4e88c4e7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/CreateEnrollmentToken.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenGenerator.java @@ -36,71 +36,51 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.nio.charset.StandardCharsets; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.ArrayList; -import java.util.Base64; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; -public class CreateEnrollmentToken { +public class EnrollmentTokenGenerator { protected static final String ENROLL_API_KEY_EXPIRATION = "30m"; - private static final Logger logger = LogManager.getLogger(CreateEnrollmentToken.class); + private static final Logger logger = LogManager.getLogger(EnrollmentTokenGenerator.class); private final Environment environment; private final SSLService sslService; private final CommandLineHttpClient client; private final URL defaultUrl; - public CreateEnrollmentToken(Environment environment) throws MalformedURLException { + public EnrollmentTokenGenerator(Environment environment) throws MalformedURLException { this(environment, new CommandLineHttpClient(environment)); } // protected for testing - protected CreateEnrollmentToken(Environment environment, CommandLineHttpClient client) throws MalformedURLException { + protected EnrollmentTokenGenerator(Environment environment, CommandLineHttpClient client) throws MalformedURLException { this.environment = environment; this.sslService = new SSLService(environment); this.client = client; this.defaultUrl = new URL(client.getDefaultURL()); } - public String createNodeEnrollmentToken(String user, SecureString password) throws Exception { + public EnrollmentToken createNodeEnrollmentToken(String user, SecureString password) throws Exception { return this.create(user, password, NodeEnrollmentAction.NAME); } - public String createKibanaEnrollmentToken(String user, SecureString password) throws Exception { + public EnrollmentToken createKibanaEnrollmentToken(String user, SecureString password) throws Exception { return this.create(user, password, KibanaEnrollmentAction.NAME); } - protected String create(String user, SecureString password, String action) throws Exception { + protected EnrollmentToken create(String user, SecureString password, String action) throws Exception { if (XPackSettings.ENROLLMENT_ENABLED.get(environment.settings()) != true) { throw new IllegalStateException("[xpack.security.enrollment.enabled] must be set to `true` to create an enrollment token"); } final String fingerprint = getCaFingerprint(); final String apiKey = getApiKeyCredentials(user, password, action); final Tuple, String> httpInfo = getNodeInfo(user, password); - - try { - final XContentBuilder builder = JsonXContent.contentBuilder(); - builder.startObject(); - builder.field("ver", httpInfo.v2()); - builder.startArray("adr"); - for (String bound_address : httpInfo.v1()) { - builder.value(bound_address); - } - builder.endArray(); - builder.field("fgr", fingerprint); - builder.field("key", apiKey); - builder.endObject(); - final String jsonString = Strings.toString(builder); - return Base64.getUrlEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8)); - } catch (Exception e) { - logger.error(("Error generating enrollment token"), e); - throw new IllegalStateException("Error generating enrollment token: " + e.getMessage()); - } + return new EnrollmentToken(apiKey, fingerprint, httpInfo.v2(), httpInfo.v1()); } private HttpResponse.HttpResponseBuilder responseBuilder(InputStream is) throws IOException { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/BootstrapPasswordAndEnrollmentTokenForInitialNode.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/BootstrapPasswordAndEnrollmentTokenForInitialNode.java new file mode 100644 index 0000000000000..1aba20f3d1d74 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/BootstrapPasswordAndEnrollmentTokenForInitialNode.java @@ -0,0 +1,154 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.enrollment.tool; + +import joptsimple.OptionSet; +import joptsimple.OptionSpec; + +import org.elasticsearch.cli.ExitCodes; +import org.elasticsearch.cli.KeyStoreAwareCommand; +import org.elasticsearch.cli.Terminal; +import org.elasticsearch.cli.UserException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.KeyStoreWrapper; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.core.CheckedFunction; +import org.elasticsearch.env.Environment; +import org.elasticsearch.xpack.core.security.user.ElasticUser; +import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; +import org.elasticsearch.xpack.security.enrollment.EnrollmentToken; +import org.elasticsearch.xpack.security.enrollment.EnrollmentTokenGenerator; +import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; +import org.elasticsearch.xpack.security.tool.HttpResponse; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.security.SecureRandom; +import java.util.function.Function; + +import static org.elasticsearch.xpack.security.tool.CommandLineHttpClient.createURL; + +public class BootstrapPasswordAndEnrollmentTokenForInitialNode extends KeyStoreAwareCommand { + private final CheckedFunction createEnrollmentTokenFunction; + private final Function clientFunction; + private final CheckedFunction keyStoreFunction; + private final OptionSpec includeNodeEnrollmentToken; + private final SecureRandom secureRandom = new SecureRandom(); + + BootstrapPasswordAndEnrollmentTokenForInitialNode() { + this( + environment -> new CommandLineHttpClient(environment), + environment -> KeyStoreWrapper.load(environment.configFile()), + environment -> new EnrollmentTokenGenerator(environment) + ); + } + + BootstrapPasswordAndEnrollmentTokenForInitialNode(Function clientFunction, + CheckedFunction keyStoreFunction, + CheckedFunction + createEnrollmentTokenFunction){ + super("Set elastic password and generate enrollment token for initial node"); + this.clientFunction = clientFunction; + this.keyStoreFunction = keyStoreFunction; + this.createEnrollmentTokenFunction = createEnrollmentTokenFunction; + includeNodeEnrollmentToken = parser.accepts("include-node-enrollment-token", "determine that we have to generate " + + "a node enrollment token"); + } + + public static void main(String[] args) throws Exception { + exit(new BootstrapPasswordAndEnrollmentTokenForInitialNode().main(args, Terminal.DEFAULT)); + } + + @Override + protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception { + final SecureString keystorePassword; + try { + keystorePassword = new SecureString(terminal.readSecret("")); + } catch (Exception e) { + throw new UserException(ExitCodes.USAGE, null); + } + + final Environment secureEnvironment = readSecureSettings(env, keystorePassword); + final CommandLineHttpClient client = clientFunction.apply(secureEnvironment); + final EnrollmentTokenGenerator enrollmentTokenGenerator = createEnrollmentTokenFunction.apply(secureEnvironment); + final SecureString bootstrapPassword = ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.get(secureEnvironment.settings()); + try { + String output; + client.checkClusterHealthWithRetriesWaitingForCluster(ElasticUser.NAME, bootstrapPassword, 15); + final EnrollmentToken kibanaToken = enrollmentTokenGenerator.createKibanaEnrollmentToken(ElasticUser.NAME, bootstrapPassword); + output = "Kibana enrollment token: " + kibanaToken.getEncoded() + System.lineSeparator(); + output += "CA fingerprint: " + kibanaToken.getFingerprint() + System.lineSeparator(); + if (options.has(includeNodeEnrollmentToken)) { + final EnrollmentToken nodeToken = enrollmentTokenGenerator.createNodeEnrollmentToken(ElasticUser.NAME, bootstrapPassword); + output += "Node enrollment token: " + nodeToken.getEncoded() + System.lineSeparator(); + } + if (ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.exists(secureEnvironment.settings()) == false) { + output += "elastic user password: " + setElasticUserPassword(client, bootstrapPassword); + } + terminal.println(output); + } catch (Exception e) { + throw new UserException(ExitCodes.UNAVAILABLE, null); + } + } + + protected SecureString setElasticUserPassword(CommandLineHttpClient client, SecureString bootstrapPassword) throws Exception { + final URL passwordSetUrl = setElasticUserPasswordUrl(client); + final HttpResponse response; + SecureString password = new SecureString(generatePassword(20)); + try { + response = client.execute("POST", passwordSetUrl, ElasticUser.NAME, bootstrapPassword, + () -> { + XContentBuilder xContentBuilder = JsonXContent.contentBuilder(); + xContentBuilder.startObject().field("password", password.toString()).endObject(); + return Strings.toString(xContentBuilder); + }, CommandLineHttpClient::responseBuilder); + if (response.getHttpStatus() != HttpURLConnection.HTTP_OK) { + throw new UserException(ExitCodes.UNAVAILABLE, null); + } + } catch (IOException e) { + throw new UserException(ExitCodes.IO_ERROR, null); + } + return password; + } + + Environment readSecureSettings(Environment env, SecureString keystorePassword) throws Exception { + final KeyStoreWrapper keyStoreWrapper = keyStoreFunction.apply(env); + keyStoreWrapper.decrypt(keystorePassword.getChars()); + Settings.Builder settingsBuilder = Settings.builder(); + settingsBuilder.put(env.settings(), true); + if (settingsBuilder.getSecureSettings() == null) { + settingsBuilder.setSecureSettings(keyStoreWrapper); + } + final Settings settings = settingsBuilder.build(); + return new Environment(settings, env.configFile()); + } + + public static URL checkClusterHealthUrl(CommandLineHttpClient client) throws MalformedURLException, URISyntaxException { + return createURL(new URL(client.getDefaultURL()), "_cluster/health", "?pretty"); + } + + public static URL setElasticUserPasswordUrl(CommandLineHttpClient client) throws MalformedURLException, URISyntaxException { + return createURL(new URL(client.getDefaultURL()), "/_security/user/" + ElasticUser.NAME + "/_password", + "?pretty"); + } + + protected char[] generatePassword(int passwordLength) { + final char[] passwordChars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&*-_=+?").toCharArray(); + char[] characters = new char[passwordLength]; + for (int i = 0; i < passwordLength; ++i) { + characters[i] = passwordChars[secureRandom.nextInt(passwordChars.length)]; + } + return characters; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenTool.java index b01b550367f92..34495e3a2b59c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenTool.java @@ -18,7 +18,7 @@ import org.elasticsearch.core.CheckedFunction; import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.core.XPackSettings; -import org.elasticsearch.xpack.security.enrollment.CreateEnrollmentToken; +import org.elasticsearch.xpack.security.enrollment.EnrollmentTokenGenerator; import org.elasticsearch.xpack.security.tool.BaseRunAsSuperuserCommand; import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; @@ -28,21 +28,21 @@ public class CreateEnrollmentTokenTool extends BaseRunAsSuperuserCommand { private final OptionSpec scope; - private final CheckedFunction createEnrollmentTokenFunction; + private final CheckedFunction createEnrollmentTokenFunction; static final List ALLOWED_SCOPES = List.of("node", "kibana"); CreateEnrollmentTokenTool() { this( environment -> new CommandLineHttpClient(environment), environment -> KeyStoreWrapper.load(environment.configFile()), - environment -> new CreateEnrollmentToken(environment) + environment -> new EnrollmentTokenGenerator(environment) ); } CreateEnrollmentTokenTool( Function clientFunction, CheckedFunction keyStoreFunction, - CheckedFunction createEnrollmentTokenFunction + CheckedFunction createEnrollmentTokenFunction ) { super(clientFunction, keyStoreFunction, "Creates enrollment tokens for elasticsearch nodes and kibana instances"); this.createEnrollmentTokenFunction = createEnrollmentTokenFunction; @@ -75,11 +75,11 @@ protected void executeCommand(Terminal terminal, OptionSet options, Environment throws Exception { final String tokenScope = scope.value(options); try { - CreateEnrollmentToken createEnrollmentTokenService = createEnrollmentTokenFunction.apply(env); + EnrollmentTokenGenerator enrollmentTokenGenerator = createEnrollmentTokenFunction.apply(env); if (tokenScope.equals("node")) { - terminal.println(createEnrollmentTokenService.createNodeEnrollmentToken(username, password)); + terminal.println(enrollmentTokenGenerator.createNodeEnrollmentToken(username, password).getEncoded()); } else { - terminal.println(createEnrollmentTokenService.createKibanaEnrollmentToken(username, password)); + terminal.println(enrollmentTokenGenerator.createKibanaEnrollmentToken(username, password).getEncoded()); } } catch (Exception e) { terminal.errorPrintln("Unable to create enrollment token for scope [" + tokenScope + "]"); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/BaseRunAsSuperuserCommand.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/BaseRunAsSuperuserCommand.java index a7bd629aa260b..def4cc047cec4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/BaseRunAsSuperuserCommand.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/BaseRunAsSuperuserCommand.java @@ -15,7 +15,6 @@ import org.elasticsearch.cli.KeyStoreAwareCommand; import org.elasticsearch.cli.Terminal; import org.elasticsearch.cli.UserException; -import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.settings.KeyStoreWrapper; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; @@ -30,11 +29,7 @@ import org.elasticsearch.xpack.security.authc.file.FileUserRolesStore; import org.elasticsearch.xpack.security.support.FileAttributesChecker; -import java.io.IOException; -import java.io.InputStream; import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Path; import java.security.SecureRandom; @@ -195,10 +190,10 @@ private void ensureFileRealmEnabled(Settings settings) throws Exception { private void checkClusterHealthWithRetries(Environment env, Terminal terminal, String username, SecureString password, int retries, boolean force) throws Exception { CommandLineHttpClient client = clientFunction.apply(env); - final URL clusterHealthUrl = createURL(new URL(client.getDefaultURL()), "_cluster/health", "?pretty"); + final URL clusterHealthUrl = CommandLineHttpClient.createURL(new URL(client.getDefaultURL()), "_cluster/health", "?pretty"); final HttpResponse response; try { - response = client.execute("GET", clusterHealthUrl, username, password, () -> null, this::responseBuilder); + response = client.execute("GET", clusterHealthUrl, username, password, () -> null, CommandLineHttpClient::responseBuilder); } catch (Exception e) { throw new UserException(ExitCodes.UNAVAILABLE, "Failed to determine the health of the cluster. ", e); } @@ -249,14 +244,7 @@ private void checkClusterHealthWithRetries(Environment env, Terminal terminal, S } } - protected HttpResponse.HttpResponseBuilder responseBuilder(InputStream is) throws IOException { - final HttpResponse.HttpResponseBuilder httpResponseBuilder = new HttpResponse.HttpResponseBuilder(); - final String responseBody = Streams.readFully(is).utf8ToString(); - httpResponseBuilder.withResponseBody(responseBody); - return httpResponseBuilder; - } - - protected char[] generatePassword(int passwordLength) { + protected char[] generatePassword(int passwordLength) { final char[] passwordChars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&*-_=+?").toCharArray(); char[] characters = new char[passwordLength]; for (int i = 0; i < passwordLength; ++i) { @@ -275,10 +263,6 @@ private String generateUsername() { return "enrollment_autogenerated_" + new String(characters); } - protected URL createURL(URL url, String path, String query) throws MalformedURLException, URISyntaxException { - return new URL(url, (url.toURI().getPath() + path).replaceAll("/+", "/") + query); - } - /** * This is called after we have created a temporary superuser in the file realm and verified that its * credentials work. The username and password of the generated user are passed as parameters. Overriding methods should diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/CommandLineHttpClient.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/CommandLineHttpClient.java index c9960da6eaba3..1f2156ea42a62 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/CommandLineHttpClient.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/CommandLineHttpClient.java @@ -6,6 +6,7 @@ */ package org.elasticsearch.xpack.security.tool; +import org.elasticsearch.common.io.Streams; import org.elasticsearch.core.CheckedFunction; import org.elasticsearch.common.CheckedSupplier; import org.elasticsearch.common.Strings; @@ -30,12 +31,16 @@ import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Objects; import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_PORT; import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_PUBLISH_HOST; @@ -116,7 +121,7 @@ public HttpResponse execute(String method, URL url, String user, SecureString pa } // this throws IOException if there is a network problem final int responseCode = conn.getResponseCode(); - HttpResponseBuilder responseBuilder = null; + HttpResponseBuilder responseBuilder; try (InputStream inputStream = conn.getInputStream()) { responseBuilder = responseHandler.apply(inputStream); } catch (IOException e) { @@ -162,4 +167,90 @@ public String getDefaultURL() { "provide the url", e); } } + + public static String getErrorCause(HttpResponse httpResponse) { + final Object error = httpResponse.getResponseBody().get("error"); + if (error == null) { + return null; + } + if (error instanceof Map) { + Object reason = ((Map) error).get("reason"); + if (reason != null) { + return reason.toString(); + } + final Object root = ((Map) error).get("root_cause"); + if (root != null && root instanceof Map) { + reason = ((Map) root).get("reason"); + if (reason != null) { + return reason.toString(); + } + final Object type = ((Map) root).get("type"); + if (type != null) { + return (String) type; + } + } + return String.valueOf(((Map) error).get("type")); + } + return error.toString(); + } + + /** + * If cluster is not up yet (connection refused or master is unavailable), we will retry @retries number of times + * If status is 'Red', we will wait for 'Yellow' for 30s (default timeout) + */ + public void checkClusterHealthWithRetriesWaitingForCluster(String username, SecureString password, int retries) + throws Exception { + final URL clusterHealthUrl = createURL(new URL(getDefaultURL()), "_cluster/health", "?wait_for_status=yellow&pretty"); + HttpResponse response; + try { + response = execute("GET", clusterHealthUrl, username, password, () -> null, CommandLineHttpClient::responseBuilder); + } catch (Exception e) { + if (retries > 0) { + Thread.sleep(1000); + retries -= 1; + checkClusterHealthWithRetriesWaitingForCluster(username, password, retries); + return; + } else { + throw new IllegalStateException("Failed to determine the health of the cluster. ", e); + } + } + final int responseStatus = response.getHttpStatus(); + if (responseStatus != HttpURLConnection.HTTP_OK) { + if (responseStatus != HttpURLConnection.HTTP_UNAVAILABLE) { + if (retries > 0) { + Thread.sleep(1000); + retries -= 1; + checkClusterHealthWithRetriesWaitingForCluster(username, password, retries); + return; + } else { + throw new IllegalStateException("Failed to determine the health of the cluster. Unexpected http status [" + + responseStatus + "]"); + } + } + throw new IllegalStateException("Failed to determine the health of the cluster. Unexpected http status [" + + responseStatus + "]"); + } else { + final String clusterStatus = Objects.toString(response.getResponseBody().get("status"), ""); + if (clusterStatus.isEmpty()) { + throw new IllegalStateException( + "Failed to determine the health of the cluster. Cluster health API did not return a status value." + ); + } else if ("red".equalsIgnoreCase(clusterStatus)) { + throw new IllegalStateException( + "Failed to determine the health of the cluster. Cluster health is currently RED."); + } + // else it is yellow or green so we can continue + } + } + + public static HttpResponse.HttpResponseBuilder responseBuilder(InputStream is) throws IOException { + final HttpResponse.HttpResponseBuilder httpResponseBuilder = new HttpResponse.HttpResponseBuilder(); + final String responseBody = Streams.readFully(is).utf8ToString(); + httpResponseBuilder.withResponseBody(responseBody); + return httpResponseBuilder; + } + + public static URL createURL(URL url, String path, String query) throws MalformedURLException, URISyntaxException { + return new URL(url, (url.toURI().getPath() + path).replaceAll("/+", "/") + query); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/CreateEnrollmentTokenTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenGeneratorTests.java similarity index 87% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/CreateEnrollmentTokenTests.java rename to x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenGeneratorTests.java index db42dfd72287e..93a68febd86c3 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/CreateEnrollmentTokenTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenGeneratorTests.java @@ -41,14 +41,14 @@ import static org.elasticsearch.test.CheckedFunctionUtils.anyCheckedFunction; import static org.elasticsearch.test.CheckedFunctionUtils.anyCheckedSupplier; -import static org.elasticsearch.xpack.security.enrollment.CreateEnrollmentToken.getFilteredAddresses; +import static org.elasticsearch.xpack.security.enrollment.EnrollmentTokenGenerator.getFilteredAddresses; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class CreateEnrollmentTokenTests extends ESTestCase { +public class EnrollmentTokenGeneratorTests extends ESTestCase { private Environment environment; @BeforeClass @@ -82,9 +82,9 @@ public void setupMocks() throws Exception { public void testCreateSuccess() throws Exception { final CommandLineHttpClient client = mock(CommandLineHttpClient.class); when(client.getDefaultURL()).thenReturn("http://localhost:9200"); - final CreateEnrollmentToken createEnrollmentToken = new CreateEnrollmentToken(environment, client); - final URL createAPIKeyURL = createEnrollmentToken.createAPIKeyUrl(); - final URL getHttpInfoURL = createEnrollmentToken.getHttpInfoUrl(); + final EnrollmentTokenGenerator enrollmentTokenGenerator = new EnrollmentTokenGenerator(environment, client); + final URL createAPIKeyURL = enrollmentTokenGenerator.createAPIKeyUrl(); + final URL getHttpInfoURL = enrollmentTokenGenerator.getHttpInfoUrl(); final HttpResponse httpResponseOK = new HttpResponse(HttpURLConnection.HTTP_OK, new HashMap<>()); when(client.execute(anyString(), any(URL.class), anyString(), any(SecureString.class), anyCheckedSupplier(), @@ -129,7 +129,8 @@ public void testCreateSuccess() throws Exception { anyCheckedSupplier(), anyCheckedFunction())) .thenReturn(createHttpResponse(HttpURLConnection.HTTP_OK, getHttpInfoResponseBody)); - final String tokenNode = createEnrollmentToken.createNodeEnrollmentToken("elastic", new SecureString("elastic")); + final String tokenNode = enrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray())) + .getEncoded(); Map infoNode = getDecoded(tokenNode); assertEquals("8.0.0", infoNode.get("ver")); @@ -137,7 +138,8 @@ public void testCreateSuccess() throws Exception { assertEquals("ce480d53728605674fcfd8ffb51000d8a33bf32de7c7f1e26b4d428f8a91362d", infoNode.get("fgr")); assertEquals("DR6CzXkBDf8amV_48yYX:x3YqU_rqQwm-ESrkExcnOg", infoNode.get("key")); - final String tokenKibana = createEnrollmentToken.createNodeEnrollmentToken("elastic", new SecureString("elastic")); + final String tokenKibana = enrollmentTokenGenerator.createKibanaEnrollmentToken("elastic", + new SecureString("elastic".toCharArray())).getEncoded(); Map infoKibana = getDecoded(tokenKibana); assertEquals("8.0.0", infoKibana.get("ver")); @@ -149,24 +151,24 @@ public void testCreateSuccess() throws Exception { public void testFailedCreateApiKey() throws Exception { final CommandLineHttpClient client = mock(CommandLineHttpClient.class); when(client.getDefaultURL()).thenReturn("http://localhost:9200"); - final CreateEnrollmentToken createEnrollmentToken = new CreateEnrollmentToken(environment, client); - final URL createAPIKeyURL = createEnrollmentToken.createAPIKeyUrl(); + final EnrollmentTokenGenerator enrollmentTokenGenerator = new EnrollmentTokenGenerator(environment, client); + final URL createAPIKeyURL = enrollmentTokenGenerator.createAPIKeyUrl(); final HttpResponse httpResponseNotOK = new HttpResponse(HttpURLConnection.HTTP_BAD_REQUEST, new HashMap<>()); when(client.execute(anyString(), eq(createAPIKeyURL), anyString(), any(SecureString.class), anyCheckedSupplier(), anyCheckedFunction())).thenReturn(httpResponseNotOK); IllegalStateException ex = expectThrows(IllegalStateException.class, () -> - createEnrollmentToken.createNodeEnrollmentToken("elastic", new SecureString("elastic"))); + enrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray())).getEncoded()); assertThat(ex.getMessage(), Matchers.containsString("Unexpected response code [400] from calling POST ")); } public void testFailedRetrieveHttpInfo() throws Exception { final CommandLineHttpClient client = mock(CommandLineHttpClient.class); when(client.getDefaultURL()).thenReturn("http://localhost:9200"); - final CreateEnrollmentToken createEnrollmentToken = new CreateEnrollmentToken(environment, client); - final URL createAPIKeyURL = createEnrollmentToken.createAPIKeyUrl(); - final URL getHttpInfoURL = createEnrollmentToken.getHttpInfoUrl(); + final EnrollmentTokenGenerator enrollmentTokenGenerator = new EnrollmentTokenGenerator(environment, client); + final URL createAPIKeyURL = enrollmentTokenGenerator.createAPIKeyUrl(); + final URL getHttpInfoURL = enrollmentTokenGenerator.getHttpInfoUrl(); final HttpResponse httpResponseOK = new HttpResponse(HttpURLConnection.HTTP_OK, new HashMap<>()); when(client.execute(anyString(), eq(createAPIKeyURL), anyString(), any(SecureString.class), anyCheckedSupplier(), @@ -191,7 +193,7 @@ public void testFailedRetrieveHttpInfo() throws Exception { anyCheckedFunction())).thenReturn(httpResponseNotOK); IllegalStateException ex = expectThrows(IllegalStateException.class, () -> - createEnrollmentToken.createNodeEnrollmentToken("elastic", new SecureString("elastic"))); + enrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray())).getEncoded()); assertThat(ex.getMessage(), Matchers.containsString("Unexpected response code [400] from calling GET ")); } @@ -216,9 +218,9 @@ public void testFailedNoCaInKeystore() throws Exception { environment = new Environment(settings, tempDir); final CommandLineHttpClient client = mock(CommandLineHttpClient.class); when(client.getDefaultURL()).thenReturn("http://localhost:9200"); - final CreateEnrollmentToken createEnrollmentToken = new CreateEnrollmentToken(environment, client); - final URL createAPIKeyURL = createEnrollmentToken.createAPIKeyUrl(); - final URL getHttpInfoURL = createEnrollmentToken.getHttpInfoUrl(); + final EnrollmentTokenGenerator enrollmentTokenGenerator = new EnrollmentTokenGenerator(environment, client); + final URL createAPIKeyURL = enrollmentTokenGenerator.createAPIKeyUrl(); + final URL getHttpInfoURL = enrollmentTokenGenerator.getHttpInfoUrl(); final HttpResponse httpResponseOK = new HttpResponse(HttpURLConnection.HTTP_OK, new HashMap<>()); when(client.execute(anyString(), eq(createAPIKeyURL), anyString(), any(SecureString.class), anyCheckedSupplier(), @@ -243,7 +245,7 @@ public void testFailedNoCaInKeystore() throws Exception { anyCheckedFunction())).thenReturn(httpResponseNotOK); IllegalStateException ex = expectThrows(IllegalStateException.class, () -> - createEnrollmentToken.createNodeEnrollmentToken("elastic", new SecureString("elastic"))); + enrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray())).getEncoded()); assertThat(ex.getMessage(), Matchers.equalTo("Unable to create an enrollment token. Elasticsearch node HTTP layer " + "SSL configuration Keystore doesn't contain any PrivateKey entries where the associated certificate is a CA certificate")); } @@ -269,9 +271,9 @@ public void testFailedManyCaInKeystore() throws Exception { environment = new Environment(settings, tempDir); final CommandLineHttpClient client = mock(CommandLineHttpClient.class); when(client.getDefaultURL()).thenReturn("http://localhost:9200"); - final CreateEnrollmentToken createEnrollmentToken = new CreateEnrollmentToken(environment, client); - final URL createAPIKeyURL = createEnrollmentToken.createAPIKeyUrl(); - final URL getHttpInfoURL = createEnrollmentToken.getHttpInfoUrl(); + final EnrollmentTokenGenerator enrollmentTokenGenerator = new EnrollmentTokenGenerator(environment, client); + final URL createAPIKeyURL = enrollmentTokenGenerator.createAPIKeyUrl(); + final URL getHttpInfoURL = enrollmentTokenGenerator.getHttpInfoUrl(); final HttpResponse httpResponseOK = new HttpResponse(HttpURLConnection.HTTP_OK, new HashMap<>()); when(client.execute(anyString(), eq(createAPIKeyURL), anyString(), any(SecureString.class), anyCheckedSupplier(), @@ -296,7 +298,7 @@ public void testFailedManyCaInKeystore() throws Exception { anyCheckedFunction())).thenReturn(httpResponseNotOK); IllegalStateException ex = expectThrows(IllegalStateException.class, () -> - createEnrollmentToken.createNodeEnrollmentToken("elastic", new SecureString("elastic"))); + enrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray())).getEncoded()); assertThat(ex.getMessage(), Matchers.equalTo("Unable to create an enrollment token. Elasticsearch node HTTP layer SSL " + "configuration Keystore contains multiple PrivateKey entries where the associated certificate is a CA certificate")); } @@ -311,10 +313,10 @@ public void testNoKeyStore() throws Exception { final Environment environment_no_keystore = new Environment(settings, tempDir); final CommandLineHttpClient client = mock(CommandLineHttpClient.class); when(client.getDefaultURL()).thenReturn("http://localhost:9200"); - final CreateEnrollmentToken createEnrollmentToken = new CreateEnrollmentToken(environment_no_keystore, client); + final EnrollmentTokenGenerator enrollmentTokenGenerator = new EnrollmentTokenGenerator(environment_no_keystore, client); IllegalStateException ex = expectThrows(IllegalStateException.class, () -> - createEnrollmentToken.createNodeEnrollmentToken("elastic", new SecureString("elastic"))); + enrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray())).getEncoded()); assertThat(ex.getMessage(), Matchers.containsString("Elasticsearch node HTTP layer SSL configuration is not configured " + "with a keystore")); } @@ -339,10 +341,10 @@ public void testEnrollmentNotEnabled() throws Exception { final Environment environment_not_enabled = new Environment(settings, tempDir); final CommandLineHttpClient client = mock(CommandLineHttpClient.class); when(client.getDefaultURL()).thenReturn("http://localhost:9200"); - final CreateEnrollmentToken createEnrollmentToken = new CreateEnrollmentToken(environment_not_enabled, client); + final EnrollmentTokenGenerator enrollmentTokenGenerator = new EnrollmentTokenGenerator(environment_not_enabled, client); IllegalStateException ex = expectThrows(IllegalStateException.class, () -> - createEnrollmentToken.createNodeEnrollmentToken("elastic", new SecureString("elastic"))); + enrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray())).getEncoded()); assertThat(ex.getMessage(), Matchers.equalTo("[xpack.security.enrollment.enabled] must be set to `true` to " + "create an enrollment token")); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenTests.java new file mode 100644 index 0000000000000..df3773e99bef8 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenTests.java @@ -0,0 +1,51 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.enrollment; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.test.ESTestCase; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class EnrollmentTokenTests extends ESTestCase { + EnrollmentToken createEnrollmentToken() { + final String apiKey = randomAlphaOfLength(16); + final String fingerprint = randomAlphaOfLength(64); + final String version = randomAlphaOfLength(5); + final List boundAddresses = Arrays.asList(generateRandomStringArray(4, randomIntBetween(2, 32), false)); + return new EnrollmentToken(apiKey, fingerprint, version, boundAddresses); + } + + public void testEnrollmentToken() throws Exception { + EnrollmentToken enrollmentToken = createEnrollmentToken(); + final String apiKey = enrollmentToken.getApiKey(); + final String fingerprint = enrollmentToken.getFingerprint(); + final String version = enrollmentToken.getVersion(); + final List boundAddresses = enrollmentToken.getBoundAddress(); + final String jsonString = enrollmentToken.getRaw(); + final String encoded = enrollmentToken.getEncoded(); + final Map enrollmentMap; + try (XContentParser parser = createParser(JsonXContent.jsonXContent, jsonString)) { + final Map info = parser.map(); + assertNotEquals(info, null); + enrollmentMap = info.entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().toString())); + } + assertEquals(enrollmentMap.get("key"), apiKey); + assertEquals(enrollmentMap.get("fgr"), fingerprint); + assertEquals(enrollmentMap.get("ver"), version); + assertEquals(enrollmentMap.get("adr"), "[" + boundAddresses.stream().collect(Collectors.joining(", ")) + "]"); + assertEquals(new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8), jsonString); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/BootstrapPasswordAndEnrollmentTokenForInitialNodeTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/BootstrapPasswordAndEnrollmentTokenForInitialNodeTests.java new file mode 100644 index 0000000000000..18ad10094358d --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/BootstrapPasswordAndEnrollmentTokenForInitialNodeTests.java @@ -0,0 +1,203 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.enrollment.tool; + +import org.elasticsearch.cli.Command; +import org.elasticsearch.cli.CommandTestCase; +import org.elasticsearch.cli.ExitCodes; +import org.elasticsearch.cli.UserException; +import org.elasticsearch.common.settings.KeyStoreWrapper; +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.xpack.security.enrollment.EnrollmentTokenGenerator; +import org.elasticsearch.xpack.security.enrollment.EnrollmentToken; +import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; +import org.elasticsearch.xpack.security.tool.HttpResponse; +import org.junit.Before; +import org.junit.BeforeClass; + +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BootstrapPasswordAndEnrollmentTokenForInitialNodeTests extends CommandTestCase { + private CommandLineHttpClient client; + private KeyStoreWrapper keyStoreWrapper; + private EnrollmentTokenGenerator enrollmentTokenGenerator; + private URL checkClusterHealthUrl; + private URL setElasticUserPasswordUrl; + private Path confDir; + private Path tempDir; + private Settings settings; + + @Override + protected Command newCommand() { + return new BootstrapPasswordAndEnrollmentTokenForInitialNode(environment -> client, environment -> keyStoreWrapper, + environment -> enrollmentTokenGenerator) { + @Override + protected char[] generatePassword(int passwordLength) { + String password = "Aljngvodjb94j8HSY803"; + return password.toCharArray(); + } + @Override + protected Environment readSecureSettings(Environment env, SecureString password) { + return new Environment(settings, tempDir); + } + @Override + protected Environment createEnv(Map settings) { + return new Environment(BootstrapPasswordAndEnrollmentTokenForInitialNodeTests.this.settings, confDir); + } + }; + } + + @BeforeClass + public static void muteInFips(){ + assumeFalse("Enrollment is not supported in FIPS 140-2 as we are using PKCS#12 keystores", inFipsJvm()); + } + + @Before + public void setup() throws Exception { + this.keyStoreWrapper = mock(KeyStoreWrapper.class); + when(keyStoreWrapper.isLoaded()).thenReturn(true); + this.client = mock(CommandLineHttpClient.class); + when(client.getDefaultURL()).thenReturn("https://localhost:9200"); + checkClusterHealthUrl = BootstrapPasswordAndEnrollmentTokenForInitialNode.checkClusterHealthUrl(client); + setElasticUserPasswordUrl = BootstrapPasswordAndEnrollmentTokenForInitialNode.setElasticUserPasswordUrl(client); + HttpResponse healthResponse = + new HttpResponse(HttpURLConnection.HTTP_OK, Map.of("status", randomFrom("yellow", "green"))); + when(client.execute(anyString(), eq(checkClusterHealthUrl), anyString(), any(SecureString.class), anyObject(), anyObject())) + .thenReturn(healthResponse); + HttpResponse setPasswordResponse = + new HttpResponse(HttpURLConnection.HTTP_OK, Collections.emptyMap()); + when(client.execute(anyString(), eq(setElasticUserPasswordUrl), anyString(), any(SecureString.class), anyObject(), anyObject())) + .thenReturn(setPasswordResponse); + this.enrollmentTokenGenerator = mock(EnrollmentTokenGenerator.class); + EnrollmentToken kibanaToken = new EnrollmentToken("DR6CzXkBDf8amV_48yYX:x3YqU_rqQwm-ESrkExcnOg", + "ce480d53728605674fcfd8ffb51000d8a33bf32de7c7f1e26b4d428f8a91362d", "8.0.0", + Arrays.asList("[192.168.0.1:9201, 172.16.254.1:9202")); + EnrollmentToken nodeToken = new EnrollmentToken("DR6CzXkBDf8amV_48yYX:4BhUk-mkFm-AwvRFg90KJ", + "ce480d53728605674fcfd8ffb51000d8a33bf32de7c7f1e26b4d428f8a91362d", "8.0.0", + Arrays.asList("[192.168.0.1:9201, 172.16.254.1:9202")); + when(enrollmentTokenGenerator.createKibanaEnrollmentToken(anyString(), any(SecureString.class))) + .thenReturn(kibanaToken); + when(enrollmentTokenGenerator.createNodeEnrollmentToken(anyString(), any(SecureString.class))) + .thenReturn(nodeToken); + tempDir = createTempDir(); + confDir = tempDir.resolve("config"); + final Path httpCaPath = tempDir.resolve("httpCa.p12"); + Files.copy(getDataPath("/org/elasticsearch/xpack/security/action/enrollment/httpCa.p12"), httpCaPath); + final MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("xpack.http.ssl.truststore.secure_password", "password"); + secureSettings.setString("xpack.security.http.ssl.keystore.secure_password", "password"); + secureSettings.setString("keystore.seed", "password"); + settings = Settings.builder() + .put("xpack.security.enabled", true) + .put("xpack.http.ssl.enabled", true) + .put("xpack.security.authc.api_key.enabled", true) + .put("xpack.http.ssl.truststore.path", "httpCa.p12") + .put("xpack.security.http.ssl.enabled", true) + .put("xpack.security.http.ssl.keystore.path", "httpCa.p12") + .put("xpack.security.enrollment.enabled", "true") + .setSecureSettings(secureSettings) + .put("path.home", tempDir) + .build(); + } + + public void testGenerateNewPasswordSuccess() throws Exception { + terminal.addSecretInput("password"); + String includeNodeEnrollmentToken = randomBoolean() ? "--include-node-enrollment-token" : ""; + String output = execute(includeNodeEnrollmentToken); + assertThat(output, containsString("elastic user password: Aljngvodjb94j8HSY803")); + assertThat(output, containsString("CA fingerprint: ce480d53728605674fcfd8ffb51000d8a33bf32de7c7f1e26b4d428" + + "f8a91362d")); + assertThat(output, containsString("Kibana enrollment token: eyJ2ZXIiOiI4LjAuMCIsImFkciI6WyJbMTkyLjE2OC4wL" + + "jE6OTIwMSwgMTcyLjE2LjI1NC4xOjkyMDIiXSwiZmdyIjoiY2U0ODBkNTM3Mjg2MDU2NzRmY2ZkOGZmYjUxMDAwZDhhMzNiZjMyZGU3YzdmMWUyN" + + "mI0ZDQyOGY4YTkxMzYyZCIsImtleSI6IkRSNkN6WGtCRGY4YW1WXzQ4eVlYOngzWXFVX3JxUXdtLUVTcmtFeGNuT2cifQ==")); + if (includeNodeEnrollmentToken.equals("--include-node-enrollment-token")) { + assertThat(output, containsString("Node enrollment token: eyJ2ZXIiOiI4LjAuMCIsImFkciI6WyJbMTkyLjE2OC4wLj" + + "E6OTIwMSwgMTcyLjE2LjI1NC4xOjkyMDIiXSwiZmdyIjoiY2U0ODBkNTM3Mjg2MDU2NzRmY2ZkOGZmYjUxMDAwZDhhMzNiZjMyZGU3YzdmMWUy" + + "NmI0ZDQyOGY4YTkxMzYyZCIsImtleSI6IkRSNkN6WGtCRGY4YW1WXzQ4eVlYOjRCaFVrLW1rRm0tQXd2UkZnOTBLSiJ9")); + } else { + assertFalse(output.contains("Node enrollment token: ")); + } + } + + public void testBootstrapPasswordSuccess() throws Exception { + final MockSecureSettings secureSettings = new MockSecureSettings(); + final Path tempDir = createTempDir(); + secureSettings.setString("bootstrap.password", "password"); + settings = Settings.builder() + .put("xpack.security.enabled", true) + .put("xpack.security.enrollment.enabled", "true") + .setSecureSettings(secureSettings) + .put("path.home", tempDir) + .build(); + terminal.addSecretInput("password"); + String includeNodeEnrollmentToken = randomBoolean() ? "--include-node-enrollment-token" : ""; + String output = execute(includeNodeEnrollmentToken); + assertFalse(terminal.getOutput().contains("elastic user password:")); + assertThat(terminal.getOutput(), containsString("CA fingerprint: ce480d53728605674fcfd8ffb51000d8a33bf32de7c7f1e26b4d428" + + "f8a91362d")); + assertThat(output, containsString("Kibana enrollment token: eyJ2ZXIiOiI4LjAuMCIsImFkciI6WyJbMTkyLjE2OC4wL" + + "jE6OTIwMSwgMTcyLjE2LjI1NC4xOjkyMDIiXSwiZmdyIjoiY2U0ODBkNTM3Mjg2MDU2NzRmY2ZkOGZmYjUxMDAwZDhhMzNiZjMyZGU3YzdmMWUy" + + "NmI0ZDQyOGY4YTkxMzYyZCIsImtleSI6IkRSNkN6WGtCRGY4YW1WXzQ4eVlYOngzWXFVX3JxUXdtLUVTcmtFeGNuT2cifQ==")); + if (includeNodeEnrollmentToken.equals("--include-node-enrollment-token")) { + assertThat(output, containsString("Node enrollment token: eyJ2ZXIiOiI4LjAuMCIsImFkciI6WyJbMTkyLjE2OC4wLj" + + "E6OTIwMSwgMTcyLjE2LjI1NC4xOjkyMDIiXSwiZmdyIjoiY2U0ODBkNTM3Mjg2MDU2NzRmY2ZkOGZmYjUxMDAwZDhhMzNiZjMyZGU3YzdmMWU" + + "yNmI0ZDQyOGY4YTkxMzYyZCIsImtleSI6IkRSNkN6WGtCRGY4YW1WXzQ4eVlYOjRCaFVrLW1rRm0tQXd2UkZnOTBLSiJ9")); + } else { + assertFalse(output.contains("Node enrollment token: ")); + } + } + + public void testClusterHealthIsRed() throws Exception { + HttpResponse healthResponse = + new HttpResponse(HttpURLConnection.HTTP_OK, Map.of("status", "red")); + when(client.execute(anyString(), eq(checkClusterHealthUrl), anyString(), any(SecureString.class), anyObject(), anyObject())) + .thenReturn(healthResponse); + doCallRealMethod().when(client).checkClusterHealthWithRetriesWaitingForCluster(anyString(), anyObject(), anyInt()); + terminal.addSecretInput("password"); + final UserException ex = expectThrows(UserException.class, () -> execute("")); + assertNull(ex.getMessage()); + assertThat(ex.exitCode, is(ExitCodes.UNAVAILABLE)); + } + + public void testFailedToSetPassword() throws Exception { + HttpResponse setPasswordResponse = + new HttpResponse(HttpURLConnection.HTTP_UNAUTHORIZED, Collections.emptyMap()); + when(client.execute(anyString(), eq(setElasticUserPasswordUrl), anyString(), any(SecureString.class), anyObject(), anyObject())) + .thenReturn(setPasswordResponse); + terminal.addSecretInput("password"); + final UserException ex = expectThrows(UserException.class, () -> execute("")); + assertNull(ex.getMessage()); + assertThat(ex.exitCode, is(ExitCodes.UNAVAILABLE)); + } + + public void testNoKeystorePassword() { + final UserException ex = expectThrows(UserException.class, () -> execute("")); + assertNull(ex.getMessage()); + assertThat(ex.exitCode, is(ExitCodes.USAGE)); + } +} diff --git a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenToolTests.java b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenToolTests.java index d7b662684c828..5a7964de2ce39 100644 --- a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenToolTests.java +++ b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenToolTests.java @@ -23,7 +23,8 @@ import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.core.XPackSettings; -import org.elasticsearch.xpack.security.enrollment.CreateEnrollmentToken; +import org.elasticsearch.xpack.security.enrollment.EnrollmentToken; +import org.elasticsearch.xpack.security.enrollment.EnrollmentTokenGenerator; import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; import org.elasticsearch.xpack.security.tool.HttpResponse; import org.junit.AfterClass; @@ -39,6 +40,7 @@ import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -60,14 +62,14 @@ public class CreateEnrollmentTokenToolTests extends CommandTestCase { private CommandLineHttpClient client; private KeyStoreWrapper keyStoreWrapper; - private CreateEnrollmentToken createEnrollmentTokenService; + private EnrollmentTokenGenerator enrollmentTokenGenerator; @Override protected Command newCommand() { return new CreateEnrollmentTokenTool(environment -> client, environment -> keyStoreWrapper, - environment -> createEnrollmentTokenService) { + environment -> enrollmentTokenGenerator) { @Override - protected Environment createEnv(Map settings) throws UserException { + protected Environment createEnv(Map settings) { return new Environment(CreateEnrollmentTokenToolTests.this.settings, confDir); } }; @@ -112,15 +114,17 @@ public void setup() throws Exception { when(client.execute(anyString(), eq(clusterHealthUrl(url)), anyString(), any(SecureString.class), any(CheckedSupplier.class), any(CheckedFunction.class))).thenReturn(healthResponse); - this.createEnrollmentTokenService = mock(CreateEnrollmentToken.class); - when(createEnrollmentTokenService.createKibanaEnrollmentToken(anyString(), any(SecureString.class))) - .thenReturn("eyJ2ZXIiOiI4LjAuMCIsImFkciI6WyJbOjoxXTo5MjAwIiwiMTI3LjAuMC4xOjkyMDAiXSwiZmdyIjoiOWQ4MTRmYzdiNDQ0MWE0MWJlMDA5ZmQ0" + - "MzlkOWU5MzRiMDZiMjZjZjk4N2I1YzNjOGU0OWI1NmQ2MGYzMmMxMiIsImtleSI6Im5NMmVYbm9CbnBvT3ZncGFiaWU5OlRHaHF5UU9UVENhUEJpOVZQak1i" + - "OWcifQ=="); - when(createEnrollmentTokenService.createNodeEnrollmentToken(anyString(), any(SecureString.class))) - .thenReturn("eyJ2ZXIiOiI4LjAuMCIsImFkciI6WyJbOjoxXTo5MjAwIiwiMTI3LjAuMC4xOjkyMDAiXSwiZmdyIjoiOWQ4MTRmYzdiNDQ0MWE0MWJlMDA5ZmQ0" + - "MzlkOWU5MzRiMDZiMjZjZjk4N2I1YzNjOGU0OWI1NmQ2MGYzMmMxMiIsImtleSI6IndLTmZYSG9CQTFPMHI4UXBOV25FOnRkdUgzTmNTVHNTOGN0c3AwaWNU" + - "eEEifQ=="); + this.enrollmentTokenGenerator = mock(EnrollmentTokenGenerator.class); + EnrollmentToken kibanaToken = new EnrollmentToken("DR6CzXkBDf8amV_48yYX:x3YqU_rqQwm-ESrkExcnOg", + "ce480d53728605674fcfd8ffb51000d8a33bf32de7c7f1e26b4d428f8a91362d", "8.0.0", + Arrays.asList("[192.168.0.1:9201, 172.16.254.1:9202")); + EnrollmentToken nodeToken = new EnrollmentToken("DR6CzXkBDf8amV_48yYX:4BhUk-mkFm-AwvRFg90KJ", + "ce480d53728605674fcfd8ffb51000d8a33bf32de7c7f1e26b4d428f8a91362d", "8.0.0", + Arrays.asList("[192.168.0.1:9201, 172.16.254.1:9202")); + when(enrollmentTokenGenerator.createKibanaEnrollmentToken(anyString(), any(SecureString.class))) + .thenReturn(kibanaToken); + when(enrollmentTokenGenerator.createNodeEnrollmentToken(anyString(), any(SecureString.class))) + .thenReturn(nodeToken); } @AfterClass @@ -135,9 +139,9 @@ public void testCreateToken() throws Exception { String scope = randomBoolean() ? "node" : "kibana"; String output = execute("--scope", scope); if (scope.equals("kibana")) { - assertThat(output, containsString("WU5OlRHaHF5UU9UVENhUEJpOVZQak1iOWcifQ==")); + assertThat(output, containsString("1WXzQ4eVlYOngzWXFVX3JxUXdtLUVTcmtFeGNuT2cifQ==")); } else { - assertThat(output, containsString("25FOnRkdUgzTmNTVHNTOGN0c3AwaWNUeEEifQ==")); + assertThat(output, containsString("4YW1WXzQ4eVlYOjRCaFVrLW1rRm0tQXd2UkZnOTBLSiJ9")); } } @@ -170,13 +174,13 @@ public void testUnhealthyClusterWithForce() throws Exception { String scope = randomBoolean() ? "node" : "kibana"; String output = execute("--scope", scope); if (scope.equals("kibana")) { - assertThat(output, containsString("WU5OlRHaHF5UU9UVENhUEJpOVZQak1iOWcifQ==")); + assertThat(output, containsString("1WXzQ4eVlYOngzWXFVX3JxUXdtLUVTcmtFeGNuT2cifQ==")); } else { - assertThat(output, containsString("25FOnRkdUgzTmNTVHNTOGN0c3AwaWNUeEEifQ==")); + assertThat(output, containsString("4YW1WXzQ4eVlYOjRCaFVrLW1rRm0tQXd2UkZnOTBLSiJ9")); } } - public void testEnrollmentDisabled() throws Exception { + public void testEnrollmentDisabled() { settings = Settings.builder() .put(settings) .put(XPackSettings.ENROLLMENT_ENABLED.getKey(), false) @@ -192,10 +196,10 @@ public void testEnrollmentDisabled() throws Exception { } public void testUnableToCreateToken() throws Exception { - this.createEnrollmentTokenService = mock(CreateEnrollmentToken.class); - when(createEnrollmentTokenService.createKibanaEnrollmentToken(anyString(), any(SecureString.class))) + this.enrollmentTokenGenerator = mock(EnrollmentTokenGenerator.class); + when(enrollmentTokenGenerator.createKibanaEnrollmentToken(anyString(), any(SecureString.class))) .thenThrow(new IllegalStateException("example exception message")); - when(createEnrollmentTokenService.createNodeEnrollmentToken(anyString(), any(SecureString.class))) + when(enrollmentTokenGenerator.createNodeEnrollmentToken(anyString(), any(SecureString.class))) .thenThrow(new IllegalStateException("example exception message")); String scope = randomBoolean() ? "node" : "kibana"; UserException e = expectThrows(UserException.class, () -> {