> for more details.
+
By default, Elasticsearch runs in the foreground, prints its logs to `STDOUT`,
and can be stopped by pressing `Ctrl-C`.
diff --git a/docs/reference/setup/secure-settings.asciidoc b/docs/reference/setup/secure-settings.asciidoc
index e565877f22f..f35c3747350 100644
--- a/docs/reference/setup/secure-settings.asciidoc
+++ b/docs/reference/setup/secure-settings.asciidoc
@@ -14,9 +14,6 @@ reference.
All the modifications to the keystore take affect only after restarting {es}.
-NOTE: The {es} keystore currently only provides obfuscation. In the future,
-password protection will be added.
-
These settings, just like the regular ones in the `elasticsearch.yml` config file,
need to be specified on each node in the cluster. Currently, all secure settings
are node-specific settings that must have the same value on every node.
@@ -37,7 +34,13 @@ using the `bin/elasticsearch-keystore add` command, call:
[source,console]
----
POST _nodes/reload_secure_settings
+{
+ "reload_secure_settings": "s3cr3t" <1>
+}
----
+// NOTCONSOLE
+
+<1> The password that the {es} keystore is encrypted with.
This API decrypts and re-reads the entire keystore, on every cluster node,
but only the *reloadable* secure settings are applied. Changes to other
diff --git a/libs/cli/src/main/java/org/elasticsearch/cli/Command.java b/libs/cli/src/main/java/org/elasticsearch/cli/Command.java
index 9ce77604a50..ec23f62b090 100644
--- a/libs/cli/src/main/java/org/elasticsearch/cli/Command.java
+++ b/libs/cli/src/main/java/org/elasticsearch/cli/Command.java
@@ -97,7 +97,9 @@ public abstract class Command implements Closeable {
if (e.exitCode == ExitCodes.USAGE) {
printHelp(terminal, true);
}
- terminal.errorPrintln(Terminal.Verbosity.SILENT, "ERROR: " + e.getMessage());
+ if (e.getMessage() != null) {
+ terminal.errorPrintln(Terminal.Verbosity.SILENT, "ERROR: " + e.getMessage());
+ }
return e.exitCode;
}
return ExitCodes.OK;
diff --git a/libs/cli/src/main/java/org/elasticsearch/cli/Terminal.java b/libs/cli/src/main/java/org/elasticsearch/cli/Terminal.java
index 74af7e2e310..aff1b8a85a0 100644
--- a/libs/cli/src/main/java/org/elasticsearch/cli/Terminal.java
+++ b/libs/cli/src/main/java/org/elasticsearch/cli/Terminal.java
@@ -24,7 +24,9 @@ import java.io.Console;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
+import java.io.Reader;
import java.nio.charset.Charset;
+import java.util.Arrays;
import java.util.Locale;
/**
@@ -78,6 +80,16 @@ public abstract class Terminal {
/** Reads password text from the terminal input. See {@link Console#readPassword()}}. */
public abstract char[] readSecret(String prompt);
+ /** Read password text form terminal input up to a maximum length. */
+ public char[] readSecret(String prompt, int maxLength) {
+ char[] result = readSecret(prompt);
+ if (result.length > maxLength) {
+ Arrays.fill(result, '\0');
+ throw new IllegalStateException("Secret exceeded maximum length of " + maxLength);
+ }
+ return result;
+ }
+
/** Returns a Writer which can be used to write to the terminal directly using standard output. */
public abstract PrintWriter getWriter();
@@ -151,6 +163,45 @@ public abstract class Terminal {
}
}
+ /**
+ * Read from the reader until we find a newline. If that newline
+ * character is immediately preceded by a carriage return, we have
+ * a Windows-style newline, so we discard the carriage return as well
+ * as the newline.
+ */
+ public static char[] readLineToCharArray(Reader reader, int maxLength) {
+ char[] buf = new char[maxLength + 2];
+ try {
+ int len = 0;
+ int next;
+ while ((next = reader.read()) != -1) {
+ char nextChar = (char) next;
+ if (nextChar == '\n') {
+ break;
+ }
+ if (len < buf.length) {
+ buf[len] = nextChar;
+ }
+ len++;
+ }
+
+ if (len > 0 && len < buf.length && buf[len-1] == '\r') {
+ len--;
+ }
+
+ if (len > maxLength) {
+ Arrays.fill(buf, '\0');
+ throw new RuntimeException("Input exceeded maximum length of " + maxLength);
+ }
+
+ char[] shortResult = Arrays.copyOf(buf, len);
+ Arrays.fill(buf, '\0');
+ return shortResult;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
public void flush() {
this.getWriter().flush();
this.getErrorWriter().flush();
@@ -184,10 +235,13 @@ public abstract class Terminal {
}
}
- private static class SystemTerminal extends Terminal {
+ /** visible for testing */
+ static class SystemTerminal extends Terminal {
private static final PrintWriter WRITER = newWriter();
+ private BufferedReader reader;
+
SystemTerminal() {
super(System.lineSeparator());
}
@@ -197,6 +251,14 @@ public abstract class Terminal {
return new PrintWriter(System.out);
}
+ /** visible for testing */
+ BufferedReader getReader() {
+ if (reader == null) {
+ reader = new BufferedReader(new InputStreamReader(System.in, Charset.defaultCharset()));
+ }
+ return reader;
+ }
+
@Override
public PrintWriter getWriter() {
return WRITER;
@@ -205,9 +267,8 @@ public abstract class Terminal {
@Override
public String readText(String text) {
getErrorWriter().print(text); // prompts should go to standard error to avoid mixing with list output
- BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, Charset.defaultCharset()));
try {
- final String line = reader.readLine();
+ final String line = getReader().readLine();
if (line == null) {
throw new IllegalStateException("unable to read from standard input; is standard input open and a tty attached?");
}
@@ -221,5 +282,11 @@ public abstract class Terminal {
public char[] readSecret(String text) {
return readText(text).toCharArray();
}
+
+ @Override
+ public char[] readSecret(String text, int maxLength) {
+ getErrorWriter().println(text);
+ return readLineToCharArray(getReader(), maxLength);
+ }
}
}
diff --git a/libs/cli/src/main/java/org/elasticsearch/cli/UserException.java b/libs/cli/src/main/java/org/elasticsearch/cli/UserException.java
index 4749b1b87b7..fd6ec7807a5 100644
--- a/libs/cli/src/main/java/org/elasticsearch/cli/UserException.java
+++ b/libs/cli/src/main/java/org/elasticsearch/cli/UserException.java
@@ -19,6 +19,8 @@
package org.elasticsearch.cli;
+import org.elasticsearch.common.Nullable;
+
/**
* An exception representing a user fixable problem in {@link Command} usage.
*/
@@ -27,20 +29,26 @@ public class UserException extends Exception {
/** The exist status the cli should use when catching this user error. */
public final int exitCode;
- /** Constructs a UserException with an exit status and message to show the user. */
- public UserException(int exitCode, String msg) {
+ /**
+ * Constructs a UserException with an exit status and message to show the user.
+ *
+ * To suppress cli output on error, supply a null message.
+ */
+ public UserException(int exitCode, @Nullable String msg) {
super(msg);
this.exitCode = exitCode;
}
/**
* Constructs a new user exception with specified exit status, message, and underlying cause.
+ *
+ * To suppress cli output on error, supply a null message.
*
* @param exitCode the exit code
* @param msg the message
* @param cause the underlying cause
*/
- public UserException(final int exitCode, final String msg, final Throwable cause) {
+ public UserException(final int exitCode, @Nullable final String msg, final Throwable cause) {
super(msg, cause);
this.exitCode = exitCode;
}
diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/ArchiveTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/ArchiveTests.java
index 30709a3d866..6d74efb9855 100644
--- a/qa/os/src/test/java/org/elasticsearch/packaging/test/ArchiveTests.java
+++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/ArchiveTests.java
@@ -20,7 +20,6 @@
package org.elasticsearch.packaging.test;
import org.apache.http.client.fluent.Request;
-import org.elasticsearch.packaging.util.Archives;
import org.elasticsearch.packaging.util.FileUtils;
import org.elasticsearch.packaging.util.Installation;
import org.elasticsearch.packaging.util.Platforms;
@@ -33,12 +32,8 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;
-import static org.elasticsearch.packaging.util.Archives.ARCHIVE_OWNER;
import static org.elasticsearch.packaging.util.Archives.installArchive;
import static org.elasticsearch.packaging.util.Archives.verifyArchiveInstallation;
-import static org.elasticsearch.packaging.util.FileMatcher.Fileness.File;
-import static org.elasticsearch.packaging.util.FileMatcher.file;
-import static org.elasticsearch.packaging.util.FileMatcher.p660;
import static org.elasticsearch.packaging.util.FileUtils.append;
import static org.elasticsearch.packaging.util.FileUtils.cp;
import static org.elasticsearch.packaging.util.FileUtils.getTempDir;
@@ -105,33 +100,6 @@ public class ArchiveTests extends PackagingTestCase {
}
- public void test40CreateKeystoreManually() throws Exception {
- final Installation.Executables bin = installation.executables();
-
- Platforms.onLinux(() -> sh.run("sudo -u " + ARCHIVE_OWNER + " " + bin.keystoreTool + " create"));
-
- // this is a hack around the fact that we can't run a command in the same session as the same user but not as administrator.
- // the keystore ends up being owned by the Administrators group, so we manually set it to be owned by the vagrant user here.
- // from the server's perspective the permissions aren't really different, this is just to reflect what we'd expect in the tests.
- // when we run these commands as a role user we won't have to do this
- Platforms.onWindows(() -> {
- sh.run(bin.keystoreTool + " create");
- sh.chown(installation.config("elasticsearch.keystore"));
- });
-
- assertThat(installation.config("elasticsearch.keystore"), file(File, ARCHIVE_OWNER, ARCHIVE_OWNER, p660));
-
- Platforms.onLinux(() -> {
- final Result r = sh.run("sudo -u " + ARCHIVE_OWNER + " " + bin.keystoreTool + " list");
- assertThat(r.stdout, containsString("keystore.seed"));
- });
-
- Platforms.onWindows(() -> {
- final Result r = sh.run(bin.keystoreTool + " list");
- assertThat(r.stdout, containsString("keystore.seed"));
- });
- }
-
public void test50StartAndStop() throws Exception {
// cleanup from previous test
rm(installation.config("elasticsearch.keystore"));
@@ -251,22 +219,6 @@ public class ArchiveTests extends PackagingTestCase {
});
}
- public void test60AutoCreateKeystore() throws Exception {
- sh.chown(installation.config("elasticsearch.keystore"));
- assertThat(installation.config("elasticsearch.keystore"), file(File, ARCHIVE_OWNER, ARCHIVE_OWNER, p660));
-
- final Installation.Executables bin = installation.executables();
- Platforms.onLinux(() -> {
- final Result result = sh.run("sudo -u " + ARCHIVE_OWNER + " " + bin.keystoreTool + " list");
- assertThat(result.stdout, containsString("keystore.seed"));
- });
-
- Platforms.onWindows(() -> {
- final Result result = sh.run(bin.keystoreTool + " list");
- assertThat(result.stdout, containsString("keystore.seed"));
- });
- }
-
public void test70CustomPathConfAndJvmOptions() throws Exception {
final Path tempConf = getTempDir().resolve("esconf-alternate");
@@ -296,7 +248,7 @@ public class ArchiveTests extends PackagingTestCase {
assertThat(nodesResponse, containsString("\"heap_init_in_bytes\":536870912"));
assertThat(nodesResponse, containsString("\"using_compressed_ordinary_object_pointers\":\"false\""));
- Archives.stopElasticsearch(installation);
+ stopElasticsearch();
} finally {
rm(tempConf);
@@ -393,7 +345,7 @@ public class ArchiveTests extends PackagingTestCase {
sh.setWorkingDirectory(getTempDir());
startElasticsearch();
- Archives.stopElasticsearch(installation);
+ stopElasticsearch();
Result result = sh.run("echo y | " + installation.executables().nodeTool + " unsafe-bootstrap");
assertThat(result.stdout, containsString("Master node was successfully bootstrapped"));
diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java
index 93c8ba5b9db..7f9b0c8a9da 100644
--- a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java
+++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java
@@ -19,23 +19,38 @@
package org.elasticsearch.packaging.test;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.http.client.fluent.Request;
+import org.elasticsearch.packaging.util.Installation;
+import org.elasticsearch.packaging.util.Platforms;
+import org.elasticsearch.packaging.util.ServerUtils;
+import org.elasticsearch.packaging.util.Shell.Result;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
import static java.nio.file.attribute.PosixFilePermissions.fromString;
import static java.util.Collections.singletonMap;
-import static org.elasticsearch.packaging.util.Docker.assertPermissionsAndOwnership;
import static org.elasticsearch.packaging.util.Docker.copyFromContainer;
-import static org.elasticsearch.packaging.util.Docker.ensureImageIsLoaded;
import static org.elasticsearch.packaging.util.Docker.existsInContainer;
import static org.elasticsearch.packaging.util.Docker.getContainerLogs;
import static org.elasticsearch.packaging.util.Docker.getImageLabels;
import static org.elasticsearch.packaging.util.Docker.getJson;
import static org.elasticsearch.packaging.util.Docker.mkDirWithPrivilegeEscalation;
-import static org.elasticsearch.packaging.util.Docker.removeContainer;
import static org.elasticsearch.packaging.util.Docker.rmDirWithPrivilegeEscalation;
import static org.elasticsearch.packaging.util.Docker.runContainer;
import static org.elasticsearch.packaging.util.Docker.runContainerExpectingFailure;
import static org.elasticsearch.packaging.util.Docker.verifyContainerInstallation;
import static org.elasticsearch.packaging.util.Docker.waitForElasticsearch;
-import static org.elasticsearch.packaging.util.Docker.waitForPathToExist;
import static org.elasticsearch.packaging.util.FileMatcher.p600;
import static org.elasticsearch.packaging.util.FileMatcher.p660;
import static org.elasticsearch.packaging.util.FileMatcher.p775;
@@ -56,53 +71,21 @@ import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
-import java.io.IOException;
import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
-import java.util.HashMap;
import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import org.apache.http.client.fluent.Request;
-import org.elasticsearch.packaging.util.Distribution;
-import org.elasticsearch.packaging.util.Docker.DockerShell;
-import org.elasticsearch.packaging.util.Installation;
-import org.elasticsearch.packaging.util.Platforms;
-import org.elasticsearch.packaging.util.ServerUtils;
-import org.elasticsearch.packaging.util.Shell.Result;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-
-import com.fasterxml.jackson.databind.JsonNode;
public class DockerTests extends PackagingTestCase {
- protected DockerShell sh;
private Path tempDir;
@BeforeClass
public static void filterDistros() {
- assumeTrue("only Docker", distribution.packaging == Distribution.Packaging.DOCKER);
-
- ensureImageIsLoaded(distribution);
- }
-
- @AfterClass
- public static void cleanup() {
- // runContainer also calls this, so we don't need this method to be annotated as `@After`
- removeContainer();
+ assumeTrue("only Docker", distribution().isDocker());
}
@Before
public void setupTest() throws IOException {
- sh = new DockerShell();
installation = runContainer(distribution());
tempDir = Files.createTempDirectory(getTempDir(), DockerTests.class.getSimpleName());
}
@@ -143,44 +126,10 @@ public class DockerTests extends PackagingTestCase {
assertThat("Expected no plugins to be listed", r.stdout, emptyString());
}
- /**
- * Check that a keystore can be manually created using the provided CLI tool.
- */
- public void test040CreateKeystoreManually() throws InterruptedException {
- final Installation.Executables bin = installation.executables();
-
- final Path keystorePath = installation.config("elasticsearch.keystore");
-
- waitForPathToExist(keystorePath);
-
- // Move the auto-created one out of the way, or else the CLI prompts asks us to confirm
- sh.run("mv " + keystorePath + " " + keystorePath + ".bak");
-
- sh.run(bin.keystoreTool + " create");
-
- final Result r = sh.run(bin.keystoreTool + " list");
- assertThat(r.stdout, containsString("keystore.seed"));
- }
-
- /**
- * Check that the default keystore is automatically created
- */
- public void test041AutoCreateKeystore() throws Exception {
- final Path keystorePath = installation.config("elasticsearch.keystore");
-
- waitForPathToExist(keystorePath);
-
- assertPermissionsAndOwnership(keystorePath, p660);
-
- final Installation.Executables bin = installation.executables();
- final Result result = sh.run(bin.keystoreTool + " list");
- assertThat(result.stdout, containsString("keystore.seed"));
- }
-
/**
* Check that the JDK's cacerts file is a symlink to the copy provided by the operating system.
*/
- public void test042JavaUsesTheOsProvidedKeystore() {
+ public void test040JavaUsesTheOsProvidedKeystore() {
final String path = sh.run("realpath jdk/lib/security/cacerts").stdout;
assertThat(path, equalTo("/etc/pki/ca-trust/extracted/java/cacerts"));
diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/KeystoreManagementTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/KeystoreManagementTests.java
new file mode 100644
index 00000000000..def1bb3dc24
--- /dev/null
+++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/KeystoreManagementTests.java
@@ -0,0 +1,427 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.packaging.test;
+
+import org.elasticsearch.packaging.util.Distribution;
+import org.elasticsearch.packaging.util.Docker;
+import org.elasticsearch.packaging.util.FileUtils;
+import org.elasticsearch.packaging.util.Installation;
+import org.elasticsearch.packaging.util.Packages;
+import org.elasticsearch.packaging.util.Platforms;
+import org.elasticsearch.packaging.util.ServerUtils;
+import org.elasticsearch.packaging.util.Shell;
+import org.junit.Ignore;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.elasticsearch.packaging.util.Archives.ARCHIVE_OWNER;
+import static org.elasticsearch.packaging.util.Archives.installArchive;
+import static org.elasticsearch.packaging.util.Archives.verifyArchiveInstallation;
+import static org.elasticsearch.packaging.util.Docker.assertPermissionsAndOwnership;
+import static org.elasticsearch.packaging.util.Docker.runContainer;
+import static org.elasticsearch.packaging.util.Docker.runContainerExpectingFailure;
+import static org.elasticsearch.packaging.util.Docker.waitForElasticsearch;
+import static org.elasticsearch.packaging.util.Docker.waitForPathToExist;
+import static org.elasticsearch.packaging.util.FileMatcher.Fileness.File;
+import static org.elasticsearch.packaging.util.FileMatcher.file;
+import static org.elasticsearch.packaging.util.FileMatcher.p660;
+import static org.elasticsearch.packaging.util.FileUtils.getTempDir;
+import static org.elasticsearch.packaging.util.FileUtils.rm;
+import static org.elasticsearch.packaging.util.Packages.assertInstalled;
+import static org.elasticsearch.packaging.util.Packages.assertRemoved;
+import static org.elasticsearch.packaging.util.Packages.installPackage;
+import static org.elasticsearch.packaging.util.Packages.verifyPackageInstallation;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.junit.Assume.assumeThat;
+import static org.junit.Assume.assumeTrue;
+
+public class KeystoreManagementTests extends PackagingTestCase {
+
+ public static final String ERROR_INCORRECT_PASSWORD = "Provided keystore password was incorrect";
+ public static final String ERROR_KEYSTORE_NOT_PASSWORD_PROTECTED = "ERROR: Keystore is not password-protected";
+ public static final String ERROR_KEYSTORE_NOT_FOUND = "ERROR: Elasticsearch keystore not found";
+
+ /** Test initial archive state */
+ public void test10InstallArchiveDistribution() throws Exception {
+ assumeTrue(distribution().isArchive());
+
+ installation = installArchive(sh, distribution);
+ verifyArchiveInstallation(installation, distribution());
+
+ final Installation.Executables bin = installation.executables();
+ Shell.Result r = sh.runIgnoreExitCode(bin.keystoreTool.toString() + " has-passwd");
+ assertFalse("has-passwd should fail", r.isSuccess());
+ assertThat("has-passwd should indicate missing keystore",
+ r.stderr, containsString(ERROR_KEYSTORE_NOT_FOUND));
+ }
+
+ /** Test initial package state */
+ public void test11InstallPackageDistribution() throws Exception {
+ assumeTrue(distribution().isPackage());
+
+ assertRemoved(distribution);
+ installation = installPackage(sh, distribution);
+ assertInstalled(distribution);
+ verifyPackageInstallation(installation, distribution, sh);
+
+ final Installation.Executables bin = installation.executables();
+ Shell.Result r = sh.runIgnoreExitCode(bin.keystoreTool.toString() + " has-passwd");
+ assertFalse("has-passwd should fail", r.isSuccess());
+ assertThat("has-passwd should indicate unprotected keystore",
+ r.stderr, containsString(ERROR_KEYSTORE_NOT_PASSWORD_PROTECTED));
+ Shell.Result r2 = bin.keystoreTool.run("list");
+ assertThat(r2.stdout, containsString("keystore.seed"));
+ }
+
+ /** Test initial Docker state */
+ public void test12InstallDockerDistribution() throws Exception {
+ assumeTrue(distribution().isDocker());
+
+ installation = Docker.runContainer(distribution());
+
+ try {
+ waitForPathToExist(installation.config("elasticsearch.keystore"));
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ final Installation.Executables bin = installation.executables();
+ Shell.Result r = sh.runIgnoreExitCode(bin.keystoreTool.toString() + " has-passwd");
+ assertFalse("has-passwd should fail", r.isSuccess());
+ assertThat("has-passwd should indicate unprotected keystore",
+ r.stdout, containsString(ERROR_KEYSTORE_NOT_PASSWORD_PROTECTED));
+ Shell.Result r2 = bin.keystoreTool.run("list");
+ assertThat(r2.stdout, containsString("keystore.seed"));
+ }
+
+ public void test20CreateKeystoreManually() throws Exception {
+ rmKeystoreIfExists();
+ createKeystore();
+
+ final Installation.Executables bin = installation.executables();
+ verifyKeystorePermissions();
+
+ Shell.Result r = bin.keystoreTool.run("list");
+ assertThat(r.stdout, containsString("keystore.seed"));
+ }
+
+ public void test30AutoCreateKeystore() throws Exception {
+ assumeTrue("Packages and docker are installed with a keystore file", distribution.isArchive());
+ rmKeystoreIfExists();
+
+ startElasticsearch();
+ stopElasticsearch();
+
+ Platforms.onWindows(() -> sh.chown(installation.config("elasticsearch.keystore")));
+
+ verifyKeystorePermissions();
+
+ final Installation.Executables bin = installation.executables();
+ Shell.Result r = bin.keystoreTool.run("list");
+ assertThat(r.stdout, containsString("keystore.seed"));
+ }
+
+ public void test40KeystorePasswordOnStandardInput() throws Exception {
+ assumeTrue("packages will use systemd, which doesn't handle stdin",
+ distribution.isArchive());
+ assumeThat(installation, is(notNullValue()));
+
+ String password = "^|<>\\&exit"; // code insertion on Windows if special characters are not escaped
+
+ rmKeystoreIfExists();
+ createKeystore();
+ setKeystorePassword(password);
+
+ assertPasswordProtectedKeystore();
+
+ awaitElasticsearchStartup(startElasticsearchStandardInputPassword(password));
+ ServerUtils.runElasticsearchTests();
+ stopElasticsearch();
+ }
+
+ public void test41WrongKeystorePasswordOnStandardInput() {
+ assumeTrue("packages will use systemd, which doesn't handle stdin",
+ distribution.isArchive());
+ assumeThat(installation, is(notNullValue()));
+
+ assertPasswordProtectedKeystore();
+
+ Shell.Result result = startElasticsearchStandardInputPassword("wrong");
+ assertElasticsearchFailure(result, ERROR_INCORRECT_PASSWORD, null);
+ }
+
+ @Ignore /* Ignored for feature branch, awaits fix: https://github.com/elastic/elasticsearch/issues/49340 */
+ public void test42KeystorePasswordOnTty() throws Exception {
+ assumeTrue("expect command isn't on Windows",
+ distribution.platform != Distribution.Platform.WINDOWS);
+ assumeTrue("packages will use systemd, which doesn't handle stdin",
+ distribution.isArchive());
+ assumeThat(installation, is(notNullValue()));
+
+ String password = "keystorepass";
+
+ rmKeystoreIfExists();
+ createKeystore();
+ setKeystorePassword(password);
+
+ assertPasswordProtectedKeystore();
+
+ awaitElasticsearchStartup(startElasticsearchTtyPassword(password));
+ ServerUtils.runElasticsearchTests();
+ stopElasticsearch();
+ }
+
+ @Ignore /* Ignored for feature branch, awaits fix: https://github.com/elastic/elasticsearch/issues/49340 */
+ public void test43WrongKeystorePasswordOnTty() throws Exception {
+ assumeTrue("expect command isn't on Windows",
+ distribution.platform != Distribution.Platform.WINDOWS);
+ assumeTrue("packages will use systemd, which doesn't handle stdin",
+ distribution.isArchive());
+ assumeThat(installation, is(notNullValue()));
+
+ assertPasswordProtectedKeystore();
+
+ Shell.Result result = startElasticsearchTtyPassword("wrong");
+ // error will be on stdout for "expect"
+ assertThat(result.stdout, containsString(ERROR_INCORRECT_PASSWORD));
+ }
+
+ public void test50KeystorePasswordFromFile() throws Exception {
+ assumeTrue("only for systemd", Platforms.isSystemd() && distribution().isPackage());
+ String password = "!@#$%^&*()|\\<>/?";
+ Path esKeystorePassphraseFile = installation.config.resolve("eks");
+
+ rmKeystoreIfExists();
+ createKeystore();
+ setKeystorePassword(password);
+
+ assertPasswordProtectedKeystore();
+
+ try {
+ sh.run("sudo systemctl set-environment ES_KEYSTORE_PASSPHRASE_FILE=" + esKeystorePassphraseFile);
+
+ Files.createFile(esKeystorePassphraseFile);
+ Files.write(esKeystorePassphraseFile,
+ (password + System.lineSeparator()).getBytes(StandardCharsets.UTF_8),
+ StandardOpenOption.WRITE);
+
+ startElasticsearch();
+ ServerUtils.runElasticsearchTests();
+ stopElasticsearch();
+ } finally {
+ sh.run("sudo systemctl unset-environment ES_KEYSTORE_PASSPHRASE_FILE");
+ }
+ }
+
+ public void test51WrongKeystorePasswordFromFile() throws Exception {
+ assumeTrue("only for systemd", Platforms.isSystemd() && distribution().isPackage());
+ Path esKeystorePassphraseFile = installation.config.resolve("eks");
+
+ assertPasswordProtectedKeystore();
+
+ try {
+ sh.run("sudo systemctl set-environment ES_KEYSTORE_PASSPHRASE_FILE=" + esKeystorePassphraseFile);
+
+ if (Files.exists(esKeystorePassphraseFile)) {
+ rm(esKeystorePassphraseFile);
+ }
+
+ Files.createFile(esKeystorePassphraseFile);
+ Files.write(esKeystorePassphraseFile,
+ ("wrongpassword" + System.lineSeparator()).getBytes(StandardCharsets.UTF_8),
+ StandardOpenOption.WRITE);
+
+ Packages.JournaldWrapper journaldWrapper = new Packages.JournaldWrapper(sh);
+ Shell.Result result = runElasticsearchStartCommand();
+ assertElasticsearchFailure(result, ERROR_INCORRECT_PASSWORD, journaldWrapper);
+ } finally {
+ sh.run("sudo systemctl unset-environment ES_KEYSTORE_PASSPHRASE_FILE");
+ }
+ }
+
+ /**
+ * Check that we can mount a password-protected keystore to a docker image
+ * and provide a password via an environment variable.
+ */
+ public void test60DockerEnvironmentVariablePassword() throws Exception {
+ assumeTrue(distribution().isDocker());
+ String password = "password";
+ Path dockerKeystore = installation.config("elasticsearch.keystore");
+
+ Path localKeystoreFile = getKeystoreFileFromDockerContainer(password, dockerKeystore);
+
+ // restart ES with password and mounted keystore
+ Map volumes = new HashMap<>();
+ volumes.put(localKeystoreFile, dockerKeystore);
+ Map envVars = new HashMap<>();
+ envVars.put("KEYSTORE_PASSWORD", password);
+ runContainer(distribution(), volumes, envVars);
+ waitForElasticsearch(installation);
+ ServerUtils.runElasticsearchTests();
+ }
+
+ /**
+ * Check that if we provide the wrong password for a mounted and password-protected
+ * keystore, Elasticsearch doesn't start.
+ */
+ public void test61DockerEnvironmentVariableBadPassword() throws Exception {
+ assumeTrue(distribution().isDocker());
+ String password = "password";
+ Path dockerKeystore = installation.config("elasticsearch.keystore");
+
+ Path localKeystoreFile = getKeystoreFileFromDockerContainer(password, dockerKeystore);
+
+ // restart ES with password and mounted keystore
+ Map volumes = new HashMap<>();
+ volumes.put(localKeystoreFile, dockerKeystore);
+ Map envVars = new HashMap<>();
+ envVars.put("KEYSTORE_PASSWORD", "wrong");
+ Shell.Result r = runContainerExpectingFailure(distribution(), volumes, envVars);
+ assertThat(r.stderr, containsString(ERROR_INCORRECT_PASSWORD));
+ }
+
+ /**
+ * In the Docker context, it's a little bit tricky to get a password-protected
+ * keystore. All of the utilities we'd want to use are on the Docker image.
+ * This method mounts a temporary directory to a Docker container, password-protects
+ * the keystore, and then returns the path of the file that appears in the
+ * mounted directory (now accessible from the local filesystem).
+ */
+ private Path getKeystoreFileFromDockerContainer(String password, Path dockerKeystore) throws IOException {
+ // Mount a temporary directory for copying the keystore
+ Path dockerTemp = Paths.get("/usr/tmp/keystore-tmp");
+ Path tempDirectory = Files.createTempDirectory(getTempDir(), KeystoreManagementTests.class.getSimpleName());
+ Map volumes = new HashMap<>();
+ volumes.put(tempDirectory, dockerTemp);
+
+ // It's very tricky to properly quote a pipeline that you're passing to
+ // a docker exec command, so we're just going to put a small script in the
+ // temp folder.
+ String setPasswordScript = "echo \"" + password + "\n" + password
+ + "\n\" | " + installation.executables().keystoreTool.toString() + " passwd";
+ Files.write(tempDirectory.resolve("set-pass.sh"), setPasswordScript.getBytes(StandardCharsets.UTF_8));
+
+ runContainer(distribution(), volumes, null);
+ try {
+ waitForPathToExist(dockerTemp);
+ waitForPathToExist(dockerKeystore);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ // We need a local shell to put the correct permissions on our mounted directory.
+ Shell localShell = new Shell();
+ localShell.run("docker exec --tty " + Docker.getContainerId() + " chown elasticsearch:root " + dockerTemp);
+ localShell.run("docker exec --tty " + Docker.getContainerId() + " chown elasticsearch:root " + dockerTemp.resolve("set-pass.sh"));
+
+ sh.run("bash " + dockerTemp.resolve("set-pass.sh"));
+
+ // copy keystore to temp file to make it available to docker host
+ sh.run("cp " + dockerKeystore + " " + dockerTemp);
+ return tempDirectory.resolve("elasticsearch.keystore");
+ }
+
+ private void createKeystore() throws Exception {
+ Path keystore = installation.config("elasticsearch.keystore");
+ final Installation.Executables bin = installation.executables();
+ bin.keystoreTool.run("create");
+
+ // this is a hack around the fact that we can't run a command in the same session as the same user but not as administrator.
+ // the keystore ends up being owned by the Administrators group, so we manually set it to be owned by the vagrant user here.
+ // from the server's perspective the permissions aren't really different, this is just to reflect what we'd expect in the tests.
+ // when we run these commands as a role user we won't have to do this
+ Platforms.onWindows(() -> {
+ sh.chown(keystore);
+ });
+
+ if (distribution().isDocker()) {
+ try {
+ waitForPathToExist(keystore);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private void rmKeystoreIfExists() {
+ Path keystore = installation.config("elasticsearch.keystore");
+ if (distribution().isDocker()) {
+ try {
+ waitForPathToExist(keystore);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ // Move the auto-created one out of the way, or else the CLI prompts asks us to confirm
+ sh.run("rm " + keystore);
+ } else {
+ if (Files.exists(keystore)) {
+ FileUtils.rm(keystore);
+ }
+ }
+ }
+
+ private void setKeystorePassword(String password) throws Exception {
+ final Installation.Executables bin = installation.executables();
+
+ // set the password by passing it to stdin twice
+ Platforms.onLinux(() -> {
+ bin.keystoreTool.run("passwd", password + "\n" + password + "\n");
+ });
+
+ Platforms.onWindows(() -> {
+ sh.run("Invoke-Command -ScriptBlock {echo \'" + password + "\'; echo \'" + password + "\'} | "
+ + bin.keystoreTool + " passwd");
+ });
+ }
+
+ private void assertPasswordProtectedKeystore() {
+ Shell.Result r = installation.executables().keystoreTool.run("has-passwd");
+ assertThat("keystore should be password protected", r.exitCode, is(0));
+ }
+
+ private void verifyKeystorePermissions() {
+ Path keystore = installation.config("elasticsearch.keystore");
+ switch (distribution.packaging) {
+ case TAR:
+ case ZIP:
+ assertThat(keystore, file(File, ARCHIVE_OWNER, ARCHIVE_OWNER, p660));
+ break;
+ case DEB:
+ case RPM:
+ assertThat(keystore, file(File, "root", "elasticsearch", p660));
+ break;
+ case DOCKER:
+ assertPermissionsAndOwnership(keystore, p660);
+ break;
+ default:
+ throw new IllegalStateException("Unknown Elasticsearch packaging type.");
+ }
+ }
+}
diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageTests.java
index 868cb75a493..d77ee768dc4 100644
--- a/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageTests.java
+++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageTests.java
@@ -105,7 +105,7 @@ public class PackageTests extends PackagingTestCase {
Files.write(installation.envFile, originalEnvFile);
}
- assertThat(FileUtils.slurpAllLogs(installation.logs, "elasticsearch.log", "*.log.gz"),
+ assertThat(FileUtils.slurpAllLogs(installation.logs, "elasticsearch.log", "elasticsearch*.log.gz"),
containsString(systemJavaHome));
}
@@ -162,6 +162,7 @@ public class PackageTests extends PackagingTestCase {
runElasticsearchTests();
verifyPackageInstallation(installation, distribution(), sh); // check startup script didn't change permissions
+ stopElasticsearch();
}
public void test50Remove() throws Exception {
diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/PackagingTestCase.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/PackagingTestCase.java
index c66bb3cb7f3..bf8ad09ad6a 100644
--- a/qa/os/src/test/java/org/elasticsearch/packaging/test/PackagingTestCase.java
+++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/PackagingTestCase.java
@@ -34,6 +34,8 @@ import org.elasticsearch.packaging.util.Installation;
import org.elasticsearch.packaging.util.Packages;
import org.elasticsearch.packaging.util.Platforms;
import org.elasticsearch.packaging.util.Shell;
+import org.junit.After;
+import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
@@ -44,9 +46,12 @@ import org.junit.runner.Description;
import org.junit.runner.RunWith;
import java.nio.file.Files;
+import java.nio.file.Path;
import java.nio.file.Paths;
import static org.elasticsearch.packaging.util.Cleanup.cleanEverything;
+import static org.elasticsearch.packaging.util.Docker.ensureImageIsLoaded;
+import static org.elasticsearch.packaging.util.Docker.removeContainer;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assume.assumeFalse;
@@ -115,9 +120,23 @@ public abstract class PackagingTestCase extends Assert {
@BeforeClass
public static void createShell() throws Exception {
- sh = new Shell();
+ if (distribution().isDocker()) {
+ ensureImageIsLoaded(distribution);
+ sh = new Docker.DockerShell();
+ } else {
+ sh = new Shell();
+ }
}
+ @AfterClass
+ public static void cleanupDocker() {
+ if (distribution().isDocker()) {
+ // runContainer also calls this, so we don't need this method to be annotated as `@After`
+ removeContainer();
+ }
+ }
+
+
@Before
public void setup() throws Exception {
assumeFalse(failed); // skip rest of tests once one fails
@@ -133,6 +152,24 @@ public abstract class PackagingTestCase extends Assert {
}
}
+ @After
+ public void teardown() throws Exception {
+ // move log file so we can avoid false positives when grepping for
+ // messages in logs during test
+ if (installation != null && Files.exists(installation.logs)) {
+ Path logFile = installation.logs.resolve("elasticsearch.log");
+ String prefix = this.getClass().getSimpleName() + "." + testNameRule.getMethodName();
+ if (Files.exists(logFile)) {
+ Path newFile = installation.logs.resolve(prefix + ".elasticsearch.log");
+ FileUtils.mv(logFile, newFile);
+ }
+ for (Path rotatedLogFile : FileUtils.lsGlob(installation.logs, "elasticsearch*.tar.gz")) {
+ Path newRotatedLogFile = installation.logs.resolve(prefix + "." + rotatedLogFile.getFileName());
+ FileUtils.mv(rotatedLogFile, newRotatedLogFile);
+ }
+ }
+ }
+
/** The {@link Distribution} that should be tested in this case */
protected static Distribution distribution() {
return distribution;
@@ -205,7 +242,7 @@ public abstract class PackagingTestCase extends Assert {
switch (distribution.packaging) {
case TAR:
case ZIP:
- return Archives.runElasticsearchStartCommand(installation, sh);
+ return Archives.runElasticsearchStartCommand(installation, sh, "");
case DEB:
case RPM:
return Packages.runElasticsearchStartCommand(sh);
@@ -263,7 +300,18 @@ public abstract class PackagingTestCase extends Assert {
awaitElasticsearchStartup(runElasticsearchStartCommand());
}
- public void assertElasticsearchFailure(Shell.Result result, String expectedMessage) {
+ public Shell.Result startElasticsearchStandardInputPassword(String password) {
+ assertTrue("Only archives support passwords on standard input", distribution().isArchive());
+ return Archives.runElasticsearchStartCommand(installation, sh, password);
+ }
+
+ public Shell.Result startElasticsearchTtyPassword(String password) throws Exception {
+ assertTrue("Only archives support passwords on TTY", distribution().isArchive());
+ return Archives.startElasticsearchWithTty(installation, sh, password);
+ }
+
+
+ public void assertElasticsearchFailure(Shell.Result result, String expectedMessage, Packages.JournaldWrapper journaldWrapper) {
if (Files.exists(installation.logs.resolve("elasticsearch.log"))) {
@@ -277,7 +325,7 @@ public abstract class PackagingTestCase extends Assert {
// For systemd, retrieve the error from journalctl
assertThat(result.stderr, containsString("Job for elasticsearch.service failed"));
- Shell.Result error = sh.run("journalctl --boot --unit elasticsearch.service");
+ Shell.Result error = journaldWrapper.getLogs();
assertThat(error.stdout, containsString(expectedMessage));
} else if (Platforms.WINDOWS == true) {
@@ -297,4 +345,5 @@ public abstract class PackagingTestCase extends Assert {
assertThat(result.stderr, containsString(expectedMessage));
}
}
+
}
diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java
index 7f5502922bb..0c07e54663a 100644
--- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java
+++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java
@@ -245,7 +245,28 @@ public class Archives {
).forEach(configFile -> assertThat(es.config(configFile), file(File, owner, owner, p660)));
}
- public static Shell.Result runElasticsearchStartCommand(Installation installation, Shell sh) {
+ public static Shell.Result startElasticsearch(Installation installation, Shell sh) {
+ return runElasticsearchStartCommand(installation, sh, "");
+ }
+
+ public static Shell.Result startElasticsearchWithTty(Installation installation, Shell sh, String keystorePassword) throws Exception {
+ final Path pidFile = installation.home.resolve("elasticsearch.pid");
+ final Installation.Executables bin = installation.executables();
+
+ // requires the "expect" utility to be installed
+ String script = "expect -c \"$(cat< ELASTICSEARCH_FILES_LINUX = Arrays.asList(
"/usr/share/elasticsearch",
+ "/etc/elasticsearch/elasticsearch.keystore",
"/etc/elasticsearch",
"/var/lib/elasticsearch",
"/var/log/elasticsearch",
diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Distribution.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Distribution.java
index 13b2f31c7e4..72bd79ff7b4 100644
--- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Distribution.java
+++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Distribution.java
@@ -64,6 +64,10 @@ public class Distribution {
return packaging == Packaging.RPM || packaging == Packaging.DEB;
}
+ public boolean isDocker() {
+ return packaging == Packaging.DOCKER;
+ }
+
public enum Packaging {
TAR(".tar.gz", Platforms.LINUX || Platforms.DARWIN),
diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java
index 8c32c7edc32..d43c2219a03 100644
--- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java
+++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java
@@ -60,6 +60,8 @@ public class Docker {
private static final Shell sh = new Shell();
private static final DockerShell dockerShell = new DockerShell();
+ public static final int STARTUP_SLEEP_INTERVAL_MILLISECONDS = 1000;
+ public static final int STARTUP_ATTEMPTS_MAX = 10;
/**
* Tracks the currently running Docker image. An earlier implementation used a fixed container name,
@@ -175,18 +177,18 @@ public class Docker {
do {
try {
// Give the container a chance to crash out
- Thread.sleep(1000);
+ Thread.sleep(STARTUP_SLEEP_INTERVAL_MILLISECONDS);
- psOutput = dockerShell.run("ps -w ax").stdout;
+ psOutput = dockerShell.run("ps -ww ax").stdout;
- if (psOutput.contains("/usr/share/elasticsearch/jdk/bin/java")) {
+ if (psOutput.contains("org.elasticsearch.bootstrap.Elasticsearch")) {
isElasticsearchRunning = true;
break;
}
} catch (Exception e) {
logger.warn("Caught exception while waiting for ES to start", e);
}
- } while (attempt++ < 5);
+ } while (attempt++ < STARTUP_ATTEMPTS_MAX);
if (isElasticsearchRunning == false) {
final Shell.Result dockerLogs = getContainerLogs();
@@ -502,6 +504,13 @@ public class Docker {
}
}
+ /**
+ * @return The ID of the container that this class will be operating on.
+ */
+ public static String getContainerId() {
+ return containerId;
+ }
+
public static JsonNode getJson(String path) throws Exception {
final String pluginsResponse = makeRequest(Request.Get("http://localhost:9200/" + path));
diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/FileUtils.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/FileUtils.java
index eb57e66239e..8d1dff077c3 100644
--- a/qa/os/src/test/java/org/elasticsearch/packaging/util/FileUtils.java
+++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/FileUtils.java
@@ -153,7 +153,7 @@ public class FileUtils {
public static String slurp(Path file) {
try {
- return String.join("\n", Files.readAllLines(file, StandardCharsets.UTF_8));
+ return String.join(System.lineSeparator(), Files.readAllLines(file, StandardCharsets.UTF_8));
} catch (IOException e) {
throw new RuntimeException(e);
}
diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Platforms.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Platforms.java
index b0778bf460e..fa324690bf6 100644
--- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Platforms.java
+++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Platforms.java
@@ -28,6 +28,7 @@ public class Platforms {
public static final boolean LINUX = OS_NAME.startsWith("Linux");
public static final boolean WINDOWS = OS_NAME.startsWith("Windows");
public static final boolean DARWIN = OS_NAME.startsWith("Mac OS X");
+ public static final PlatformAction NO_ACTION = () -> {};
public static String getOsRelease() {
if (LINUX) {
diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Shell.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Shell.java
index 95141aae173..d8fd73f6d1c 100644
--- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Shell.java
+++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Shell.java
@@ -171,7 +171,7 @@ public class Shell {
readFileIfExists(stdErr)
);
throw new IllegalStateException(
- "Timed out running shell command: " + command + "\n" +
+ "Timed out running shell command: " + Arrays.toString(command) + "\n" +
"Result:\n" + result
);
}
diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequest.java
index 1e5e2b07cde..7796fba531c 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequest.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequest.java
@@ -19,30 +19,97 @@
package org.elasticsearch.action.admin.cluster.node.reload;
+import org.elasticsearch.Version;
import org.elasticsearch.action.support.nodes.BaseNodesRequest;
import org.elasticsearch.common.io.stream.StreamInput;
import java.io.IOException;
+import org.elasticsearch.common.CharArrays;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.bytes.BytesArray;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.settings.SecureString;
+
+import java.util.Arrays;
+
/**
- * Request for a reload secure settings action.
+ * Request for a reload secure settings action
*/
public class NodesReloadSecureSettingsRequest extends BaseNodesRequest {
+ /**
+ * The password is used to re-read and decrypt the contents
+ * of the node's keystore (backing the implementation of
+ * {@code SecureSettings}).
+ */
+ @Nullable
+ private SecureString secureSettingsPassword;
+
public NodesReloadSecureSettingsRequest() {
super((String[]) null);
}
public NodesReloadSecureSettingsRequest(StreamInput in) throws IOException {
super(in);
+ if (in.getVersion().onOrAfter(Version.V_7_7_0)) {
+ final BytesReference bytesRef = in.readOptionalBytesReference();
+ if (bytesRef != null) {
+ byte[] bytes = BytesReference.toBytes(bytesRef);
+ try {
+ this.secureSettingsPassword = new SecureString(CharArrays.utf8BytesToChars(bytes));
+ } finally {
+ Arrays.fill(bytes, (byte) 0);
+ }
+ } else {
+ this.secureSettingsPassword = null;
+ }
+ }
}
/**
- * Reload secure settings only on certain nodes, based on the nodes IDs specified. If none are passed, secure settings will be reloaded
- * on all the nodes.
+ * Reload secure settings only on certain nodes, based on the nodes ids
+ * specified. If none are passed, secure settings will be reloaded on all the
+ * nodes.
*/
- public NodesReloadSecureSettingsRequest(final String... nodesIds) {
+ public NodesReloadSecureSettingsRequest(String... nodesIds) {
super(nodesIds);
}
+ @Nullable
+ public SecureString getSecureSettingsPassword() {
+ return secureSettingsPassword;
+ }
+
+ public void setSecureStorePassword(SecureString secureStorePassword) {
+ this.secureSettingsPassword = secureStorePassword;
+ }
+
+ public void closePassword() {
+ if (this.secureSettingsPassword != null) {
+ this.secureSettingsPassword.close();
+ }
+ }
+
+ boolean hasPassword() {
+ return this.secureSettingsPassword != null && this.secureSettingsPassword.length() > 0;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ super.writeTo(out);
+ if (out.getVersion().onOrAfter(Version.V_7_4_0)) {
+ if (this.secureSettingsPassword == null) {
+ out.writeOptionalBytesReference(null);
+ } else {
+ final byte[] passwordBytes = CharArrays.toUtf8Bytes(this.secureSettingsPassword.getChars());
+ try {
+ out.writeOptionalBytesReference(new BytesArray(passwordBytes));
+ } finally {
+ Arrays.fill(passwordBytes, (byte) 0);
+ }
+ }
+ }
+ }
}
diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequestBuilder.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequestBuilder.java
index c8250455e6b..c3c0401efdf 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequestBuilder.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequestBuilder.java
@@ -21,6 +21,7 @@ package org.elasticsearch.action.admin.cluster.node.reload;
import org.elasticsearch.action.support.nodes.NodesOperationRequestBuilder;
import org.elasticsearch.client.ElasticsearchClient;
+import org.elasticsearch.common.settings.SecureString;
/**
* Builder for the reload secure settings nodes request
@@ -32,4 +33,9 @@ public class NodesReloadSecureSettingsRequestBuilder extends NodesOperationReque
super(client, action, new NodesReloadSecureSettingsRequest());
}
+ public NodesReloadSecureSettingsRequestBuilder setSecureStorePassword(SecureString secureStorePassword) {
+ request.setSecureStorePassword(secureStorePassword);
+ return this;
+ }
+
}
diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/TransportNodesReloadSecureSettingsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/TransportNodesReloadSecureSettingsAction.java
index 7d8c39b6422..b8dfe9dc5f5 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/TransportNodesReloadSecureSettingsAction.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/TransportNodesReloadSecureSettingsAction.java
@@ -21,20 +21,25 @@ package org.elasticsearch.action.admin.cluster.node.reload;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.util.Supplier;
+import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ExceptionsHelper;
+import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.FailedNodeException;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.nodes.BaseNodeRequest;
import org.elasticsearch.action.support.nodes.TransportNodesAction;
+import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.settings.KeyStoreWrapper;
+import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.plugins.PluginsService;
import org.elasticsearch.plugins.ReloadablePlugin;
+import org.elasticsearch.tasks.Task;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
@@ -77,15 +82,39 @@ public class TransportNodesReloadSecureSettingsAction extends TransportNodesActi
return new NodesReloadSecureSettingsResponse.NodeResponse(in);
}
+ @Override
+ protected void doExecute(Task task, NodesReloadSecureSettingsRequest request,
+ ActionListener listener) {
+ if (request.hasPassword() && isNodeLocal(request) == false && isNodeTransportTLSEnabled() == false) {
+ request.closePassword();
+ listener.onFailure(
+ new ElasticsearchException("Secure settings cannot be updated cluster wide when TLS for the transport layer" +
+ " is not enabled. Enable TLS or use the API with a `_local` filter on each node."));
+ } else {
+ super.doExecute(task, request, ActionListener.wrap(response -> {
+ request.closePassword();
+ listener.onResponse(response);
+ }, e -> {
+ request.closePassword();
+ listener.onFailure(e);
+ }));
+ }
+ }
+
@Override
protected NodesReloadSecureSettingsResponse.NodeResponse nodeOperation(NodeRequest nodeReloadRequest) {
+ final NodesReloadSecureSettingsRequest request = nodeReloadRequest.request;
+ // We default to using an empty string as the keystore password so that we mimic pre 7.3 API behavior
+ final SecureString secureSettingsPassword = request.hasPassword() ? request.getSecureSettingsPassword() :
+ new SecureString(new char[0]);
try (KeyStoreWrapper keystore = KeyStoreWrapper.load(environment.configFile())) {
// reread keystore from config file
if (keystore == null) {
return new NodesReloadSecureSettingsResponse.NodeResponse(clusterService.localNode(),
new IllegalStateException("Keystore is missing"));
}
- keystore.decrypt(new char[0]);
+ // decrypt the keystore using the password from the request
+ keystore.decrypt(secureSettingsPassword.getChars());
// add the keystore to the original node settings object
final Settings settingsWithKeystore = Settings.builder()
.put(environment.settings(), false)
@@ -106,6 +135,8 @@ public class TransportNodesReloadSecureSettingsAction extends TransportNodesActi
return new NodesReloadSecureSettingsResponse.NodeResponse(clusterService.localNode(), null);
} catch (final Exception e) {
return new NodesReloadSecureSettingsResponse.NodeResponse(clusterService.localNode(), e);
+ } finally {
+ secureSettingsPassword.close();
}
}
@@ -128,4 +159,20 @@ public class TransportNodesReloadSecureSettingsAction extends TransportNodesActi
request.writeTo(out);
}
}
+
+ /**
+ * Returns true if the node is configured for TLS on the transport layer
+ */
+ private boolean isNodeTransportTLSEnabled() {
+ return transportService.isTransportSecure();
+ }
+
+ private boolean isNodeLocal(NodesReloadSecureSettingsRequest request) {
+ if (null == request.concreteNodes()) {
+ resolveRequest(request, clusterService.state());
+ assert request.concreteNodes() != null;
+ }
+ final DiscoveryNode[] nodes = request.concreteNodes();
+ return nodes.length == 1 && nodes[0].getId().equals(clusterService.localNode().getId());
+ }
}
diff --git a/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java b/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java
index 6b5896ac8d4..462c2b897b3 100644
--- a/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java
+++ b/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java
@@ -26,6 +26,9 @@ import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.ConsoleAppender;
import org.apache.logging.log4j.core.config.Configurator;
import org.apache.lucene.util.Constants;
+import org.elasticsearch.cli.KeyStoreAwareCommand;
+import org.elasticsearch.cli.Terminal;
+import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.core.internal.io.IOUtils;
import org.apache.lucene.util.StringHelper;
import org.elasticsearch.ElasticsearchException;
@@ -52,9 +55,12 @@ import org.elasticsearch.node.NodeValidationException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
@@ -236,21 +242,59 @@ final class Bootstrap {
throw new BootstrapException(e);
}
+ SecureString password;
try {
+ if (keystore != null && keystore.hasPassword()) {
+ password = readPassphrase(System.in, KeyStoreAwareCommand.MAX_PASSPHRASE_LENGTH);
+ } else {
+ password = new SecureString(new char[0]);
+ }
+ } catch (IOException e) {
+ throw new BootstrapException(e);
+ }
+
+ try{
if (keystore == null) {
final KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.create();
keyStoreWrapper.save(initialEnv.configFile(), new char[0]);
return keyStoreWrapper;
} else {
- keystore.decrypt(new char[0] /* TODO: read password from stdin */);
- KeyStoreWrapper.upgrade(keystore, initialEnv.configFile(), new char[0]);
+ keystore.decrypt(password.getChars());
+ KeyStoreWrapper.upgrade(keystore, initialEnv.configFile(), password.getChars());
}
} catch (Exception e) {
throw new BootstrapException(e);
+ } finally {
+ password.close();
}
return keystore;
}
+ // visible for tests
+ /**
+ * Read from an InputStream up to the first carriage return or newline,
+ * returning no more than maxLength characters.
+ */
+ static SecureString readPassphrase(InputStream stream, int maxLength) throws IOException {
+ SecureString passphrase;
+
+ try(InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) {
+ passphrase = new SecureString(Terminal.readLineToCharArray(reader, maxLength));
+ } catch (RuntimeException e) {
+ if (e.getMessage().startsWith("Input exceeded maximum length")) {
+ throw new IllegalStateException("Password exceeded maximum length of " + maxLength, e);
+ }
+ throw e;
+ }
+
+ if (passphrase.length() == 0) {
+ passphrase.close();
+ throw new IllegalStateException("Keystore passphrase required but none provided.");
+ }
+
+ return passphrase;
+ }
+
private static Environment createEnvironment(
final Path pidFile,
final SecureSettings secureSettings,
diff --git a/server/src/main/java/org/elasticsearch/cli/KeyStoreAwareCommand.java b/server/src/main/java/org/elasticsearch/cli/KeyStoreAwareCommand.java
new file mode 100644
index 00000000000..dd26366f9b0
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/cli/KeyStoreAwareCommand.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.cli;
+
+import joptsimple.OptionSet;
+import org.elasticsearch.common.settings.KeyStoreWrapper;
+import org.elasticsearch.common.settings.SecureString;
+import org.elasticsearch.env.Environment;
+
+import javax.crypto.AEADBadTagException;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
+/**
+ * An {@link org.elasticsearch.cli.EnvironmentAwareCommand} that needs to access the elasticsearch keystore, possibly
+ * decrypting it if it is password protected.
+ */
+public abstract class KeyStoreAwareCommand extends EnvironmentAwareCommand {
+ public KeyStoreAwareCommand(String description) {
+ super(description);
+ }
+
+ /** Arbitrarily chosen maximum passphrase length */
+ public static final int MAX_PASSPHRASE_LENGTH = 128;
+
+ /**
+ * Reads the keystore password from the {@link Terminal}, prompting for verification where applicable and returns it as a
+ * {@link SecureString}.
+ *
+ * @param terminal the terminal to use for user inputs
+ * @param withVerification whether the user should be prompted for password verification
+ * @return a SecureString with the password the user entered
+ * @throws UserException If the user is prompted for verification and enters a different password
+ */
+ protected static SecureString readPassword(Terminal terminal, boolean withVerification) throws UserException {
+ final char[] passwordArray;
+ if (withVerification) {
+ passwordArray = terminal.readSecret("Enter new password for the elasticsearch keystore (empty for no password): ",
+ MAX_PASSPHRASE_LENGTH);
+ char[] passwordVerification = terminal.readSecret("Enter same password again: ",
+ MAX_PASSPHRASE_LENGTH);
+ if (Arrays.equals(passwordArray, passwordVerification) == false) {
+ throw new UserException(ExitCodes.DATA_ERROR, "Passwords are not equal, exiting.");
+ }
+ Arrays.fill(passwordVerification, '\u0000');
+ } else {
+ passwordArray = terminal.readSecret("Enter password for the elasticsearch keystore : ");
+ }
+ return new SecureString(passwordArray);
+ }
+
+ /**
+ * Decrypt the {@code keyStore}, prompting the user to enter the password in the {@link Terminal} if it is password protected
+ */
+ protected static void decryptKeyStore(KeyStoreWrapper keyStore, Terminal terminal)
+ throws UserException, GeneralSecurityException, IOException {
+ try (SecureString keystorePassword = keyStore.hasPassword() ?
+ readPassword(terminal, false) : new SecureString(new char[0])) {
+ keyStore.decrypt(keystorePassword.getChars());
+ } catch (SecurityException e) {
+ if (e.getCause() instanceof AEADBadTagException) {
+ throw new UserException(ExitCodes.DATA_ERROR, "Wrong password for elasticsearch.keystore");
+ }
+ }
+ }
+
+ protected abstract void execute(Terminal terminal, OptionSet options, Environment env) throws Exception;
+}
diff --git a/server/src/main/java/org/elasticsearch/common/settings/AddFileKeyStoreCommand.java b/server/src/main/java/org/elasticsearch/common/settings/AddFileKeyStoreCommand.java
index f5b3cb9cf71..544c58e0388 100644
--- a/server/src/main/java/org/elasticsearch/common/settings/AddFileKeyStoreCommand.java
+++ b/server/src/main/java/org/elasticsearch/common/settings/AddFileKeyStoreCommand.java
@@ -26,7 +26,6 @@ import java.util.List;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
-import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
@@ -37,14 +36,14 @@ import org.elasticsearch.env.Environment;
/**
* A subcommand for the keystore cli which adds a file setting.
*/
-class AddFileKeyStoreCommand extends EnvironmentAwareCommand {
+class AddFileKeyStoreCommand extends BaseKeyStoreCommand {
- private final OptionSpec forceOption;
private final OptionSpec arguments;
AddFileKeyStoreCommand() {
- super("Add a file setting to the keystore");
- this.forceOption = parser.acceptsAll(Arrays.asList("f", "force"), "Overwrite existing setting without prompting");
+ super("Add a file setting to the keystore", false);
+ this.forceOption = parser.acceptsAll(Arrays.asList("f", "force"),
+ "Overwrite existing setting without prompting, creating keystore if necessary");
// jopt simple has issue with multiple non options, so we just get one set of them here
// and convert to File when necessary
// see https://github.com/jopt-simple/jopt-simple/issues/103
@@ -52,27 +51,14 @@ class AddFileKeyStoreCommand extends EnvironmentAwareCommand {
}
@Override
- protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
- KeyStoreWrapper keystore = KeyStoreWrapper.load(env.configFile());
- if (keystore == null) {
- if (options.has(forceOption) == false &&
- terminal.promptYesNo("The elasticsearch keystore does not exist. Do you want to create it?", false) == false) {
- terminal.println("Exiting without creating keystore.");
- return;
- }
- keystore = KeyStoreWrapper.create();
- keystore.save(env.configFile(), new char[0] /* always use empty passphrase for auto created keystore */);
- terminal.println("Created elasticsearch keystore in " + env.configFile());
- } else {
- keystore.decrypt(new char[0] /* TODO: prompt for password when they are supported */);
- }
-
+ protected void executeCommand(Terminal terminal, OptionSet options, Environment env) throws Exception {
List argumentValues = arguments.values(options);
if (argumentValues.size() == 0) {
throw new UserException(ExitCodes.USAGE, "Missing setting name");
}
String setting = argumentValues.get(0);
- if (keystore.getSettingNames().contains(setting) && options.has(forceOption) == false) {
+ final KeyStoreWrapper keyStore = getKeyStore();
+ if (keyStore.getSettingNames().contains(setting) && options.has(forceOption) == false) {
if (terminal.promptYesNo("Setting " + setting + " already exists. Overwrite?", false) == false) {
terminal.println("Exiting without modifying keystore.");
return;
@@ -90,11 +76,11 @@ class AddFileKeyStoreCommand extends EnvironmentAwareCommand {
throw new UserException(ExitCodes.USAGE, "Unrecognized extra arguments [" +
String.join(", ", argumentValues.subList(2, argumentValues.size())) + "] after filepath");
}
- keystore.setFile(setting, Files.readAllBytes(file));
- keystore.save(env.configFile(), new char[0]);
+ keyStore.setFile(setting, Files.readAllBytes(file));
+ keyStore.save(env.configFile(), getKeyStorePassword().getChars());
}
- @SuppressForbidden(reason="file arg for cli")
+ @SuppressForbidden(reason = "file arg for cli")
private Path getPath(String file) {
return PathUtils.get(file);
}
diff --git a/server/src/main/java/org/elasticsearch/common/settings/AddStringKeyStoreCommand.java b/server/src/main/java/org/elasticsearch/common/settings/AddStringKeyStoreCommand.java
index ba006cd36f3..6fba5acce75 100644
--- a/server/src/main/java/org/elasticsearch/common/settings/AddStringKeyStoreCommand.java
+++ b/server/src/main/java/org/elasticsearch/common/settings/AddStringKeyStoreCommand.java
@@ -28,7 +28,6 @@ import java.util.Arrays;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
-import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
@@ -37,16 +36,16 @@ import org.elasticsearch.env.Environment;
/**
* A subcommand for the keystore cli which adds a string setting.
*/
-class AddStringKeyStoreCommand extends EnvironmentAwareCommand {
+class AddStringKeyStoreCommand extends BaseKeyStoreCommand {
private final OptionSpec stdinOption;
- private final OptionSpec forceOption;
private final OptionSpec arguments;
AddStringKeyStoreCommand() {
- super("Add a string setting to the keystore");
+ super("Add a string setting to the keystore", false);
this.stdinOption = parser.acceptsAll(Arrays.asList("x", "stdin"), "Read setting value from stdin");
- this.forceOption = parser.acceptsAll(Arrays.asList("f", "force"), "Overwrite existing setting without prompting");
+ this.forceOption = parser.acceptsAll(Arrays.asList("f", "force"),
+ "Overwrite existing setting without prompting, creating keystore if necessary");
this.arguments = parser.nonOptions("setting name");
}
@@ -56,26 +55,13 @@ class AddStringKeyStoreCommand extends EnvironmentAwareCommand {
}
@Override
- protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
- KeyStoreWrapper keystore = KeyStoreWrapper.load(env.configFile());
- if (keystore == null) {
- if (options.has(forceOption) == false &&
- terminal.promptYesNo("The elasticsearch keystore does not exist. Do you want to create it?", false) == false) {
- terminal.println("Exiting without creating keystore.");
- return;
- }
- keystore = KeyStoreWrapper.create();
- keystore.save(env.configFile(), new char[0] /* always use empty passphrase for auto created keystore */);
- terminal.println("Created elasticsearch keystore in " + env.configFile());
- } else {
- keystore.decrypt(new char[0] /* TODO: prompt for password when they are supported */);
- }
-
+ protected void executeCommand(Terminal terminal, OptionSet options, Environment env) throws Exception {
String setting = arguments.value(options);
if (setting == null) {
throw new UserException(ExitCodes.USAGE, "The setting name can not be null");
}
- if (keystore.getSettingNames().contains(setting) && options.has(forceOption) == false) {
+ final KeyStoreWrapper keyStore = getKeyStore();
+ if (keyStore.getSettingNames().contains(setting) && options.has(forceOption) == false) {
if (terminal.promptYesNo("Setting " + setting + " already exists. Overwrite?", false) == false) {
terminal.println("Exiting without modifying keystore.");
return;
@@ -100,10 +86,11 @@ class AddStringKeyStoreCommand extends EnvironmentAwareCommand {
}
try {
- keystore.setString(setting, value);
- } catch (final IllegalArgumentException e) {
+ keyStore.setString(setting, value);
+ } catch (IllegalArgumentException e) {
throw new UserException(ExitCodes.DATA_ERROR, e.getMessage());
}
- keystore.save(env.configFile(), new char[0]);
+ keyStore.save(env.configFile(), getKeyStorePassword().getChars());
+
}
}
diff --git a/server/src/main/java/org/elasticsearch/common/settings/BaseKeyStoreCommand.java b/server/src/main/java/org/elasticsearch/common/settings/BaseKeyStoreCommand.java
new file mode 100644
index 00000000000..493d455e42f
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/common/settings/BaseKeyStoreCommand.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.common.settings;
+
+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.env.Environment;
+
+import java.nio.file.Path;
+
+public abstract class BaseKeyStoreCommand extends KeyStoreAwareCommand {
+
+ private KeyStoreWrapper keyStore;
+ private SecureString keyStorePassword;
+ private final boolean keyStoreMustExist;
+ OptionSpec forceOption;
+
+ public BaseKeyStoreCommand(String description, boolean keyStoreMustExist) {
+ super(description);
+ this.keyStoreMustExist = keyStoreMustExist;
+ }
+
+ @Override
+ protected final void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
+ try {
+ final Path configFile = env.configFile();
+ keyStore = KeyStoreWrapper.load(configFile);
+ if (keyStore == null) {
+ if (keyStoreMustExist) {
+ throw new UserException(ExitCodes.DATA_ERROR, "Elasticsearch keystore not found at [" +
+ KeyStoreWrapper.keystorePath(env.configFile()) + "]. Use 'create' command to create one.");
+ } else if (options.has(forceOption) == false) {
+ if (terminal.promptYesNo("The elasticsearch keystore does not exist. Do you want to create it?", false) == false) {
+ terminal.println("Exiting without creating keystore.");
+ return;
+ }
+ }
+ keyStorePassword = new SecureString(new char[0]);
+ keyStore = KeyStoreWrapper.create();
+ keyStore.save(configFile, keyStorePassword.getChars());
+ } else {
+ keyStorePassword = keyStore.hasPassword() ? readPassword(terminal, false) : new SecureString(new char[0]);
+ keyStore.decrypt(keyStorePassword.getChars());
+ }
+ executeCommand(terminal, options, env);
+ } catch (SecurityException e) {
+ throw new UserException(ExitCodes.DATA_ERROR, e.getMessage());
+ } finally {
+ if (keyStorePassword != null) {
+ keyStorePassword.close();
+ }
+ }
+ }
+
+ protected KeyStoreWrapper getKeyStore() {
+ return keyStore;
+ }
+
+ protected SecureString getKeyStorePassword() {
+ return keyStorePassword;
+ }
+
+ /**
+ * This is called after the keystore password has been read from the stdin and the keystore is decrypted and
+ * loaded. The keystore and keystore passwords are available to classes extending {@link BaseKeyStoreCommand}
+ * using {@link BaseKeyStoreCommand#getKeyStore()} and {@link BaseKeyStoreCommand#getKeyStorePassword()}
+ * respectively.
+ */
+ protected abstract void executeCommand(Terminal terminal, OptionSet options, Environment env) throws Exception;
+}
diff --git a/server/src/main/java/org/elasticsearch/common/settings/ChangeKeyStorePasswordCommand.java b/server/src/main/java/org/elasticsearch/common/settings/ChangeKeyStorePasswordCommand.java
new file mode 100644
index 00000000000..526201ede8f
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/common/settings/ChangeKeyStorePasswordCommand.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.common.settings;
+
+import joptsimple.OptionSet;
+import org.elasticsearch.cli.ExitCodes;
+import org.elasticsearch.cli.Terminal;
+import org.elasticsearch.cli.UserException;
+import org.elasticsearch.env.Environment;
+
+/**
+ * A sub-command for the keystore cli which changes the password.
+ */
+class ChangeKeyStorePasswordCommand extends BaseKeyStoreCommand {
+
+ ChangeKeyStorePasswordCommand() {
+ super("Changes the password of a keystore", true);
+ }
+
+ @Override
+ protected void executeCommand(Terminal terminal, OptionSet options, Environment env) throws Exception {
+ try (SecureString newPassword = readPassword(terminal, true)) {
+ final KeyStoreWrapper keyStore = getKeyStore();
+ keyStore.save(env.configFile(), newPassword.getChars());
+ terminal.println("Elasticsearch keystore password changed successfully.");
+ } catch (SecurityException e) {
+ throw new UserException(ExitCodes.DATA_ERROR, e.getMessage());
+ }
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/common/settings/CreateKeyStoreCommand.java b/server/src/main/java/org/elasticsearch/common/settings/CreateKeyStoreCommand.java
index 3529d7f6810..c8833650581 100644
--- a/server/src/main/java/org/elasticsearch/common/settings/CreateKeyStoreCommand.java
+++ b/server/src/main/java/org/elasticsearch/common/settings/CreateKeyStoreCommand.java
@@ -21,41 +21,44 @@ package org.elasticsearch.common.settings;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Arrays;
import joptsimple.OptionSet;
-import org.elasticsearch.cli.EnvironmentAwareCommand;
+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.env.Environment;
/**
- * A subcommand for the keystore cli to create a new keystore.
+ * A sub-command for the keystore cli to create a new keystore.
*/
-class CreateKeyStoreCommand extends EnvironmentAwareCommand {
+class CreateKeyStoreCommand extends KeyStoreAwareCommand {
+
+ private final OptionSpec passwordOption;
CreateKeyStoreCommand() {
super("Creates a new elasticsearch keystore");
+ this.passwordOption = parser.acceptsAll(Arrays.asList("p", "password"), "Prompt for password to encrypt the keystore");
}
@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
- Path keystoreFile = KeyStoreWrapper.keystorePath(env.configFile());
- if (Files.exists(keystoreFile)) {
- if (terminal.promptYesNo("An elasticsearch keystore already exists. Overwrite?", false) == false) {
- terminal.println("Exiting without creating keystore.");
- return;
+ try (SecureString password = options.has(passwordOption) ?
+ readPassword(terminal, true) : new SecureString(new char[0])) {
+ Path keystoreFile = KeyStoreWrapper.keystorePath(env.configFile());
+ if (Files.exists(keystoreFile)) {
+ if (terminal.promptYesNo("An elasticsearch keystore already exists. Overwrite?", false) == false) {
+ terminal.println("Exiting without creating keystore.");
+ return;
+ }
}
+ KeyStoreWrapper keystore = KeyStoreWrapper.create();
+ keystore.save(env.configFile(), password.getChars());
+ terminal.println("Created elasticsearch keystore in " + KeyStoreWrapper.keystorePath(env.configFile()));
+ } catch (SecurityException e) {
+ throw new UserException(ExitCodes.IO_ERROR, "Error creating the elasticsearch keystore.");
}
-
-
- char[] password = new char[0];// terminal.readSecret("Enter passphrase (empty for no passphrase): ");
- /* TODO: uncomment when entering passwords on startup is supported
- char[] passwordRepeat = terminal.readSecret("Enter same passphrase again: ");
- if (Arrays.equals(password, passwordRepeat) == false) {
- throw new UserException(ExitCodes.DATA_ERROR, "Passphrases are not equal, exiting.");
- }*/
-
- KeyStoreWrapper keystore = KeyStoreWrapper.create();
- keystore.save(env.configFile(), password);
- terminal.println("Created elasticsearch keystore in " + env.configFile());
}
}
diff --git a/server/src/main/java/org/elasticsearch/common/settings/HasPasswordKeyStoreCommand.java b/server/src/main/java/org/elasticsearch/common/settings/HasPasswordKeyStoreCommand.java
new file mode 100644
index 00000000000..d3cc8529971
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/common/settings/HasPasswordKeyStoreCommand.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.common.settings;
+
+import joptsimple.OptionSet;
+import org.elasticsearch.cli.KeyStoreAwareCommand;
+import org.elasticsearch.cli.Terminal;
+import org.elasticsearch.cli.UserException;
+import org.elasticsearch.env.Environment;
+
+import java.nio.file.Path;
+
+public class HasPasswordKeyStoreCommand extends KeyStoreAwareCommand {
+
+ static final int NO_PASSWORD_EXIT_CODE = 1;
+
+ HasPasswordKeyStoreCommand() {
+ super("Succeeds if the keystore exists and is password-protected, " +
+ "fails with exit code " + NO_PASSWORD_EXIT_CODE + " otherwise.");
+ }
+
+ @Override
+ protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
+ final Path configFile = env.configFile();
+ final KeyStoreWrapper keyStore = KeyStoreWrapper.load(configFile);
+
+ // We handle error printing here so we can respect the "--silent" flag
+ // We have to throw an exception to get a nonzero exit code
+ if (keyStore == null) {
+ terminal.errorPrintln(Terminal.Verbosity.NORMAL, "ERROR: Elasticsearch keystore not found");
+ throw new UserException(NO_PASSWORD_EXIT_CODE, null);
+ }
+ if (keyStore.hasPassword() == false) {
+ terminal.errorPrintln(Terminal.Verbosity.NORMAL, "ERROR: Keystore is not password-protected");
+ throw new UserException(NO_PASSWORD_EXIT_CODE, null);
+ }
+
+ terminal.println(Terminal.Verbosity.NORMAL, "Keystore is password-protected");
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/common/settings/KeyStoreCli.java b/server/src/main/java/org/elasticsearch/common/settings/KeyStoreCli.java
index 19a453f7e90..f08c83432de 100644
--- a/server/src/main/java/org/elasticsearch/common/settings/KeyStoreCli.java
+++ b/server/src/main/java/org/elasticsearch/common/settings/KeyStoreCli.java
@@ -35,6 +35,8 @@ public class KeyStoreCli extends LoggingAwareMultiCommand {
subcommands.put("add-file", new AddFileKeyStoreCommand());
subcommands.put("remove", new RemoveSettingKeyStoreCommand());
subcommands.put("upgrade", new UpgradeKeyStoreCommand());
+ subcommands.put("passwd", new ChangeKeyStorePasswordCommand());
+ subcommands.put("has-passwd", new HasPasswordKeyStoreCommand());
}
public static void main(String[] args) throws Exception {
diff --git a/server/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java b/server/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java
index db378922655..d3080df034c 100644
--- a/server/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java
+++ b/server/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java
@@ -34,6 +34,7 @@ import org.elasticsearch.cli.UserException;
import org.elasticsearch.common.Randomness;
import org.elasticsearch.common.hash.MessageDigests;
+import javax.crypto.AEADBadTagException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
@@ -378,6 +379,9 @@ public class KeyStoreWrapper implements SecureSettings {
throw new SecurityException("Keystore has been corrupted or tampered with");
}
} catch (IOException e) {
+ if (e.getCause() instanceof AEADBadTagException) {
+ throw new SecurityException("Provided keystore password was incorrect", e);
+ }
throw new SecurityException("Keystore has been corrupted or tampered with", e);
}
}
@@ -580,7 +584,9 @@ public class KeyStoreWrapper implements SecureSettings {
}
}
- /** Set a string setting. */
+ /**
+ * Set a string setting.
+ */
synchronized void setString(String setting, char[] value) {
ensureOpen();
validateSettingName(setting);
@@ -593,7 +599,9 @@ public class KeyStoreWrapper implements SecureSettings {
}
}
- /** Set a file setting. */
+ /**
+ * Set a file setting.
+ */
synchronized void setFile(String setting, byte[] bytes) {
ensureOpen();
validateSettingName(setting);
@@ -604,7 +612,9 @@ public class KeyStoreWrapper implements SecureSettings {
}
}
- /** Remove the given setting from the keystore. */
+ /**
+ * Remove the given setting from the keystore.
+ */
void remove(String setting) {
ensureOpen();
Entry oldEntry = entries.get().remove(setting);
diff --git a/server/src/main/java/org/elasticsearch/common/settings/ListKeyStoreCommand.java b/server/src/main/java/org/elasticsearch/common/settings/ListKeyStoreCommand.java
index 8eef02f2131..edd8a68cc6f 100644
--- a/server/src/main/java/org/elasticsearch/common/settings/ListKeyStoreCommand.java
+++ b/server/src/main/java/org/elasticsearch/common/settings/ListKeyStoreCommand.java
@@ -25,31 +25,22 @@ import java.util.Collections;
import java.util.List;
import joptsimple.OptionSet;
-import org.elasticsearch.cli.EnvironmentAwareCommand;
-import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
-import org.elasticsearch.cli.UserException;
import org.elasticsearch.env.Environment;
/**
* A subcommand for the keystore cli to list all settings in the keystore.
*/
-class ListKeyStoreCommand extends EnvironmentAwareCommand {
+class ListKeyStoreCommand extends BaseKeyStoreCommand {
ListKeyStoreCommand() {
- super("List entries in the keystore");
+ super("List entries in the keystore", true);
}
@Override
- protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
- KeyStoreWrapper keystore = KeyStoreWrapper.load(env.configFile());
- if (keystore == null) {
- throw new UserException(ExitCodes.DATA_ERROR, "Elasticsearch keystore not found. Use 'create' command to create one.");
- }
-
- keystore.decrypt(new char[0] /* TODO: prompt for password when they are supported */);
-
- List sortedEntries = new ArrayList<>(keystore.getSettingNames());
+ protected void executeCommand(Terminal terminal, OptionSet options, Environment env) throws Exception {
+ final KeyStoreWrapper keyStore = getKeyStore();
+ List sortedEntries = new ArrayList<>(keyStore.getSettingNames());
Collections.sort(sortedEntries);
for (String entry : sortedEntries) {
terminal.println(entry);
diff --git a/server/src/main/java/org/elasticsearch/common/settings/RemoveSettingKeyStoreCommand.java b/server/src/main/java/org/elasticsearch/common/settings/RemoveSettingKeyStoreCommand.java
index 9a83375e6e0..6e839d4f331 100644
--- a/server/src/main/java/org/elasticsearch/common/settings/RemoveSettingKeyStoreCommand.java
+++ b/server/src/main/java/org/elasticsearch/common/settings/RemoveSettingKeyStoreCommand.java
@@ -23,7 +23,6 @@ import java.util.List;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
-import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
@@ -32,35 +31,28 @@ import org.elasticsearch.env.Environment;
/**
* A subcommand for the keystore cli to remove a setting.
*/
-class RemoveSettingKeyStoreCommand extends EnvironmentAwareCommand {
+class RemoveSettingKeyStoreCommand extends BaseKeyStoreCommand {
private final OptionSpec arguments;
RemoveSettingKeyStoreCommand() {
- super("Remove a setting from the keystore");
+ super("Remove a setting from the keystore", true);
arguments = parser.nonOptions("setting names");
}
@Override
- protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
+ protected void executeCommand(Terminal terminal, OptionSet options, Environment env) throws Exception {
List settings = arguments.values(options);
if (settings.isEmpty()) {
throw new UserException(ExitCodes.USAGE, "Must supply at least one setting to remove");
}
-
- KeyStoreWrapper keystore = KeyStoreWrapper.load(env.configFile());
- if (keystore == null) {
- throw new UserException(ExitCodes.DATA_ERROR, "Elasticsearch keystore not found. Use 'create' command to create one.");
- }
-
- keystore.decrypt(new char[0] /* TODO: prompt for password when they are supported */);
-
+ final KeyStoreWrapper keyStore = getKeyStore();
for (String setting : arguments.values(options)) {
- if (keystore.getSettingNames().contains(setting) == false) {
+ if (keyStore.getSettingNames().contains(setting) == false) {
throw new UserException(ExitCodes.CONFIG, "Setting [" + setting + "] does not exist in the keystore.");
}
- keystore.remove(setting);
+ keyStore.remove(setting);
}
- keystore.save(env.configFile(), new char[0]);
+ keyStore.save(env.configFile(), getKeyStorePassword().getChars());
}
}
diff --git a/server/src/main/java/org/elasticsearch/common/settings/UpgradeKeyStoreCommand.java b/server/src/main/java/org/elasticsearch/common/settings/UpgradeKeyStoreCommand.java
index 6338f40ea05..640a76432d3 100644
--- a/server/src/main/java/org/elasticsearch/common/settings/UpgradeKeyStoreCommand.java
+++ b/server/src/main/java/org/elasticsearch/common/settings/UpgradeKeyStoreCommand.java
@@ -20,31 +20,21 @@
package org.elasticsearch.common.settings;
import joptsimple.OptionSet;
-import org.elasticsearch.cli.EnvironmentAwareCommand;
-import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
-import org.elasticsearch.cli.UserException;
import org.elasticsearch.env.Environment;
/**
* A sub-command for the keystore CLI that enables upgrading the keystore format.
*/
-public class UpgradeKeyStoreCommand extends EnvironmentAwareCommand {
+public class UpgradeKeyStoreCommand extends BaseKeyStoreCommand {
UpgradeKeyStoreCommand() {
- super("Upgrade the keystore format");
+ super("Upgrade the keystore format", true);
}
@Override
- protected void execute(final Terminal terminal, final OptionSet options, final Environment env) throws Exception {
- final KeyStoreWrapper wrapper = KeyStoreWrapper.load(env.configFile());
- if (wrapper == null) {
- throw new UserException(
- ExitCodes.CONFIG,
- "keystore does not exist at [" + KeyStoreWrapper.keystorePath(env.configFile()) + "]");
- }
- wrapper.decrypt(new char[0]);
- KeyStoreWrapper.upgrade(wrapper, env.configFile(), new char[0]);
+ protected void executeCommand(final Terminal terminal, final OptionSet options, final Environment env) throws Exception {
+ KeyStoreWrapper.upgrade(getKeyStore(), env.configFile(), getKeyStorePassword().getChars());
}
}
diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestReloadSecureSettingsAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestReloadSecureSettingsAction.java
index cb21c7e30da..e5f85c569ee 100644
--- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestReloadSecureSettingsAction.java
+++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestReloadSecureSettingsAction.java
@@ -19,10 +19,14 @@
package org.elasticsearch.rest.action.admin.cluster;
+import org.elasticsearch.action.admin.cluster.node.reload.NodesReloadSecureSettingsRequest;
import org.elasticsearch.action.admin.cluster.node.reload.NodesReloadSecureSettingsRequestBuilder;
import org.elasticsearch.action.admin.cluster.node.reload.NodesReloadSecureSettingsResponse;
import org.elasticsearch.client.node.NodeClient;
+import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.settings.SecureString;
+import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.BytesRestResponse;
@@ -39,6 +43,14 @@ import static org.elasticsearch.rest.RestRequest.Method.POST;
public final class RestReloadSecureSettingsAction extends BaseRestHandler {
+ static final ObjectParser PARSER =
+ new ObjectParser<>("reload_secure_settings", NodesReloadSecureSettingsRequest::new);
+
+ static {
+ PARSER.declareString((request, value) -> request.setSecureStorePassword(new SecureString(value.toCharArray())),
+ new ParseField("secure_settings_password"));
+ }
+
public RestReloadSecureSettingsAction(RestController controller) {
controller.registerHandler(POST, "/_nodes/reload_secure_settings", this);
controller.registerHandler(POST, "/_nodes/{nodeId}/reload_secure_settings", this);
@@ -53,22 +65,28 @@ public final class RestReloadSecureSettingsAction extends BaseRestHandler {
public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
final String[] nodesIds = Strings.splitStringByCommaToArray(request.param("nodeId"));
final NodesReloadSecureSettingsRequestBuilder nodesRequestBuilder = client.admin()
- .cluster()
- .prepareReloadSecureSettings()
- .setTimeout(request.param("timeout"))
- .setNodesIds(nodesIds);
+ .cluster()
+ .prepareReloadSecureSettings()
+ .setTimeout(request.param("timeout"))
+ .setNodesIds(nodesIds);
+ request.withContentOrSourceParamParserOrNull(parser -> {
+ if (parser != null) {
+ final NodesReloadSecureSettingsRequest nodesRequest = nodesRequestBuilder.request();
+ nodesRequestBuilder.setSecureStorePassword(nodesRequest.getSecureSettingsPassword());
+ }
+ });
+
return channel -> nodesRequestBuilder
.execute(new RestBuilderListener(channel) {
@Override
public RestResponse buildResponse(NodesReloadSecureSettingsResponse response, XContentBuilder builder)
- throws Exception {
+ throws Exception {
builder.startObject();
- {
- RestActions.buildNodesHeader(builder, channel.request(), response);
- builder.field("cluster_name", response.getClusterName().value());
- response.toXContent(builder, channel.request());
- }
+ RestActions.buildNodesHeader(builder, channel.request(), response);
+ builder.field("cluster_name", response.getClusterName().value());
+ response.toXContent(builder, channel.request());
builder.endObject();
+ nodesRequestBuilder.request().closePassword();
return new BytesRestResponse(RestStatus.OK, builder);
}
});
diff --git a/server/src/main/java/org/elasticsearch/transport/Transport.java b/server/src/main/java/org/elasticsearch/transport/Transport.java
index f89692caa73..32499ce19d5 100644
--- a/server/src/main/java/org/elasticsearch/transport/Transport.java
+++ b/server/src/main/java/org/elasticsearch/transport/Transport.java
@@ -52,6 +52,10 @@ public interface Transport extends LifecycleComponent {
void setMessageListener(TransportMessageListener listener);
+ default boolean isSecure() {
+ return false;
+ }
+
/**
* The address the transport is bound on.
*/
diff --git a/server/src/main/java/org/elasticsearch/transport/TransportService.java b/server/src/main/java/org/elasticsearch/transport/TransportService.java
index e72719d9877..fa84a2aa0ca 100644
--- a/server/src/main/java/org/elasticsearch/transport/TransportService.java
+++ b/server/src/main/java/org/elasticsearch/transport/TransportService.java
@@ -310,6 +310,10 @@ public class TransportService extends AbstractLifecycleComponent implements Tran
return transport.getStats();
}
+ public boolean isTransportSecure() {
+ return transport.isSecure();
+ }
+
public BoundTransportAddress boundAddress() {
return transport.boundAddress();
}
diff --git a/server/src/test/java/org/elasticsearch/action/admin/ReloadSecureSettingsIT.java b/server/src/test/java/org/elasticsearch/action/admin/ReloadSecureSettingsIT.java
index 3f9e258ffec..fbd3fe0432e 100644
--- a/server/src/test/java/org/elasticsearch/action/admin/ReloadSecureSettingsIT.java
+++ b/server/src/test/java/org/elasticsearch/action/admin/ReloadSecureSettingsIT.java
@@ -19,10 +19,13 @@
package org.elasticsearch.action.admin;
+import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.admin.cluster.node.reload.NodesReloadSecureSettingsResponse;
+import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.KeyStoreWrapper;
import org.elasticsearch.common.settings.SecureSettings;
+import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.plugins.Plugin;
@@ -42,50 +45,53 @@ import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
-import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.containsString;
+@ESIntegTestCase.ClusterScope(minNumDataNodes = 2)
public class ReloadSecureSettingsIT extends ESIntegTestCase {
public void testMissingKeystoreFile() throws Exception {
final PluginsService pluginsService = internalCluster().getInstance(PluginsService.class);
final MockReloadablePlugin mockReloadablePlugin = pluginsService.filterPlugins(MockReloadablePlugin.class)
- .stream().findFirst().get();
+ .stream().findFirst().get();
final Environment environment = internalCluster().getInstance(Environment.class);
final AtomicReference reloadSettingsError = new AtomicReference<>();
// keystore file should be missing for this test case
Files.deleteIfExists(KeyStoreWrapper.keystorePath(environment.configFile()));
final int initialReloadCount = mockReloadablePlugin.getReloadCount();
final CountDownLatch latch = new CountDownLatch(1);
- client().admin().cluster().prepareReloadSecureSettings().execute(
- new ActionListener() {
- @Override
- public void onResponse(NodesReloadSecureSettingsResponse nodesReloadResponse) {
- try {
- assertThat(nodesReloadResponse, notNullValue());
- final Map nodesMap = nodesReloadResponse.getNodesMap();
- assertThat(nodesMap.size(), equalTo(cluster().size()));
- for (final NodesReloadSecureSettingsResponse.NodeResponse nodeResponse : nodesReloadResponse.getNodes()) {
- assertThat(nodeResponse.reloadException(), notNullValue());
- assertThat(nodeResponse.reloadException(), instanceOf(IllegalStateException.class));
- assertThat(nodeResponse.reloadException().getMessage(), containsString("Keystore is missing"));
- }
- } catch (final AssertionError e) {
- reloadSettingsError.set(e);
- } finally {
- latch.countDown();
+ final SecureString emptyPassword = randomBoolean() ? new SecureString(new char[0]) : null;
+ client().admin().cluster().prepareReloadSecureSettings().setSecureStorePassword(emptyPassword)
+ .setNodesIds(Strings.EMPTY_ARRAY).execute(
+ new ActionListener() {
+ @Override
+ public void onResponse(NodesReloadSecureSettingsResponse nodesReloadResponse) {
+ try {
+ assertThat(nodesReloadResponse, notNullValue());
+ final Map nodesMap = nodesReloadResponse.getNodesMap();
+ assertThat(nodesMap.size(), equalTo(cluster().size()));
+ for (final NodesReloadSecureSettingsResponse.NodeResponse nodeResponse : nodesReloadResponse.getNodes()) {
+ assertThat(nodeResponse.reloadException(), notNullValue());
+ assertThat(nodeResponse.reloadException(), instanceOf(IllegalStateException.class));
+ assertThat(nodeResponse.reloadException().getMessage(), containsString("Keystore is missing"));
}
- }
-
- @Override
- public void onFailure(Exception e) {
- reloadSettingsError.set(new AssertionError("Nodes request failed", e));
+ } catch (final AssertionError e) {
+ reloadSettingsError.set(e);
+ } finally {
latch.countDown();
}
- });
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ reloadSettingsError.set(new AssertionError("Nodes request failed", e));
+ latch.countDown();
+ }
+ });
latch.await();
if (reloadSettingsError.get() != null) {
throw reloadSettingsError.get();
@@ -97,7 +103,7 @@ public class ReloadSecureSettingsIT extends ESIntegTestCase {
public void testInvalidKeystoreFile() throws Exception {
final PluginsService pluginsService = internalCluster().getInstance(PluginsService.class);
final MockReloadablePlugin mockReloadablePlugin = pluginsService.filterPlugins(MockReloadablePlugin.class)
- .stream().findFirst().get();
+ .stream().findFirst().get();
final Environment environment = internalCluster().getInstance(Environment.class);
final AtomicReference reloadSettingsError = new AtomicReference<>();
final int initialReloadCount = mockReloadablePlugin.getReloadCount();
@@ -109,30 +115,32 @@ public class ReloadSecureSettingsIT extends ESIntegTestCase {
Files.copy(keystore, KeyStoreWrapper.keystorePath(environment.configFile()), StandardCopyOption.REPLACE_EXISTING);
}
final CountDownLatch latch = new CountDownLatch(1);
- client().admin().cluster().prepareReloadSecureSettings().execute(
- new ActionListener() {
- @Override
- public void onResponse(NodesReloadSecureSettingsResponse nodesReloadResponse) {
- try {
- assertThat(nodesReloadResponse, notNullValue());
- final Map nodesMap = nodesReloadResponse.getNodesMap();
- assertThat(nodesMap.size(), equalTo(cluster().size()));
- for (final NodesReloadSecureSettingsResponse.NodeResponse nodeResponse : nodesReloadResponse.getNodes()) {
- assertThat(nodeResponse.reloadException(), notNullValue());
- }
- } catch (final AssertionError e) {
- reloadSettingsError.set(e);
- } finally {
- latch.countDown();
+ final SecureString emptyPassword = randomBoolean() ? new SecureString(new char[0]) : null;
+ client().admin().cluster().prepareReloadSecureSettings().setSecureStorePassword(emptyPassword)
+ .setNodesIds(Strings.EMPTY_ARRAY).execute(
+ new ActionListener() {
+ @Override
+ public void onResponse(NodesReloadSecureSettingsResponse nodesReloadResponse) {
+ try {
+ assertThat(nodesReloadResponse, notNullValue());
+ final Map nodesMap = nodesReloadResponse.getNodesMap();
+ assertThat(nodesMap.size(), equalTo(cluster().size()));
+ for (final NodesReloadSecureSettingsResponse.NodeResponse nodeResponse : nodesReloadResponse.getNodes()) {
+ assertThat(nodeResponse.reloadException(), notNullValue());
}
- }
-
- @Override
- public void onFailure(Exception e) {
- reloadSettingsError.set(new AssertionError("Nodes request failed", e));
+ } catch (final AssertionError e) {
+ reloadSettingsError.set(e);
+ } finally {
latch.countDown();
}
- });
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ reloadSettingsError.set(new AssertionError("Nodes request failed", e));
+ latch.countDown();
+ }
+ });
latch.await();
if (reloadSettingsError.get() != null) {
throw reloadSettingsError.get();
@@ -141,16 +149,142 @@ public class ReloadSecureSettingsIT extends ESIntegTestCase {
assertThat(mockReloadablePlugin.getReloadCount(), equalTo(initialReloadCount));
}
+ public void testReloadAllNodesWithPasswordWithoutTLSFails() throws Exception {
+ final PluginsService pluginsService = internalCluster().getInstance(PluginsService.class);
+ final MockReloadablePlugin mockReloadablePlugin = pluginsService.filterPlugins(MockReloadablePlugin.class)
+ .stream().findFirst().get();
+ final Environment environment = internalCluster().getInstance(Environment.class);
+ final AtomicReference reloadSettingsError = new AtomicReference<>();
+ final int initialReloadCount = mockReloadablePlugin.getReloadCount();
+ final char[] password = randomAlphaOfLength(12).toCharArray();
+ writeEmptyKeystore(environment, password);
+ final CountDownLatch latch = new CountDownLatch(1);
+ client().admin()
+ .cluster()
+ .prepareReloadSecureSettings()
+ // No filter should try to hit all nodes
+ .setNodesIds(Strings.EMPTY_ARRAY)
+ .setSecureStorePassword(new SecureString(password))
+ .execute(new ActionListener() {
+ @Override
+ public void onResponse(NodesReloadSecureSettingsResponse nodesReloadResponse) {
+ reloadSettingsError.set(new AssertionError("Nodes request succeeded when it should have failed", null));
+ latch.countDown();
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ assertThat(e, instanceOf(ElasticsearchException.class));
+ assertThat(e.getMessage(),
+ containsString("Secure settings cannot be updated cluster wide when TLS for the transport layer is not enabled"));
+ latch.countDown();
+ }
+ });
+ latch.await();
+ if (reloadSettingsError.get() != null) {
+ throw reloadSettingsError.get();
+ }
+ //no reload should be triggered
+ assertThat(mockReloadablePlugin.getReloadCount(), equalTo(initialReloadCount));
+ }
+
+ public void testReloadLocalNodeWithPasswordWithoutTLSSucceeds() throws Exception {
+ final Environment environment = internalCluster().getInstance(Environment.class);
+ final AtomicReference reloadSettingsError = new AtomicReference<>();
+ final char[] password = randomAlphaOfLength(12).toCharArray();
+ writeEmptyKeystore(environment, password);
+ final CountDownLatch latch = new CountDownLatch(1);
+ client().admin()
+ .cluster()
+ .prepareReloadSecureSettings()
+ .setNodesIds("_local")
+ .setSecureStorePassword(new SecureString(password))
+ .execute(new ActionListener() {
+ @Override
+ public void onResponse(NodesReloadSecureSettingsResponse nodesReloadResponse) {
+ try {
+ assertThat(nodesReloadResponse, notNullValue());
+ final Map nodesMap = nodesReloadResponse.getNodesMap();
+ assertThat(nodesMap.size(), equalTo(1));
+ assertThat(nodesReloadResponse.getNodes().size(), equalTo(1));
+ final NodesReloadSecureSettingsResponse.NodeResponse nodeResponse = nodesReloadResponse.getNodes().get(0);
+ assertThat(nodeResponse.reloadException(), nullValue());
+ } catch (final AssertionError e) {
+ reloadSettingsError.set(e);
+ } finally {
+ latch.countDown();
+ }
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ reloadSettingsError.set(new AssertionError("Nodes request failed", e));
+ latch.countDown();
+ }
+ });
+ latch.await();
+ if (reloadSettingsError.get() != null) {
+ throw reloadSettingsError.get();
+ }
+ }
+
+ public void testWrongKeystorePassword() throws Exception {
+ final PluginsService pluginsService = internalCluster().getInstance(PluginsService.class);
+ final MockReloadablePlugin mockReloadablePlugin = pluginsService.filterPlugins(MockReloadablePlugin.class)
+ .stream().findFirst().get();
+ final Environment environment = internalCluster().getInstance(Environment.class);
+ final AtomicReference reloadSettingsError = new AtomicReference<>();
+ final int initialReloadCount = mockReloadablePlugin.getReloadCount();
+ // "some" keystore should be present in this case
+ writeEmptyKeystore(environment, new char[0]);
+ final CountDownLatch latch = new CountDownLatch(1);
+ client().admin()
+ .cluster()
+ .prepareReloadSecureSettings()
+ .setNodesIds("_local")
+ .setSecureStorePassword(new SecureString(new char[]{'W', 'r', 'o', 'n', 'g'}))
+ .execute(new ActionListener() {
+ @Override
+ public void onResponse(NodesReloadSecureSettingsResponse nodesReloadResponse) {
+ try {
+ assertThat(nodesReloadResponse, notNullValue());
+ final Map nodesMap = nodesReloadResponse.getNodesMap();
+ assertThat(nodesMap.size(), equalTo(1));
+ for (final NodesReloadSecureSettingsResponse.NodeResponse nodeResponse : nodesReloadResponse.getNodes()) {
+ assertThat(nodeResponse.reloadException(), notNullValue());
+ assertThat(nodeResponse.reloadException(), instanceOf(SecurityException.class));
+ }
+ } catch (final AssertionError e) {
+ reloadSettingsError.set(e);
+ } finally {
+ latch.countDown();
+ }
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ reloadSettingsError.set(new AssertionError("Nodes request failed", e));
+ latch.countDown();
+ }
+ });
+ latch.await();
+ if (reloadSettingsError.get() != null) {
+ throw reloadSettingsError.get();
+ }
+ // in the wrong password case no reload should be triggered
+ assertThat(mockReloadablePlugin.getReloadCount(), equalTo(initialReloadCount));
+ }
+
public void testMisbehavingPlugin() throws Exception {
final Environment environment = internalCluster().getInstance(Environment.class);
final PluginsService pluginsService = internalCluster().getInstance(PluginsService.class);
final MockReloadablePlugin mockReloadablePlugin = pluginsService.filterPlugins(MockReloadablePlugin.class)
- .stream().findFirst().get();
+ .stream().findFirst().get();
// make plugins throw on reload
for (final String nodeName : internalCluster().getNodeNames()) {
internalCluster().getInstance(PluginsService.class, nodeName)
- .filterPlugins(MisbehavingReloadablePlugin.class)
- .stream().findFirst().get().setShouldThrow(true);
+ .filterPlugins(MisbehavingReloadablePlugin.class)
+ .stream().findFirst().get().setShouldThrow(true);
}
final AtomicReference reloadSettingsError = new AtomicReference<>();
final int initialReloadCount = mockReloadablePlugin.getReloadCount();
@@ -158,34 +292,36 @@ public class ReloadSecureSettingsIT extends ESIntegTestCase {
final SecureSettings secureSettings = writeEmptyKeystore(environment, new char[0]);
// read seed setting value from the test case (not from the node)
final String seedValue = KeyStoreWrapper.SEED_SETTING
- .get(Settings.builder().put(environment.settings()).setSecureSettings(secureSettings).build())
- .toString();
+ .get(Settings.builder().put(environment.settings()).setSecureSettings(secureSettings).build())
+ .toString();
final CountDownLatch latch = new CountDownLatch(1);
- client().admin().cluster().prepareReloadSecureSettings().execute(
- new ActionListener() {
- @Override
- public void onResponse(NodesReloadSecureSettingsResponse nodesReloadResponse) {
- try {
- assertThat(nodesReloadResponse, notNullValue());
- final Map nodesMap = nodesReloadResponse.getNodesMap();
- assertThat(nodesMap.size(), equalTo(cluster().size()));
- for (final NodesReloadSecureSettingsResponse.NodeResponse nodeResponse : nodesReloadResponse.getNodes()) {
- assertThat(nodeResponse.reloadException(), notNullValue());
- assertThat(nodeResponse.reloadException().getMessage(), containsString("If shouldThrow I throw"));
- }
- } catch (final AssertionError e) {
- reloadSettingsError.set(e);
- } finally {
- latch.countDown();
+ final SecureString emptyPassword = randomBoolean() ? new SecureString(new char[0]) : null;
+ client().admin().cluster().prepareReloadSecureSettings().setSecureStorePassword(emptyPassword)
+ .setNodesIds(Strings.EMPTY_ARRAY).execute(
+ new ActionListener() {
+ @Override
+ public void onResponse(NodesReloadSecureSettingsResponse nodesReloadResponse) {
+ try {
+ assertThat(nodesReloadResponse, notNullValue());
+ final Map nodesMap = nodesReloadResponse.getNodesMap();
+ assertThat(nodesMap.size(), equalTo(cluster().size()));
+ for (final NodesReloadSecureSettingsResponse.NodeResponse nodeResponse : nodesReloadResponse.getNodes()) {
+ assertThat(nodeResponse.reloadException(), notNullValue());
+ assertThat(nodeResponse.reloadException().getMessage(), containsString("If shouldThrow I throw"));
}
- }
-
- @Override
- public void onFailure(Exception e) {
- reloadSettingsError.set(new AssertionError("Nodes request failed", e));
+ } catch (final AssertionError e) {
+ reloadSettingsError.set(e);
+ } finally {
latch.countDown();
}
- });
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ reloadSettingsError.set(new AssertionError("Nodes request failed", e));
+ latch.countDown();
+ }
+ });
latch.await();
if (reloadSettingsError.get() != null) {
throw reloadSettingsError.get();
@@ -200,7 +336,7 @@ public class ReloadSecureSettingsIT extends ESIntegTestCase {
public void testReloadWhileKeystoreChanged() throws Exception {
final PluginsService pluginsService = internalCluster().getInstance(PluginsService.class);
final MockReloadablePlugin mockReloadablePlugin = pluginsService.filterPlugins(MockReloadablePlugin.class)
- .stream().findFirst().get();
+ .stream().findFirst().get();
final Environment environment = internalCluster().getInstance(Environment.class);
final int initialReloadCount = mockReloadablePlugin.getReloadCount();
for (int i = 0; i < randomIntBetween(4, 8); i++) {
@@ -208,8 +344,8 @@ public class ReloadSecureSettingsIT extends ESIntegTestCase {
final SecureSettings secureSettings = writeEmptyKeystore(environment, new char[0]);
// read seed setting value from the test case (not from the node)
final String seedValue = KeyStoreWrapper.SEED_SETTING
- .get(Settings.builder().put(environment.settings()).setSecureSettings(secureSettings).build())
- .toString();
+ .get(Settings.builder().put(environment.settings()).setSecureSettings(secureSettings).build())
+ .toString();
// reload call
successfulReloadCall();
assertThat(mockReloadablePlugin.getSeedValue(), equalTo(seedValue));
@@ -228,30 +364,32 @@ public class ReloadSecureSettingsIT extends ESIntegTestCase {
private void successfulReloadCall() throws InterruptedException {
final AtomicReference reloadSettingsError = new AtomicReference<>();
final CountDownLatch latch = new CountDownLatch(1);
- client().admin().cluster().prepareReloadSecureSettings().execute(
- new ActionListener() {
- @Override
- public void onResponse(NodesReloadSecureSettingsResponse nodesReloadResponse) {
- try {
- assertThat(nodesReloadResponse, notNullValue());
- final Map nodesMap = nodesReloadResponse.getNodesMap();
- assertThat(nodesMap.size(), equalTo(cluster().size()));
- for (final NodesReloadSecureSettingsResponse.NodeResponse nodeResponse : nodesReloadResponse.getNodes()) {
- assertThat(nodeResponse.reloadException(), nullValue());
- }
- } catch (final AssertionError e) {
- reloadSettingsError.set(e);
- } finally {
- latch.countDown();
+ final SecureString emptyPassword = randomBoolean() ? new SecureString(new char[0]) : null;
+ client().admin().cluster().prepareReloadSecureSettings().setSecureStorePassword(emptyPassword)
+ .setNodesIds(Strings.EMPTY_ARRAY).execute(
+ new ActionListener() {
+ @Override
+ public void onResponse(NodesReloadSecureSettingsResponse nodesReloadResponse) {
+ try {
+ assertThat(nodesReloadResponse, notNullValue());
+ final Map nodesMap = nodesReloadResponse.getNodesMap();
+ assertThat(nodesMap.size(), equalTo(cluster().size()));
+ for (final NodesReloadSecureSettingsResponse.NodeResponse nodeResponse : nodesReloadResponse.getNodes()) {
+ assertThat(nodeResponse.reloadException(), nullValue());
}
- }
-
- @Override
- public void onFailure(Exception e) {
- reloadSettingsError.set(new AssertionError("Nodes request failed", e));
+ } catch (final AssertionError e) {
+ reloadSettingsError.set(e);
+ } finally {
latch.countDown();
}
- });
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ reloadSettingsError.set(new AssertionError("Nodes request failed", e));
+ latch.countDown();
+ }
+ });
latch.await();
if (reloadSettingsError.get() != null) {
throw reloadSettingsError.get();
diff --git a/server/src/test/java/org/elasticsearch/bootstrap/BootstrapTests.java b/server/src/test/java/org/elasticsearch/bootstrap/BootstrapTests.java
index 6b336fdf2b7..df2056e8c6b 100644
--- a/server/src/test/java/org/elasticsearch/bootstrap/BootstrapTests.java
+++ b/server/src/test/java/org/elasticsearch/bootstrap/BootstrapTests.java
@@ -29,17 +29,24 @@ import org.elasticsearch.test.ESTestCase;
import org.junit.After;
import org.junit.Before;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
+import static org.hamcrest.Matchers.equalTo;
+
public class BootstrapTests extends ESTestCase {
Environment env;
List fileSystems = new ArrayList<>();
+ private static final int MAX_PASSPHRASE_LENGTH = 10;
+
@After
public void closeMockFileSystems() throws IOException {
IOUtils.close(fileSystems);
@@ -66,4 +73,43 @@ public class BootstrapTests extends ESTestCase {
assertTrue(Files.exists(configPath.resolve("elasticsearch.keystore")));
}
}
+
+ public void testReadCharsFromStdin() throws Exception {
+ assertPassphraseRead("hello", "hello");
+ assertPassphraseRead("hello\n", "hello");
+ assertPassphraseRead("hello\r\n", "hello");
+
+ assertPassphraseRead("hellohello", "hellohello");
+ assertPassphraseRead("hellohello\n", "hellohello");
+ assertPassphraseRead("hellohello\r\n", "hellohello");
+
+ assertPassphraseRead("hello\nhi\n", "hello");
+ assertPassphraseRead("hello\r\nhi\r\n", "hello");
+ }
+
+ public void testPassphraseTooLong() throws Exception {
+ byte[] source = "hellohello!\n".getBytes(StandardCharsets.UTF_8);
+ try (InputStream stream = new ByteArrayInputStream(source)) {
+ expectThrows(RuntimeException.class, "Password exceeded maximum length of 10",
+ () -> Bootstrap.readPassphrase(stream, MAX_PASSPHRASE_LENGTH));
+ }
+ }
+
+ public void testNoPassPhraseProvided() throws Exception {
+ byte[] source = "\r\n".getBytes(StandardCharsets.UTF_8);
+ try (InputStream stream = new ByteArrayInputStream(source)) {
+ expectThrows(RuntimeException.class, "Keystore passphrase required but none provided.",
+ () -> Bootstrap.readPassphrase(stream, MAX_PASSPHRASE_LENGTH));
+ }
+ }
+
+ private void assertPassphraseRead(String source, String expected) {
+ try (InputStream stream = new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8))) {
+ SecureString result = Bootstrap.readPassphrase(stream, MAX_PASSPHRASE_LENGTH);
+ assertThat(result, equalTo(expected));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
}
diff --git a/server/src/test/java/org/elasticsearch/cli/MultiCommandTests.java b/server/src/test/java/org/elasticsearch/cli/MultiCommandTests.java
index 38c0edaee80..736b19aaef0 100644
--- a/server/src/test/java/org/elasticsearch/cli/MultiCommandTests.java
+++ b/server/src/test/java/org/elasticsearch/cli/MultiCommandTests.java
@@ -29,6 +29,9 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.emptyString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
public class MultiCommandTests extends CommandTestCase {
@@ -200,4 +203,55 @@ public class MultiCommandTests extends CommandTestCase {
assertTrue("SubCommand2 was not closed when close method is invoked", subCommand2.closeCalled.get());
}
+ // Tests for multicommand error logging
+
+ static class ErrorHandlingMultiCommand extends MultiCommand {
+ ErrorHandlingMultiCommand() {
+ super("error catching", () -> {});
+ }
+
+ @Override
+ protected boolean addShutdownHook() {
+ return false;
+ }
+ }
+
+ static class ErrorThrowingSubCommand extends Command {
+ ErrorThrowingSubCommand() {
+ super("error throwing", () -> {});
+ }
+ @Override
+ protected void execute(Terminal terminal, OptionSet options) throws Exception {
+ throw new UserException(1, "Dummy error");
+ }
+
+ @Override
+ protected boolean addShutdownHook() {
+ return false;
+ }
+ }
+
+ public void testErrorDisplayedWithDefault() throws Exception {
+ MockTerminal terminal = new MockTerminal();
+ MultiCommand mc = new ErrorHandlingMultiCommand();
+ mc.subcommands.put("throw", new ErrorThrowingSubCommand());
+ mc.main(new String[]{"throw", "--silent"}, terminal);
+ assertThat(terminal.getOutput(), is(emptyString()));
+ assertThat(terminal.getErrorOutput(), equalTo("ERROR: Dummy error\n"));
+ }
+
+ public void testNullErrorMessageSuppressesErrorOutput() throws Exception {
+ MockTerminal terminal = new MockTerminal();
+ MultiCommand mc = new ErrorHandlingMultiCommand();
+ mc.subcommands.put("throw", new ErrorThrowingSubCommand() {
+ @Override
+ protected void execute(Terminal terminal, OptionSet options) throws Exception {
+ throw new UserException(1, null);
+ }
+ });
+ mc.main(new String[]{"throw", "--silent"}, terminal);
+ assertThat(terminal.getOutput(), is(emptyString()));
+ assertThat(terminal.getErrorOutput(), is(emptyString()));
+ }
+
}
diff --git a/server/src/test/java/org/elasticsearch/cli/TerminalTests.java b/server/src/test/java/org/elasticsearch/cli/TerminalTests.java
index 99bbe9d6184..85b8ec5bf26 100644
--- a/server/src/test/java/org/elasticsearch/cli/TerminalTests.java
+++ b/server/src/test/java/org/elasticsearch/cli/TerminalTests.java
@@ -21,7 +21,14 @@ package org.elasticsearch.cli;
import org.elasticsearch.test.ESTestCase;
+import java.io.BufferedReader;
+import java.io.StringReader;
+
+import static org.elasticsearch.cli.Terminal.readLineToCharArray;
+import static org.hamcrest.Matchers.equalTo;
+
public class TerminalTests extends ESTestCase {
+
public void testVerbosity() throws Exception {
MockTerminal terminal = new MockTerminal();
terminal.setVerbosity(Terminal.Verbosity.SILENT);
@@ -95,6 +102,22 @@ public class TerminalTests extends ESTestCase {
assertFalse(terminal.promptYesNo("Answer?", true));
}
+ public void testMaxSecretLength() throws Exception {
+ MockTerminal terminal = new MockTerminal();
+ String secret = "A very long secret, too long in fact for our purposes.";
+ terminal.addSecretInput(secret);
+
+ expectThrows(IllegalStateException.class, "Secret exceeded maximum length of ",
+ () -> terminal.readSecret("Secret? ", secret.length() - 1));
+ }
+
+ public void testTerminalReusesBufferedReaders() throws Exception {
+ Terminal.SystemTerminal terminal = new Terminal.SystemTerminal();
+ BufferedReader reader1 = terminal.getReader();
+ BufferedReader reader2 = terminal.getReader();
+ assertSame("System terminal should not create multiple buffered readers", reader1, reader2);
+ }
+
private void assertPrinted(MockTerminal logTerminal, Terminal.Verbosity verbosity, String text) throws Exception {
logTerminal.println(verbosity, text);
String output = logTerminal.getOutput();
@@ -121,4 +144,47 @@ public class TerminalTests extends ESTestCase {
assertTrue(output, output.isEmpty());
}
+ public void testSystemTerminalReadsSingleLines() throws Exception {
+ assertRead("\n", "");
+ assertRead("\r\n", "");
+
+ assertRead("hello\n", "hello");
+ assertRead("hello\r\n", "hello");
+
+ assertRead("hellohello\n", "hellohello");
+ assertRead("hellohello\r\n", "hellohello");
+ }
+
+ public void testSystemTerminalReadsMultipleLines() throws Exception {
+ assertReadLines("hello\nhello\n", "hello", "hello");
+ assertReadLines("hello\r\nhello\r\n", "hello", "hello");
+
+ assertReadLines("one\ntwo\n\nthree", "one", "two", "", "three");
+ assertReadLines("one\r\ntwo\r\n\r\nthree", "one", "two", "", "three");
+ }
+
+ public void testSystemTerminalLineExceedsMaxCharacters() throws Exception {
+ try (StringReader reader = new StringReader("hellohellohello!\n")) {
+ expectThrows(RuntimeException.class, "Input exceeded maximum length of 10",
+ () -> readLineToCharArray(reader, 10));
+ }
+ }
+
+ private void assertRead(String source, String expected) {
+ try (StringReader reader = new StringReader(source)) {
+ char[] result = readLineToCharArray(reader, 10);
+ assertThat(result, equalTo(expected.toCharArray()));
+ }
+ }
+
+ private void assertReadLines(String source, String... expected) {
+ try (StringReader reader = new StringReader(source)) {
+ char[] result;
+ for (String exp : expected) {
+ result = readLineToCharArray(reader, 10);
+ assertThat(result, equalTo(exp.toCharArray()));
+ }
+ }
+ }
+
}
diff --git a/server/src/test/java/org/elasticsearch/common/settings/AddFileKeyStoreCommandTests.java b/server/src/test/java/org/elasticsearch/common/settings/AddFileKeyStoreCommandTests.java
index 6cfa2c1fdf2..cd64fdc08d3 100644
--- a/server/src/test/java/org/elasticsearch/common/settings/AddFileKeyStoreCommandTests.java
+++ b/server/src/test/java/org/elasticsearch/common/settings/AddFileKeyStoreCommandTests.java
@@ -53,110 +53,156 @@ public class AddFileKeyStoreCommandTests extends KeyStoreCommandTestCase {
return file;
}
- private void addFile(KeyStoreWrapper keystore, String setting, Path file) throws Exception {
+ private void addFile(KeyStoreWrapper keystore, String setting, Path file, String password) throws Exception {
keystore.setFile(setting, Files.readAllBytes(file));
- keystore.save(env.configFile(), new char[0]);
+ keystore.save(env.configFile(), password.toCharArray());
}
- public void testMissingPromptCreate() throws Exception {
+ public void testMissingCreateWithEmptyPasswordWhenPrompted() throws Exception {
+ String password = "";
Path file1 = createRandomFile();
terminal.addTextInput("y");
execute("foo", file1.toString());
- assertSecureFile("foo", file1);
+ assertSecureFile("foo", file1, password);
}
- public void testMissingForceCreate() throws Exception {
+ public void testMissingCreateWithEmptyPasswordWithoutPromptIfForced() throws Exception {
+ String password = "";
Path file1 = createRandomFile();
- terminal.addSecretInput("bar");
execute("-f", "foo", file1.toString());
- assertSecureFile("foo", file1);
+ assertSecureFile("foo", file1, password);
}
public void testMissingNoCreate() throws Exception {
+ terminal.addSecretInput(randomFrom("", "keystorepassword"));
terminal.addTextInput("n"); // explicit no
execute("foo");
assertNull(KeyStoreWrapper.load(env.configFile()));
}
public void testOverwritePromptDefault() throws Exception {
+ String password = "keystorepassword";
Path file = createRandomFile();
- KeyStoreWrapper keystore = createKeystore("");
- addFile(keystore, "foo", file);
+ KeyStoreWrapper keystore = createKeystore(password);
+ addFile(keystore, "foo", file, password);
+ terminal.addSecretInput(password);
+ terminal.addSecretInput(password);
terminal.addTextInput("");
execute("foo", "path/dne");
- assertSecureFile("foo", file);
+ assertSecureFile("foo", file, password);
}
public void testOverwritePromptExplicitNo() throws Exception {
+ String password = "keystorepassword";
Path file = createRandomFile();
- KeyStoreWrapper keystore = createKeystore("");
- addFile(keystore, "foo", file);
+ KeyStoreWrapper keystore = createKeystore(password);
+ addFile(keystore, "foo", file, password);
+ terminal.addSecretInput(password);
terminal.addTextInput("n"); // explicit no
execute("foo", "path/dne");
- assertSecureFile("foo", file);
+ assertSecureFile("foo", file, password);
}
public void testOverwritePromptExplicitYes() throws Exception {
+ String password = "keystorepassword";
Path file1 = createRandomFile();
- KeyStoreWrapper keystore = createKeystore("");
- addFile(keystore, "foo", file1);
+ KeyStoreWrapper keystore = createKeystore(password);
+ addFile(keystore, "foo", file1, password);
+ terminal.addSecretInput(password);
+ terminal.addSecretInput(password);
terminal.addTextInput("y");
Path file2 = createRandomFile();
execute("foo", file2.toString());
- assertSecureFile("foo", file2);
+ assertSecureFile("foo", file2, password);
}
public void testOverwriteForceShort() throws Exception {
+ String password = "keystorepassword";
Path file1 = createRandomFile();
- KeyStoreWrapper keystore = createKeystore("");
- addFile(keystore, "foo", file1);
+ KeyStoreWrapper keystore = createKeystore(password);
+ addFile(keystore, "foo", file1, password);
Path file2 = createRandomFile();
+ terminal.addSecretInput(password);
+ terminal.addSecretInput(password);
execute("-f", "foo", file2.toString());
- assertSecureFile("foo", file2);
+ assertSecureFile("foo", file2, password);
}
public void testOverwriteForceLong() throws Exception {
+ String password = "keystorepassword";
Path file1 = createRandomFile();
- KeyStoreWrapper keystore = createKeystore("");
- addFile(keystore, "foo", file1);
+ KeyStoreWrapper keystore = createKeystore(password);
+ addFile(keystore, "foo", file1, password);
Path file2 = createRandomFile();
+ terminal.addSecretInput(password);
execute("--force", "foo", file2.toString());
- assertSecureFile("foo", file2);
+ assertSecureFile("foo", file2, password);
}
- public void testForceNonExistent() throws Exception {
- createKeystore("");
+ public void testForceDoesNotAlreadyExist() throws Exception {
+ String password = "keystorepassword";
+ createKeystore(password);
Path file = createRandomFile();
+ terminal.addSecretInput(password);
execute("--force", "foo", file.toString());
- assertSecureFile("foo", file);
+ assertSecureFile("foo", file, password);
}
public void testMissingSettingName() throws Exception {
- createKeystore("");
+ String password = "keystorepassword";
+ createKeystore(password);
+ terminal.addSecretInput(password);
UserException e = expectThrows(UserException.class, this::execute);
assertEquals(ExitCodes.USAGE, e.exitCode);
assertThat(e.getMessage(), containsString("Missing setting name"));
}
public void testMissingFileName() throws Exception {
- createKeystore("");
+ String password = "keystorepassword";
+ createKeystore(password);
+ terminal.addSecretInput(password);
UserException e = expectThrows(UserException.class, () -> execute("foo"));
assertEquals(ExitCodes.USAGE, e.exitCode);
assertThat(e.getMessage(), containsString("Missing file name"));
}
public void testFileDNE() throws Exception {
- createKeystore("");
+ String password = "keystorepassword";
+ createKeystore(password);
+ terminal.addSecretInput(password);
UserException e = expectThrows(UserException.class, () -> execute("foo", "path/dne"));
assertEquals(ExitCodes.IO_ERROR, e.exitCode);
assertThat(e.getMessage(), containsString("File [path/dne] does not exist"));
}
public void testExtraArguments() throws Exception {
- createKeystore("");
+ String password = "keystorepassword";
+ createKeystore(password);
Path file = createRandomFile();
+ terminal.addSecretInput(password);
UserException e = expectThrows(UserException.class, () -> execute("foo", file.toString(), "bar"));
assertEquals(e.getMessage(), ExitCodes.USAGE, e.exitCode);
assertThat(e.getMessage(), containsString("Unrecognized extra arguments [bar]"));
}
+
+ public void testIncorrectPassword() throws Exception {
+ String password = "keystorepassword";
+ createKeystore(password);
+ Path file = createRandomFile();
+ terminal.addSecretInput("thewrongkeystorepassword");
+ UserException e = expectThrows(UserException.class, () -> execute("foo", file.toString()));
+ assertEquals(e.getMessage(), ExitCodes.DATA_ERROR, e.exitCode);
+ assertThat(e.getMessage(), containsString("Provided keystore password was incorrect"));
+ }
+
+ public void testAddToUnprotectedKeystore() throws Exception {
+ String password = "";
+ Path file = createRandomFile();
+ KeyStoreWrapper keystore = createKeystore(password);
+ addFile(keystore, "foo", file, password);
+ terminal.addTextInput("");
+ // will not be prompted for a password
+ execute("foo", "path/dne");
+ assertSecureFile("foo", file, password);
+ }
}
diff --git a/server/src/test/java/org/elasticsearch/common/settings/AddStringKeyStoreCommandTests.java b/server/src/test/java/org/elasticsearch/common/settings/AddStringKeyStoreCommandTests.java
index b5e6a31e148..274dfb39eff 100644
--- a/server/src/test/java/org/elasticsearch/common/settings/AddStringKeyStoreCommandTests.java
+++ b/server/src/test/java/org/elasticsearch/common/settings/AddStringKeyStoreCommandTests.java
@@ -43,6 +43,7 @@ public class AddStringKeyStoreCommandTests extends KeyStoreCommandTestCase {
protected Environment createEnv(Map settings) throws UserException {
return env;
}
+
@Override
InputStream getStdin() {
return input;
@@ -50,17 +51,27 @@ public class AddStringKeyStoreCommandTests extends KeyStoreCommandTestCase {
};
}
- public void testMissingPromptCreate() throws Exception {
+ public void testInvalidPassphrease() throws Exception {
+ String password = "keystorepassword";
+ createKeystore(password, "foo", "bar");
+ terminal.addSecretInput("thewrongpassword");
+ UserException e = expectThrows(UserException.class, () -> execute("foo2"));
+ assertEquals(e.getMessage(), ExitCodes.DATA_ERROR, e.exitCode);
+ assertThat(e.getMessage(), containsString("Provided keystore password was incorrect"));
+
+ }
+
+ public void testMissingPromptCreateWithoutPasswordWhenPrompted() throws Exception {
terminal.addTextInput("y");
terminal.addSecretInput("bar");
execute("foo");
- assertSecureString("foo", "bar");
+ assertSecureString("foo", "bar", "");
}
- public void testMissingForceCreate() throws Exception {
+ public void testMissingPromptCreateWithoutPasswordWithoutPromptIfForced() throws Exception {
terminal.addSecretInput("bar");
execute("-f", "foo");
- assertSecureString("foo", "bar");
+ assertSecureString("foo", "bar", "");
}
public void testMissingNoCreate() throws Exception {
@@ -70,92 +81,118 @@ public class AddStringKeyStoreCommandTests extends KeyStoreCommandTestCase {
}
public void testOverwritePromptDefault() throws Exception {
- createKeystore("", "foo", "bar");
+ String password = "keystorepassword";
+ createKeystore(password, "foo", "bar");
+ terminal.addSecretInput(password);
terminal.addTextInput("");
execute("foo");
- assertSecureString("foo", "bar");
+ assertSecureString("foo", "bar", password);
}
public void testOverwritePromptExplicitNo() throws Exception {
- createKeystore("", "foo", "bar");
+ String password = "keystorepassword";
+ createKeystore(password, "foo", "bar");
+ terminal.addSecretInput(password);
terminal.addTextInput("n"); // explicit no
execute("foo");
- assertSecureString("foo", "bar");
+ assertSecureString("foo", "bar", password);
}
public void testOverwritePromptExplicitYes() throws Exception {
- createKeystore("", "foo", "bar");
+ String password = "keystorepassword";
+ createKeystore(password, "foo", "bar");
terminal.addTextInput("y");
+ terminal.addSecretInput(password);
terminal.addSecretInput("newvalue");
execute("foo");
- assertSecureString("foo", "newvalue");
+ assertSecureString("foo", "newvalue", password);
}
public void testOverwriteForceShort() throws Exception {
- createKeystore("", "foo", "bar");
+ String password = "keystorepassword";
+ createKeystore(password, "foo", "bar");
+ terminal.addSecretInput(password);
terminal.addSecretInput("newvalue");
execute("-f", "foo"); // force
- assertSecureString("foo", "newvalue");
+ assertSecureString("foo", "newvalue", password);
}
public void testOverwriteForceLong() throws Exception {
- createKeystore("", "foo", "bar");
+ String password = "keystorepassword";
+ createKeystore(password, "foo", "bar");
+ terminal.addSecretInput(password);
terminal.addSecretInput("and yet another secret value");
execute("--force", "foo"); // force
- assertSecureString("foo", "and yet another secret value");
+ assertSecureString("foo", "and yet another secret value", password);
}
public void testForceNonExistent() throws Exception {
- createKeystore("");
+ String password = "keystorepassword";
+ createKeystore(password);
+ terminal.addSecretInput(password);
terminal.addSecretInput("value");
execute("--force", "foo"); // force
- assertSecureString("foo", "value");
+ assertSecureString("foo", "value", password);
}
public void testPromptForValue() throws Exception {
- KeyStoreWrapper.create().save(env.configFile(), new char[0]);
+ String password = "keystorepassword";
+ KeyStoreWrapper.create().save(env.configFile(), password.toCharArray());
+ terminal.addSecretInput(password);
terminal.addSecretInput("secret value");
execute("foo");
- assertSecureString("foo", "secret value");
+ assertSecureString("foo", "secret value", password);
}
public void testStdinShort() throws Exception {
- KeyStoreWrapper.create().save(env.configFile(), new char[0]);
+ String password = "keystorepassword";
+ KeyStoreWrapper.create().save(env.configFile(), password.toCharArray());
+ terminal.addSecretInput(password);
setInput("secret value 1");
execute("-x", "foo");
- assertSecureString("foo", "secret value 1");
+ assertSecureString("foo", "secret value 1", password);
}
public void testStdinLong() throws Exception {
- KeyStoreWrapper.create().save(env.configFile(), new char[0]);
+ String password = "keystorepassword";
+ KeyStoreWrapper.create().save(env.configFile(), password.toCharArray());
+ terminal.addSecretInput(password);
setInput("secret value 2");
execute("--stdin", "foo");
- assertSecureString("foo", "secret value 2");
+ assertSecureString("foo", "secret value 2", password);
}
public void testStdinNoInput() throws Exception {
- KeyStoreWrapper.create().save(env.configFile(), new char[0]);
+ String password = "keystorepassword";
+ KeyStoreWrapper.create().save(env.configFile(), password.toCharArray());
+ terminal.addSecretInput(password);
setInput("");
execute("-x", "foo");
- assertSecureString("foo", "");
+ assertSecureString("foo", "", password);
}
public void testStdinInputWithLineBreaks() throws Exception {
- KeyStoreWrapper.create().save(env.configFile(), new char[0]);
+ String password = "keystorepassword";
+ KeyStoreWrapper.create().save(env.configFile(), password.toCharArray());
+ terminal.addSecretInput(password);
setInput("Typedthisandhitenter\n");
execute("-x", "foo");
- assertSecureString("foo", "Typedthisandhitenter");
+ assertSecureString("foo", "Typedthisandhitenter", password);
}
public void testStdinInputWithCarriageReturn() throws Exception {
- KeyStoreWrapper.create().save(env.configFile(), new char[0]);
+ String password = "keystorepassword";
+ KeyStoreWrapper.create().save(env.configFile(), password.toCharArray());
+ terminal.addSecretInput(password);
setInput("Typedthisandhitenter\r");
execute("-x", "foo");
- assertSecureString("foo", "Typedthisandhitenter");
+ assertSecureString("foo", "Typedthisandhitenter", password);
}
public void testAddUtf8String() throws Exception {
- KeyStoreWrapper.create().save(env.configFile(), new char[0]);
+ String password = "keystorepassword";
+ KeyStoreWrapper.create().save(env.configFile(), password.toCharArray());
+ terminal.addSecretInput(password);
final int stringSize = randomIntBetween(8, 16);
try (CharArrayWriter secretChars = new CharArrayWriter(stringSize)) {
for (int i = 0; i < stringSize; i++) {
@@ -163,12 +200,15 @@ public class AddStringKeyStoreCommandTests extends KeyStoreCommandTestCase {
}
setInput(secretChars.toString());
execute("-x", "foo");
- assertSecureString("foo", secretChars.toString());
+ assertSecureString("foo", secretChars.toString(), password);
}
}
public void testMissingSettingName() throws Exception {
- createKeystore("");
+ String password = "keystorepassword";
+ createKeystore(password);
+ terminal.addSecretInput(password);
+ terminal.addSecretInput(password);
terminal.addTextInput("");
UserException e = expectThrows(UserException.class, this::execute);
assertEquals(ExitCodes.USAGE, e.exitCode);
@@ -180,10 +220,19 @@ public class AddStringKeyStoreCommandTests extends KeyStoreCommandTestCase {
terminal.addSecretInput("value");
final String key = randomAlphaOfLength(4) + '@' + randomAlphaOfLength(4);
final UserException e = expectThrows(UserException.class, () -> execute(key));
- final String exceptionString= "Setting name [" + key + "] does not match the allowed setting name pattern [[A-Za-z0-9_\\-.]+]";
+ final String exceptionString = "Setting name [" + key + "] does not match the allowed setting name pattern [[A-Za-z0-9_\\-.]+]";
assertThat(
- e,
- hasToString(containsString(exceptionString)));
+ e,
+ hasToString(containsString(exceptionString)));
+ }
+
+ public void testAddToUnprotectedKeystore() throws Exception {
+ String password = "";
+ createKeystore(password, "foo", "bar");
+ terminal.addTextInput("");
+ // will not be prompted for a password
+ execute("foo");
+ assertSecureString("foo", "bar", password);
}
void setInput(String inputStr) {
diff --git a/server/src/test/java/org/elasticsearch/common/settings/ChangeKeyStorePasswordCommandTests.java b/server/src/test/java/org/elasticsearch/common/settings/ChangeKeyStorePasswordCommandTests.java
new file mode 100644
index 00000000000..ca0b5fa3633
--- /dev/null
+++ b/server/src/test/java/org/elasticsearch/common/settings/ChangeKeyStorePasswordCommandTests.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.common.settings;
+
+import org.elasticsearch.cli.Command;
+import org.elasticsearch.cli.ExitCodes;
+import org.elasticsearch.cli.UserException;
+import org.elasticsearch.env.Environment;
+
+import java.util.Map;
+
+import static org.hamcrest.Matchers.containsString;
+
+public class ChangeKeyStorePasswordCommandTests extends KeyStoreCommandTestCase {
+ @Override
+ protected Command newCommand() {
+ return new ChangeKeyStorePasswordCommand() {
+ @Override
+ protected Environment createEnv(Map settings) throws UserException {
+ return env;
+ }
+ };
+ }
+
+ public void testSetKeyStorePassword() throws Exception {
+ createKeystore("");
+ loadKeystore("");
+ terminal.addSecretInput("thepassword");
+ terminal.addSecretInput("thepassword");
+ // Prompted twice for the new password, since we didn't have an existing password
+ execute();
+ loadKeystore("thepassword");
+ }
+
+ public void testChangeKeyStorePassword() throws Exception {
+ createKeystore("theoldpassword");
+ loadKeystore("theoldpassword");
+ terminal.addSecretInput("theoldpassword");
+ terminal.addSecretInput("thepassword");
+ terminal.addSecretInput("thepassword");
+ // Prompted thrice: Once for the existing and twice for the new password
+ execute();
+ loadKeystore("thepassword");
+ }
+
+ public void testChangeKeyStorePasswordToEmpty() throws Exception {
+ createKeystore("theoldpassword");
+ loadKeystore("theoldpassword");
+ terminal.addSecretInput("theoldpassword");
+ terminal.addSecretInput("");
+ terminal.addSecretInput("");
+ // Prompted thrice: Once for the existing and twice for the new password
+ execute();
+ loadKeystore("");
+ }
+
+ public void testChangeKeyStorePasswordWrongVerification() throws Exception {
+ createKeystore("theoldpassword");
+ loadKeystore("theoldpassword");
+ terminal.addSecretInput("theoldpassword");
+ terminal.addSecretInput("thepassword");
+ terminal.addSecretInput("themisspelledpassword");
+ // Prompted thrice: Once for the existing and twice for the new password
+ UserException e = expectThrows(UserException.class, this::execute);
+ assertEquals(e.getMessage(), ExitCodes.DATA_ERROR, e.exitCode);
+ assertThat(e.getMessage(), containsString("Passwords are not equal, exiting"));
+ }
+
+ public void testChangeKeyStorePasswordWrongExistingPassword() throws Exception {
+ createKeystore("theoldpassword");
+ loadKeystore("theoldpassword");
+ terminal.addSecretInput("theoldmisspelledpassword");
+ // We'll only be prompted once (for the old password)
+ UserException e = expectThrows(UserException.class, this::execute);
+ assertEquals(e.getMessage(), ExitCodes.DATA_ERROR, e.exitCode);
+ assertThat(e.getMessage(), containsString("Provided keystore password was incorrect"));
+ }
+}
diff --git a/server/src/test/java/org/elasticsearch/common/settings/CreateKeyStoreCommandTests.java b/server/src/test/java/org/elasticsearch/common/settings/CreateKeyStoreCommandTests.java
index aefedf86e77..4fd21a9b61a 100644
--- a/server/src/test/java/org/elasticsearch/common/settings/CreateKeyStoreCommandTests.java
+++ b/server/src/test/java/org/elasticsearch/common/settings/CreateKeyStoreCommandTests.java
@@ -25,9 +25,12 @@ import java.nio.file.Path;
import java.util.Map;
import org.elasticsearch.cli.Command;
+import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.env.Environment;
+import static org.hamcrest.Matchers.containsString;
+
public class CreateKeyStoreCommandTests extends KeyStoreCommandTestCase {
@Override
@@ -40,13 +43,34 @@ public class CreateKeyStoreCommandTests extends KeyStoreCommandTestCase {
};
}
+ public void testNotMatchingPasswords() throws Exception {
+ String password = randomFrom("", "keystorepassword");
+ terminal.addSecretInput(password);
+ terminal.addSecretInput("notthekeystorepasswordyouarelookingfor");
+ UserException e = expectThrows(UserException.class, () -> execute(randomFrom("-p", "--password")));
+ assertEquals(e.getMessage(), ExitCodes.DATA_ERROR, e.exitCode);
+ assertThat(e.getMessage(), containsString("Passwords are not equal, exiting"));
+ }
+
+ public void testDefaultNotPromptForPassword() throws Exception {
+ execute();
+ Path configDir = env.configFile();
+ assertNotNull(KeyStoreWrapper.load(configDir));
+ }
+
public void testPosix() throws Exception {
+ String password = randomFrom("", "keystorepassword");
+ terminal.addSecretInput(password);
+ terminal.addSecretInput(password);
execute();
Path configDir = env.configFile();
assertNotNull(KeyStoreWrapper.load(configDir));
}
public void testNotPosix() throws Exception {
+ String password = randomFrom("", "keystorepassword");
+ terminal.addSecretInput(password);
+ terminal.addSecretInput(password);
env = setupEnv(false, fileSystems);
execute();
Path configDir = env.configFile();
@@ -54,6 +78,7 @@ public class CreateKeyStoreCommandTests extends KeyStoreCommandTestCase {
}
public void testOverwrite() throws Exception {
+ String password = randomFrom("", "keystorepassword");
Path keystoreFile = KeyStoreWrapper.keystorePath(env.configFile());
byte[] content = "not a keystore".getBytes(StandardCharsets.UTF_8);
Files.write(keystoreFile, content);
@@ -67,6 +92,8 @@ public class CreateKeyStoreCommandTests extends KeyStoreCommandTestCase {
assertArrayEquals(content, Files.readAllBytes(keystoreFile));
terminal.addTextInput("y");
+ terminal.addSecretInput(password);
+ terminal.addSecretInput(password);
execute();
assertNotNull(KeyStoreWrapper.load(env.configFile()));
}
diff --git a/server/src/test/java/org/elasticsearch/common/settings/HasPasswordKeyStoreCommandTests.java b/server/src/test/java/org/elasticsearch/common/settings/HasPasswordKeyStoreCommandTests.java
new file mode 100644
index 00000000000..93e6b0cae20
--- /dev/null
+++ b/server/src/test/java/org/elasticsearch/common/settings/HasPasswordKeyStoreCommandTests.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.common.settings;
+
+import org.elasticsearch.cli.Command;
+import org.elasticsearch.cli.UserException;
+import org.elasticsearch.env.Environment;
+
+import java.util.Map;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.emptyString;
+import static org.hamcrest.Matchers.nullValue;
+
+public class HasPasswordKeyStoreCommandTests extends KeyStoreCommandTestCase {
+ @Override
+ protected Command newCommand() {
+ return new HasPasswordKeyStoreCommand() {
+ @Override
+ protected Environment createEnv(Map settings) throws UserException {
+ return env;
+ }
+ };
+ }
+
+ public void testFailsWithNoKeystore() throws Exception {
+ UserException e = expectThrows(UserException.class, this::execute);
+ assertEquals("Unexpected exit code", HasPasswordKeyStoreCommand.NO_PASSWORD_EXIT_CODE, e.exitCode);
+ assertThat("Exception should have null message", e.getMessage(), is(nullValue()));
+ }
+
+ public void testFailsWhenKeystoreLacksPassword() throws Exception {
+ createKeystore("");
+ UserException e = expectThrows(UserException.class, this::execute);
+ assertEquals("Unexpected exit code", HasPasswordKeyStoreCommand.NO_PASSWORD_EXIT_CODE, e.exitCode);
+ assertThat("Exception should have null message", e.getMessage(), is(nullValue()));
+ }
+
+ public void testSucceedsWhenKeystoreHasPassword() throws Exception {
+ createKeystore("password");
+ String output = execute();
+ assertThat(output, containsString("Keystore is password-protected"));
+ }
+
+ public void testSilentSucceedsWhenKeystoreHasPassword() throws Exception {
+ createKeystore("password");
+ String output = execute("--silent");
+ assertThat(output, is(emptyString()));
+ }
+}
diff --git a/server/src/test/java/org/elasticsearch/common/settings/KeyStoreCommandTestCase.java b/server/src/test/java/org/elasticsearch/common/settings/KeyStoreCommandTestCase.java
index 7f8c71889e0..1e5527a1e24 100644
--- a/server/src/test/java/org/elasticsearch/common/settings/KeyStoreCommandTestCase.java
+++ b/server/src/test/java/org/elasticsearch/common/settings/KeyStoreCommandTestCase.java
@@ -89,16 +89,16 @@ public abstract class KeyStoreCommandTestCase extends CommandTestCase {
return keystore;
}
- void assertSecureString(String setting, String value) throws Exception {
- assertSecureString(loadKeystore(""), setting, value);
+ void assertSecureString(String setting, String value, String password) throws Exception {
+ assertSecureString(loadKeystore(password), setting, value);
}
void assertSecureString(KeyStoreWrapper keystore, String setting, String value) throws Exception {
assertEquals(value, keystore.getString(setting).toString());
}
- void assertSecureFile(String setting, Path file) throws Exception {
- assertSecureFile(loadKeystore(""), setting, file);
+ void assertSecureFile(String setting, Path file, String password) throws Exception {
+ assertSecureFile(loadKeystore(password), setting, file);
}
void assertSecureFile(KeyStoreWrapper keystore, String setting, Path file) throws Exception {
diff --git a/server/src/test/java/org/elasticsearch/common/settings/KeyStoreWrapperTests.java b/server/src/test/java/org/elasticsearch/common/settings/KeyStoreWrapperTests.java
index 5a1e3790a09..aa4dc566e69 100644
--- a/server/src/test/java/org/elasticsearch/common/settings/KeyStoreWrapperTests.java
+++ b/server/src/test/java/org/elasticsearch/common/settings/KeyStoreWrapperTests.java
@@ -84,7 +84,7 @@ public class KeyStoreWrapperTests extends ESTestCase {
KeyStoreWrapper keystore = KeyStoreWrapper.create();
byte[] bytes = new byte[256];
for (int i = 0; i < 256; ++i) {
- bytes[i] = (byte)i;
+ bytes[i] = (byte) i;
}
keystore.setFile("foo", bytes);
keystore.save(env.configFile(), new char[0]);
@@ -113,7 +113,7 @@ public class KeyStoreWrapperTests extends ESTestCase {
final KeyStoreWrapper loadedkeystore = KeyStoreWrapper.load(env.configFile());
final SecurityException exception = expectThrows(SecurityException.class,
() -> loadedkeystore.decrypt(new char[]{'i', 'n', 'v', 'a', 'l', 'i', 'd'}));
- assertThat(exception.getMessage(), containsString("Keystore has been corrupted or tampered with"));
+ assertThat(exception.getMessage(), containsString("Provided keystore password was incorrect"));
}
public void testCannotReadStringFromClosedKeystore() throws Exception {
@@ -388,7 +388,7 @@ public class KeyStoreWrapperTests extends ESTestCase {
byte[] base64Bytes = Base64.getEncoder().encode(fileBytes);
char[] chars = new char[base64Bytes.length];
for (int i = 0; i < chars.length; ++i) {
- chars[i] = (char)base64Bytes[i]; // PBE only stores the lower 8 bits, so this narrowing is ok
+ chars[i] = (char) base64Bytes[i]; // PBE only stores the lower 8 bits, so this narrowing is ok
}
secretKey = secretFactory.generateSecret(new PBEKeySpec(chars));
keystore.setEntry("file_setting", new KeyStore.SecretKeyEntry(secretKey), protectionParameter);
diff --git a/server/src/test/java/org/elasticsearch/common/settings/ListKeyStoreCommandTests.java b/server/src/test/java/org/elasticsearch/common/settings/ListKeyStoreCommandTests.java
index 27c30d3aa8f..f79fd751465 100644
--- a/server/src/test/java/org/elasticsearch/common/settings/ListKeyStoreCommandTests.java
+++ b/server/src/test/java/org/elasticsearch/common/settings/ListKeyStoreCommandTests.java
@@ -47,20 +47,42 @@ public class ListKeyStoreCommandTests extends KeyStoreCommandTestCase {
}
public void testEmpty() throws Exception {
- createKeystore("");
+ String password = randomFrom("", "keystorepassword");
+ createKeystore(password);
+ terminal.addSecretInput(password);
execute();
assertEquals("keystore.seed\n", terminal.getOutput());
}
public void testOne() throws Exception {
- createKeystore("", "foo", "bar");
+ String password = randomFrom("", "keystorepassword");
+ createKeystore(password, "foo", "bar");
+ terminal.addSecretInput(password);
execute();
assertEquals("foo\nkeystore.seed\n", terminal.getOutput());
}
public void testMultiple() throws Exception {
- createKeystore("", "foo", "1", "baz", "2", "bar", "3");
+ String password = randomFrom("", "keystorepassword");
+ createKeystore(password, "foo", "1", "baz", "2", "bar", "3");
+ terminal.addSecretInput(password);
execute();
assertEquals("bar\nbaz\nfoo\nkeystore.seed\n", terminal.getOutput()); // sorted
}
+
+ public void testListWithIncorrectPassword() throws Exception {
+ String password = "keystorepassword";
+ createKeystore(password, "foo", "bar");
+ terminal.addSecretInput("thewrongkeystorepassword");
+ UserException e = expectThrows(UserException.class, this::execute);
+ assertEquals(e.getMessage(), ExitCodes.DATA_ERROR, e.exitCode);
+ assertThat(e.getMessage(), containsString("Provided keystore password was incorrect"));
+ }
+
+ public void testListWithUnprotectedKeystore() throws Exception {
+ createKeystore("", "foo", "bar");
+ execute();
+ // Not prompted for a password
+ assertEquals("foo\nkeystore.seed\n", terminal.getOutput());
+ }
}
diff --git a/server/src/test/java/org/elasticsearch/common/settings/RemoveSettingKeyStoreCommandTests.java b/server/src/test/java/org/elasticsearch/common/settings/RemoveSettingKeyStoreCommandTests.java
index 2259dee31a8..b4cc08c8465 100644
--- a/server/src/test/java/org/elasticsearch/common/settings/RemoveSettingKeyStoreCommandTests.java
+++ b/server/src/test/java/org/elasticsearch/common/settings/RemoveSettingKeyStoreCommandTests.java
@@ -41,39 +41,66 @@ public class RemoveSettingKeyStoreCommandTests extends KeyStoreCommandTestCase {
};
}
- public void testMissing() throws Exception {
+ public void testMissing() {
+ String password = "keystorepassword";
+ terminal.addSecretInput(password);
UserException e = expectThrows(UserException.class, () -> execute("foo"));
assertEquals(ExitCodes.DATA_ERROR, e.exitCode);
assertThat(e.getMessage(), containsString("keystore not found"));
}
public void testNoSettings() throws Exception {
- createKeystore("");
+ String password = "keystorepassword";
+ createKeystore(password);
+ terminal.addSecretInput(password);
UserException e = expectThrows(UserException.class, this::execute);
assertEquals(ExitCodes.USAGE, e.exitCode);
assertThat(e.getMessage(), containsString("Must supply at least one setting"));
}
public void testNonExistentSetting() throws Exception {
- createKeystore("");
+ String password = "keystorepassword";
+ createKeystore(password);
+ terminal.addSecretInput(password);
UserException e = expectThrows(UserException.class, () -> execute("foo"));
assertEquals(ExitCodes.CONFIG, e.exitCode);
assertThat(e.getMessage(), containsString("[foo] does not exist"));
}
public void testOne() throws Exception {
- createKeystore("", "foo", "bar");
+ String password = "keystorepassword";
+ createKeystore(password, "foo", "bar");
+ terminal.addSecretInput(password);
execute("foo");
- assertFalse(loadKeystore("").getSettingNames().contains("foo"));
+ assertFalse(loadKeystore(password).getSettingNames().contains("foo"));
}
public void testMany() throws Exception {
- createKeystore("", "foo", "1", "bar", "2", "baz", "3");
+ String password = "keystorepassword";
+ createKeystore(password, "foo", "1", "bar", "2", "baz", "3");
+ terminal.addSecretInput(password);
execute("foo", "baz");
- Set settings = loadKeystore("").getSettingNames();
+ Set settings = loadKeystore(password).getSettingNames();
assertFalse(settings.contains("foo"));
assertFalse(settings.contains("baz"));
assertTrue(settings.contains("bar"));
assertEquals(2, settings.size()); // account for keystore.seed too
}
+
+ public void testRemoveWithIncorrectPassword() throws Exception {
+ String password = "keystorepassword";
+ createKeystore(password, "foo", "bar");
+ terminal.addSecretInput("thewrongpassword");
+ UserException e = expectThrows(UserException.class, () -> execute("foo"));
+ assertEquals(e.getMessage(), ExitCodes.DATA_ERROR, e.exitCode);
+ assertThat(e.getMessage(), containsString("Provided keystore password was incorrect"));
+ }
+
+ public void testRemoveFromUnprotectedKeystore() throws Exception {
+ String password = "";
+ createKeystore(password, "foo", "bar");
+ // will not be prompted for a password
+ execute("foo");
+ assertFalse(loadKeystore(password).getSettingNames().contains("foo"));
+ }
}
diff --git a/server/src/test/java/org/elasticsearch/common/settings/UpgradeKeyStoreCommandTests.java b/server/src/test/java/org/elasticsearch/common/settings/UpgradeKeyStoreCommandTests.java
index ec9a1432539..075aeaae5a0 100644
--- a/server/src/test/java/org/elasticsearch/common/settings/UpgradeKeyStoreCommandTests.java
+++ b/server/src/test/java/org/elasticsearch/common/settings/UpgradeKeyStoreCommandTests.java
@@ -73,7 +73,7 @@ public class UpgradeKeyStoreCommandTests extends KeyStoreCommandTestCase {
public void testKeystoreDoesNotExist() {
final UserException e = expectThrows(UserException.class, this::execute);
- assertThat(e, hasToString(containsString("keystore does not exist at [" + KeyStoreWrapper.keystorePath(env.configFile()) + "]")));
+ assertThat(e, hasToString(containsString("keystore not found at [" + KeyStoreWrapper.keystorePath(env.configFile()) + "]")));
}
}
diff --git a/server/src/test/java/org/elasticsearch/rest/action/admin/cluster/RestReloadSecureSettingsActionTests.java b/server/src/test/java/org/elasticsearch/rest/action/admin/cluster/RestReloadSecureSettingsActionTests.java
new file mode 100644
index 00000000000..7dfd294e8ae
--- /dev/null
+++ b/server/src/test/java/org/elasticsearch/rest/action/admin/cluster/RestReloadSecureSettingsActionTests.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.rest.action.admin.cluster;
+
+import org.elasticsearch.action.admin.cluster.node.reload.NodesReloadSecureSettingsRequest;
+import org.elasticsearch.common.xcontent.DeprecationHandler;
+import org.elasticsearch.common.xcontent.NamedXContentRegistry;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.test.ESTestCase;
+
+import static org.hamcrest.Matchers.nullValue;
+
+public class RestReloadSecureSettingsActionTests extends ESTestCase {
+
+ public void testParserWithPassword() throws Exception {
+ final String request = "{" +
+ "\"secure_settings_password\": \"secure_settings_password_string\"" +
+ "}";
+ try (XContentParser parser = XContentType.JSON.xContent()
+ .createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, request)) {
+ NodesReloadSecureSettingsRequest reloadSecureSettingsRequest = RestReloadSecureSettingsAction.PARSER.parse(parser, null);
+ assertEquals("secure_settings_password_string", reloadSecureSettingsRequest.getSecureSettingsPassword().toString());
+ }
+ }
+
+ public void testParserWithoutPassword() throws Exception {
+ final String request = "{" +
+ "}";
+ try (XContentParser parser = XContentType.JSON.xContent()
+ .createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, request)) {
+ NodesReloadSecureSettingsRequest reloadSecureSettingsRequest = RestReloadSecureSettingsAction.PARSER.parse(parser, null);
+ assertThat(reloadSecureSettingsRequest.getSecureSettingsPassword(), nullValue());
+ }
+ }
+}
diff --git a/test/framework/src/main/java/org/elasticsearch/cli/CommandTestCase.java b/test/framework/src/main/java/org/elasticsearch/cli/CommandTestCase.java
index e9c6a2eec9c..e8a518dffd7 100644
--- a/test/framework/src/main/java/org/elasticsearch/cli/CommandTestCase.java
+++ b/test/framework/src/main/java/org/elasticsearch/cli/CommandTestCase.java
@@ -30,9 +30,6 @@ public abstract class CommandTestCase extends ESTestCase {
/** The terminal that execute uses. */
protected final MockTerminal terminal = new MockTerminal();
- /** The last command that was executed. */
- protected Command command;
-
@Before
public void resetTerminal() {
terminal.reset();
@@ -43,13 +40,20 @@ public abstract class CommandTestCase extends ESTestCase {
protected abstract Command newCommand();
/**
- * Runs the command with the given args.
+ * Runs a command with the given args.
*
* Output can be found in {@link #terminal}.
- * The command created can be found in {@link #command}.
*/
public String execute(String... args) throws Exception {
- command = newCommand();
+ return execute(newCommand(), args);
+ }
+
+ /**
+ * Runs the specified command with the given args.
+ *
+ * Output can be found in {@link #terminal}.
+ */
+ public String execute(Command command, String... args) throws Exception {
command.mainWithoutErrorHandling(args, terminal);
return terminal.getOutput();
}
diff --git a/test/framework/src/main/java/org/elasticsearch/cli/MockTerminal.java b/test/framework/src/main/java/org/elasticsearch/cli/MockTerminal.java
index cff5c1b49fb..4959e6436f4 100644
--- a/test/framework/src/main/java/org/elasticsearch/cli/MockTerminal.java
+++ b/test/framework/src/main/java/org/elasticsearch/cli/MockTerminal.java
@@ -85,7 +85,7 @@ public class MockTerminal extends Terminal {
textInput.add(input);
}
- /** Adds an an input that will be return from {@link #readText(String)}. Values are read in FIFO order. */
+ /** Adds an an input that will be return from {@link #readSecret(String)}. Values are read in FIFO order. */
public void addSecretInput(String input) {
secretInput.add(input);
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/transport/netty4/SecurityNetty4Transport.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/transport/netty4/SecurityNetty4Transport.java
index 6e2b9c1a7ef..624b90125b0 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/transport/netty4/SecurityNetty4Transport.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/transport/netty4/SecurityNetty4Transport.java
@@ -131,6 +131,11 @@ public class SecurityNetty4Transport extends Netty4Transport {
return new SslChannelInitializer(name, sslConfiguration);
}
+ @Override
+ public boolean isSecure() {
+ return this.sslEnabled;
+ }
+
private class SecurityClientChannelInitializer extends ClientChannelInitializer {
private final boolean hostnameVerificationEnabled;
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 5ac81a06480..29d7c3a94ac 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
@@ -9,8 +9,8 @@ import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.elasticsearch.ExceptionsHelper;
-import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.ExitCodes;
+import org.elasticsearch.cli.KeyStoreAwareCommand;
import org.elasticsearch.cli.LoggingAwareMultiCommand;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.Terminal.Verbosity;
@@ -125,7 +125,7 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
terminal.println(Verbosity.VERBOSE, "Running with configuration path: " + env.configFile());
- setupOptions(options, env);
+ setupOptions(terminal, options, env);
checkElasticKeystorePasswordValid(terminal, env);
checkClusterHealth(terminal);
@@ -171,7 +171,7 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
terminal.println(Verbosity.VERBOSE, "Running with configuration path: " + env.configFile());
- setupOptions(options, env);
+ setupOptions(terminal, options, env);
checkElasticKeystorePasswordValid(terminal, env);
checkClusterHealth(terminal);
@@ -221,7 +221,7 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
* An abstract class that provides functionality common to both the auto and
* interactive setup modes.
*/
- private abstract class SetupCommand extends EnvironmentAwareCommand {
+ private abstract class SetupCommand extends KeyStoreAwareCommand {
boolean shouldPrompt;
@@ -248,10 +248,9 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
}
}
- void setupOptions(OptionSet options, Environment env) throws Exception {
+ void setupOptions(Terminal terminal, OptionSet options, Environment env) throws Exception {
keyStoreWrapper = keyStoreFunction.apply(env);
- // TODO: We currently do not support keystore passwords
- keyStoreWrapper.decrypt(new char[0]);
+ decryptKeyStore(keyStoreWrapper, terminal);
Settings.Builder settingsBuilder = Settings.builder();
settingsBuilder.put(env.settings(), true);
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java
index 68be01a2e3f..3a2b87afe1f 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java
@@ -32,8 +32,8 @@ import joptsimple.OptionSpec;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.ExitCodes;
+import org.elasticsearch.cli.KeyStoreAwareCommand;
import org.elasticsearch.cli.SuppressForbidden;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
@@ -68,7 +68,7 @@ import org.xml.sax.SAXException;
/**
* CLI tool to generate SAML Metadata for a Service Provider (realm)
*/
-public class SamlMetadataCommand extends EnvironmentAwareCommand {
+public class SamlMetadataCommand extends KeyStoreAwareCommand {
static final String METADATA_SCHEMA = "saml-schema-metadata-2.0.xsd";
@@ -415,13 +415,12 @@ public class SamlMetadataCommand extends EnvironmentAwareCommand {
/**
* @TODO REALM-SETTINGS[TIM] This can be redone a lot now the realm settings are keyed by type
*/
- private RealmConfig findRealm(Terminal terminal, OptionSet options, Environment env) throws UserException, IOException, Exception {
+ private RealmConfig findRealm(Terminal terminal, OptionSet options, Environment env) throws Exception {
keyStoreWrapper = keyStoreFunction.apply(env);
final Settings settings;
if (keyStoreWrapper != null) {
- // TODO: We currently do not support keystore passwords
- keyStoreWrapper.decrypt(new char[0]);
+ decryptKeyStore(keyStoreWrapper, terminal);
final Settings.Builder settingsBuilder = Settings.builder();
settingsBuilder.put(env.settings(), true);
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioTransport.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioTransport.java
index d546b88a8ce..3b7600c55b5 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioTransport.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioTransport.java
@@ -126,6 +126,11 @@ public class SecurityNioTransport extends NioTransport {
};
}
+ @Override
+ public boolean isSecure() {
+ return this.sslEnabled;
+ }
+
private class SecurityTcpChannelFactory extends TcpChannelFactory {
private final String profileName;
diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java
index 4d0e05a5c32..780a101d4e3 100644
--- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java
+++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java
@@ -31,7 +31,6 @@ import org.elasticsearch.xpack.core.security.support.Validation;
import org.elasticsearch.xpack.core.security.user.ElasticUser;
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
import org.elasticsearch.xpack.security.authc.esnative.tool.HttpResponse.HttpResponseBuilder;
-import org.hamcrest.CoreMatchers;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Rule;
@@ -40,6 +39,7 @@ import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mockito;
+import javax.crypto.AEADBadTagException;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.net.HttpURLConnection;
@@ -55,9 +55,11 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
+import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -68,8 +70,11 @@ public class SetupPasswordToolTests extends CommandTestCase {
private final String pathHomeParameter = "-Epath.home=" + createTempDir();
private SecureString bootstrapPassword;
private CommandLineHttpClient httpClient;
- private KeyStoreWrapper keyStore;
private List usersInSetOrder;
+ private KeyStoreWrapper passwordProtectedKeystore;
+ private KeyStoreWrapper keyStore;
+ private KeyStoreWrapper usedKeyStore;
+
@Rule
public ExpectedException thrown = ExpectedException.none();
@@ -79,19 +84,15 @@ public class SetupPasswordToolTests extends CommandTestCase {
boolean useFallback = randomBoolean();
bootstrapPassword = useFallback ? new SecureString("0xCAFEBABE".toCharArray()) :
new SecureString("bootstrap-password".toCharArray());
- this.keyStore = mock(KeyStoreWrapper.class);
- this.httpClient = mock(CommandLineHttpClient.class);
-
- when(keyStore.isLoaded()).thenReturn(true);
- if (useFallback) {
- when(keyStore.getSettingNames()).thenReturn(new HashSet<>(Arrays.asList(ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.getKey(),
- KeyStoreWrapper.SEED_SETTING.getKey())));
- when(keyStore.getString(ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.getKey())).thenReturn(bootstrapPassword);
- } else {
- when(keyStore.getSettingNames()).thenReturn(Collections.singleton(KeyStoreWrapper.SEED_SETTING.getKey()));
- when(keyStore.getString(KeyStoreWrapper.SEED_SETTING.getKey())).thenReturn(bootstrapPassword);
+ keyStore = mockKeystore(false, useFallback);
+ // create a password protected keystore eitherway, so that it can be used for SetupPasswordToolTests#testWrongKeystorePassword
+ passwordProtectedKeystore = mockKeystore(true, useFallback);
+ usedKeyStore = randomFrom(keyStore, passwordProtectedKeystore);
+ if (usedKeyStore.hasPassword()) {
+ terminal.addSecretInput("keystore-password");
}
+ this.httpClient = mock(CommandLineHttpClient.class);
when(httpClient.getDefaultURL()).thenReturn("http://localhost:9200");
HttpResponse httpResponse = new HttpResponse(HttpURLConnection.HTTP_OK, new HashMap());
@@ -122,35 +123,29 @@ public class SetupPasswordToolTests extends CommandTestCase {
}
}
+ private KeyStoreWrapper mockKeystore(boolean isPasswordProtected, boolean useFallback) throws Exception {
+ KeyStoreWrapper keyStore = mock(KeyStoreWrapper.class);
+ when(keyStore.isLoaded()).thenReturn(true);
+ if (useFallback) {
+ when(keyStore.getSettingNames()).thenReturn(new HashSet<>(Arrays.asList(ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.getKey(),
+ KeyStoreWrapper.SEED_SETTING.getKey())));
+ when(keyStore.getString(ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.getKey())).thenReturn(bootstrapPassword);
+ } else {
+ when(keyStore.getSettingNames()).thenReturn(Collections.singleton(KeyStoreWrapper.SEED_SETTING.getKey()));
+ when(keyStore.getString(KeyStoreWrapper.SEED_SETTING.getKey())).thenReturn(bootstrapPassword);
+ }
+ if (isPasswordProtected) {
+ when(keyStore.hasPassword()).thenReturn(true);
+ doNothing().when(keyStore).decrypt("keystore-password".toCharArray());
+ doThrow(new SecurityException("Provided keystore password was incorrect", new AEADBadTagException()))
+ .when(keyStore).decrypt("wrong-password".toCharArray());
+ }
+ return keyStore;
+ }
+
@Override
protected Command newCommand() {
- return new SetupPasswordTool((e, s) -> httpClient, (e) -> keyStore) {
-
- @Override
- protected AutoSetup newAutoSetup() {
- return new AutoSetup() {
- @Override
- protected Environment createEnv(Map settings) throws UserException {
- Settings.Builder builder = Settings.builder();
- settings.forEach((k, v) -> builder.put(k, v));
- return TestEnvironment.newEnvironment(builder.build());
- }
- };
- }
-
- @Override
- protected InteractiveSetup newInteractiveSetup() {
- return new InteractiveSetup() {
- @Override
- protected Environment createEnv(Map settings) throws UserException {
- Settings.Builder builder = Settings.builder();
- settings.forEach((k, v) -> builder.put(k, v));
- return TestEnvironment.newEnvironment(builder.build());
- }
- };
- }
-
- };
+ return getSetupPasswordCommandWithKeyStore(usedKeyStore);
}
public void testAutoSetup() throws Exception {
@@ -161,8 +156,12 @@ public class SetupPasswordToolTests extends CommandTestCase {
terminal.addTextInput("Y");
execute("auto", pathHomeParameter);
}
-
- verify(keyStore).decrypt(new char[0]);
+ if (usedKeyStore.hasPassword()) {
+ // SecureString is already closed (zero-filled) and keystore-password is 17 char long
+ verify(usedKeyStore).decrypt(new char[17]);
+ } else {
+ verify(usedKeyStore).decrypt(new char[0]);
+ }
InOrder inOrder = Mockito.inOrder(httpClient);
@@ -397,7 +396,7 @@ public class SetupPasswordToolTests extends CommandTestCase {
ArgumentCaptor> passwordCaptor = ArgumentCaptor.forClass((Class) CheckedSupplier.class);
inOrder.verify(httpClient).execute(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(bootstrapPassword),
passwordCaptor.capture(), any(CheckedFunction.class));
- assertThat(passwordCaptor.getValue().get(), CoreMatchers.containsString(user + "-password"));
+ assertThat(passwordCaptor.getValue().get(), containsString(user + "-password"));
}
}
@@ -405,6 +404,9 @@ public class SetupPasswordToolTests extends CommandTestCase {
URL url = new URL(httpClient.getDefaultURL());
terminal.reset();
+ if (usedKeyStore.hasPassword()) {
+ terminal.addSecretInput("keystore-password");
+ }
terminal.addTextInput("Y");
for (String user : SetupPasswordTool.USERS) {
// fail in strength and match
@@ -435,10 +437,25 @@ public class SetupPasswordToolTests extends CommandTestCase {
ArgumentCaptor> passwordCaptor = ArgumentCaptor.forClass((Class) CheckedSupplier.class);
inOrder.verify(httpClient).execute(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(bootstrapPassword),
passwordCaptor.capture(), any(CheckedFunction.class));
- assertThat(passwordCaptor.getValue().get(), CoreMatchers.containsString(user + "-password"));
+ assertThat(passwordCaptor.getValue().get(), containsString(user + "-password"));
}
}
+ public void testWrongKeystorePassword() throws Exception {
+ Command commandWithPasswordProtectedKeystore = getSetupPasswordCommandWithKeyStore(passwordProtectedKeystore);
+ terminal.reset();
+ terminal.addSecretInput("wrong-password");
+ final UserException e = expectThrows(UserException.class, () -> {
+ if (randomBoolean()) {
+ execute(commandWithPasswordProtectedKeystore, "auto", pathHomeParameter, "-b", "true");
+ } else {
+ terminal.addTextInput("Y");
+ execute(commandWithPasswordProtectedKeystore, "auto", pathHomeParameter);
+ }
+ });
+ assertThat(e.getMessage(), containsString("Wrong password for elasticsearch.keystore"));
+ }
+
private URL authenticateUrl(URL url) throws MalformedURLException, URISyntaxException {
return new URL(url, (url.toURI().getPath() + "/_security/_authenticate").replaceAll("/+", "/") + "?pretty");
}
@@ -462,4 +479,35 @@ public class SetupPasswordToolTests extends CommandTestCase {
builder.withResponseBody(responseJson);
return builder.build();
}
+
+ private Command getSetupPasswordCommandWithKeyStore(KeyStoreWrapper keyStore) {
+ return new SetupPasswordTool((e, s) -> httpClient, (e) -> keyStore) {
+
+ @Override
+ protected AutoSetup newAutoSetup() {
+ return new AutoSetup() {
+ @Override
+ protected Environment createEnv(Map settings) throws UserException {
+ Settings.Builder builder = Settings.builder();
+ settings.forEach((k, v) -> builder.put(k, v));
+ return TestEnvironment.newEnvironment(builder.build());
+ }
+ };
+ }
+
+ @Override
+ protected InteractiveSetup newInteractiveSetup() {
+ return new InteractiveSetup() {
+ @Override
+ protected Environment createEnv(Map settings) throws UserException {
+ Settings.Builder builder = Settings.builder();
+ settings.forEach((k, v) -> builder.put(k, v));
+ return TestEnvironment.newEnvironment(builder.build());
+ }
+ };
+ }
+
+ };
+
+ }
}
diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommandTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommandTests.java
index 734ea0be0d4..9f9b743cbb2 100644
--- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommandTests.java
+++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommandTests.java
@@ -18,6 +18,7 @@ import org.elasticsearch.env.TestEnvironment;
import org.elasticsearch.xpack.core.security.authc.RealmSettings;
import org.elasticsearch.xpack.core.ssl.CertParsingUtils;
import org.elasticsearch.xpack.core.ssl.PemUtils;
+import org.hamcrest.CoreMatchers;
import org.junit.Before;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
@@ -33,6 +34,7 @@ import org.opensaml.xmlsec.signature.X509Certificate;
import org.opensaml.xmlsec.signature.X509Data;
import org.opensaml.xmlsec.signature.support.SignatureValidator;
+import javax.crypto.AEADBadTagException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -54,25 +56,35 @@ import static org.hamcrest.Matchers.iterableWithSize;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class SamlMetadataCommandTests extends SamlTestCase {
private KeyStoreWrapper keyStore;
+ private KeyStoreWrapper passwordProtectedKeystore;
@Before
public void setup() throws Exception {
SamlUtils.initialize(logger);
this.keyStore = mock(KeyStoreWrapper.class);
when(keyStore.isLoaded()).thenReturn(true);
+ this.passwordProtectedKeystore = mock(KeyStoreWrapper.class);
+ when(passwordProtectedKeystore.isLoaded()).thenReturn(true);
+ when(passwordProtectedKeystore.hasPassword()).thenReturn(true);
+ doNothing().when(passwordProtectedKeystore).decrypt("keystore-password".toCharArray());
+ doThrow(new SecurityException("Provided keystore password was incorrect", new AEADBadTagException()))
+ .when(passwordProtectedKeystore).decrypt("wrong-password".toCharArray());
}
public void testDefaultOptions() throws Exception {
+ final KeyStoreWrapper usedKeyStore = randomFrom(keyStore, passwordProtectedKeystore);
final Path certPath = getDataPath("saml.crt");
final Path keyPath = getDataPath("saml.key");
- final SamlMetadataCommand command = new SamlMetadataCommand((e) -> randomFrom(keyStore, null));
+ final SamlMetadataCommand command = new SamlMetadataCommand((e) -> usedKeyStore);
final OptionSet options = command.getParser().parse(new String[0]);
final boolean useSigningCredentials = randomBoolean();
@@ -93,6 +105,9 @@ public class SamlMetadataCommandTests extends SamlTestCase {
final MockTerminal terminal = new MockTerminal();
+ if (usedKeyStore.hasPassword()) {
+ terminal.addSecretInput("keystore-password");
+ }
// What is the friendly name for "principal" attribute "urn:oid:0.9.2342.19200300.100.1.1" [default: principal]
terminal.addTextInput("");
@@ -147,6 +162,7 @@ public class SamlMetadataCommandTests extends SamlTestCase {
}
public void testFailIfMultipleRealmsExist() throws Exception {
+ final KeyStoreWrapper usedKeyStore = randomFrom(keyStore, passwordProtectedKeystore);
final Settings settings = Settings.builder()
.put("path.home", createTempDir())
.put(RealmSettings.PREFIX + "saml.saml_a.type", "saml")
@@ -158,11 +174,10 @@ public class SamlMetadataCommandTests extends SamlTestCase {
.build();
final Environment env = TestEnvironment.newEnvironment(settings);
- final SamlMetadataCommand command = new SamlMetadataCommand((e) -> randomFrom(keyStore, null));
+ final SamlMetadataCommand command = new SamlMetadataCommand((e) -> usedKeyStore);
final OptionSet options = command.getParser().parse(new String[0]);
- final MockTerminal terminal = new MockTerminal();
-
+ final MockTerminal terminal = getTerminalPossiblyWithPassword(usedKeyStore);
final UserException userException = expectThrows(UserException.class, () -> command.buildEntityDescriptor(terminal, options, env));
assertThat(userException.getMessage(), containsString("multiple SAML realms"));
assertThat(terminal.getErrorOutput(), containsString("saml_a"));
@@ -171,6 +186,7 @@ public class SamlMetadataCommandTests extends SamlTestCase {
}
public void testSpecifyRealmNameAsParameter() throws Exception {
+ final KeyStoreWrapper usedKeyStore = randomFrom(keyStore, passwordProtectedKeystore);
final Settings settings = Settings.builder()
.put("path.home", createTempDir())
.put(RealmSettings.PREFIX + "saml.saml_a.type", "saml")
@@ -182,12 +198,12 @@ public class SamlMetadataCommandTests extends SamlTestCase {
.build();
final Environment env = TestEnvironment.newEnvironment(settings);
- final SamlMetadataCommand command = new SamlMetadataCommand((e) -> keyStore);
+ final SamlMetadataCommand command = new SamlMetadataCommand((e) -> usedKeyStore);
final OptionSet options = command.getParser().parse(new String[] {
"-realm", "saml_b"
});
- final MockTerminal terminal = new MockTerminal();
+ final MockTerminal terminal = getTerminalPossiblyWithPassword(usedKeyStore);
final EntityDescriptor descriptor = command.buildEntityDescriptor(terminal, options, env);
assertThat(descriptor, notNullValue());
@@ -202,6 +218,7 @@ public class SamlMetadataCommandTests extends SamlTestCase {
}
public void testHandleAttributes() throws Exception {
+ final KeyStoreWrapper usedKeyStore = randomFrom(keyStore, passwordProtectedKeystore);
final Settings settings = Settings.builder()
.put("path.home", createTempDir())
.put(RealmSettings.PREFIX + "saml.saml1.type", "saml")
@@ -212,14 +229,13 @@ public class SamlMetadataCommandTests extends SamlTestCase {
.build();
final Environment env = TestEnvironment.newEnvironment(settings);
- final SamlMetadataCommand command = new SamlMetadataCommand((e) -> randomFrom(keyStore, null));
+ final SamlMetadataCommand command = new SamlMetadataCommand((e) -> usedKeyStore);
final OptionSet options = command.getParser().parse(new String[] {
"-attribute", "urn:oid:0.9.2342.19200300.100.1.3",
"-attribute", "groups"
});
- final MockTerminal terminal = new MockTerminal();
-
+ final MockTerminal terminal = getTerminalPossiblyWithPassword(usedKeyStore);
// What is the friendly name for command line attribute "urn:oid:0.9.2342.19200300.100.1.3" [default: none]
terminal.addTextInput("mail");
// What is the standard (urn) name for attribute "groups" (required)
@@ -256,6 +272,7 @@ public class SamlMetadataCommandTests extends SamlTestCase {
}
public void testHandleAttributesInBatchMode() throws Exception {
+ final KeyStoreWrapper usedKeyStore = randomFrom(keyStore, passwordProtectedKeystore);
final Settings settings = Settings.builder()
.put("path.home", createTempDir())
.put(RealmSettings.PREFIX + "saml.saml1.type", "saml")
@@ -265,13 +282,13 @@ public class SamlMetadataCommandTests extends SamlTestCase {
.build();
final Environment env = TestEnvironment.newEnvironment(settings);
- final SamlMetadataCommand command = new SamlMetadataCommand((e) -> randomFrom(keyStore, null));
+ final SamlMetadataCommand command = new SamlMetadataCommand((e) -> usedKeyStore);
final OptionSet options = command.getParser().parse(new String[] {
"-attribute", "urn:oid:0.9.2342.19200300.100.1.3",
"-batch"
});
- final MockTerminal terminal = new MockTerminal();
+ final MockTerminal terminal = getTerminalPossiblyWithPassword(usedKeyStore);
final EntityDescriptor descriptor = command.buildEntityDescriptor(terminal, options, env);
assertThat(descriptor, notNullValue());
@@ -294,10 +311,11 @@ public class SamlMetadataCommandTests extends SamlTestCase {
public void testSigningMetadataWithPfx() throws Exception {
assumeFalse("Can't run in a FIPS JVM, PKCS12 keystores are not usable", inFipsJvm());
+ final KeyStoreWrapper usedKeyStore = randomFrom(keyStore, passwordProtectedKeystore);
final Path certPath = getDataPath("saml.crt");
final Path keyPath = getDataPath("saml.key");
final Path p12Path = getDataPath("saml.p12");
- final SamlMetadataCommand command = new SamlMetadataCommand((e) -> randomFrom(keyStore, null));
+ final SamlMetadataCommand command = new SamlMetadataCommand((e) -> usedKeyStore);
final OptionSet options = command.getParser().parse(new String[]{
"-signing-bundle", p12Path.toString()
});
@@ -319,8 +337,7 @@ public class SamlMetadataCommandTests extends SamlTestCase {
final Settings settings = settingsBuilder.build();
final Environment env = TestEnvironment.newEnvironment(settings);
- final MockTerminal terminal = new MockTerminal();
-
+ final MockTerminal terminal = getTerminalPossiblyWithPassword(usedKeyStore);
// What is the friendly name for "principal" attribute "urn:oid:0.9.2342.19200300.100.1.1" [default: principal]
terminal.addTextInput("");
terminal.addSecretInput("");
@@ -354,10 +371,11 @@ public class SamlMetadataCommandTests extends SamlTestCase {
public void testSigningMetadataWithPasswordProtectedPfx() throws Exception {
assumeFalse("Can't run in a FIPS JVM, PKCS12 keystores are not usable", inFipsJvm());
+ final KeyStoreWrapper usedKeyStore = randomFrom(keyStore, passwordProtectedKeystore);
final Path certPath = getDataPath("saml.crt");
final Path keyPath = getDataPath("saml.key");
final Path p12Path = getDataPath("saml_with_password.p12");
- final SamlMetadataCommand command = new SamlMetadataCommand((e) -> randomFrom(keyStore, null));
+ final SamlMetadataCommand command = new SamlMetadataCommand((e) -> usedKeyStore);
final OptionSet options = command.getParser().parse(new String[]{
"-signing-bundle", p12Path.toString(),
"-signing-key-password", "saml"
@@ -379,8 +397,7 @@ public class SamlMetadataCommandTests extends SamlTestCase {
final Settings settings = settingsBuilder.build();
final Environment env = TestEnvironment.newEnvironment(settings);
- final MockTerminal terminal = new MockTerminal();
-
+ final MockTerminal terminal = getTerminalPossiblyWithPassword(usedKeyStore);
final EntityDescriptor descriptor = command.buildEntityDescriptor(terminal, options, env);
command.possiblySignDescriptor(terminal, options, descriptor, env);
assertThat(descriptor, notNullValue());
@@ -390,10 +407,11 @@ public class SamlMetadataCommandTests extends SamlTestCase {
}
public void testErrorSigningMetadataWithWrongPassword() throws Exception {
+ final KeyStoreWrapper usedKeyStore = randomFrom(keyStore, passwordProtectedKeystore);
final Path certPath = getDataPath("saml.crt");
final Path keyPath = getDataPath("saml.key");
final Path signingKeyPath = getDataPath("saml_with_password.key");
- final SamlMetadataCommand command = new SamlMetadataCommand((e) -> randomFrom(keyStore, null));
+ final SamlMetadataCommand command = new SamlMetadataCommand((e) -> keyStore);
final OptionSet options = command.getParser().parse(new String[]{
"-signing-cert", certPath.toString(),
"-signing-key", signingKeyPath.toString(),
@@ -417,8 +435,7 @@ public class SamlMetadataCommandTests extends SamlTestCase {
final Settings settings = settingsBuilder.build();
final Environment env = TestEnvironment.newEnvironment(settings);
- final MockTerminal terminal = new MockTerminal();
-
+ final MockTerminal terminal = getTerminalPossiblyWithPassword(usedKeyStore);
final EntityDescriptor descriptor = command.buildEntityDescriptor(terminal, options, env);
final UserException userException = expectThrows(UserException.class, () -> command.possiblySignDescriptor(terminal, options,
descriptor, env));
@@ -427,11 +444,12 @@ public class SamlMetadataCommandTests extends SamlTestCase {
}
public void testSigningMetadataWithPem() throws Exception {
+ final KeyStoreWrapper usedKeyStore = randomFrom(keyStore, passwordProtectedKeystore);
//Use this keypair for signing the metadata also
final Path certPath = getDataPath("saml.crt");
final Path keyPath = getDataPath("saml.key");
- final SamlMetadataCommand command = new SamlMetadataCommand((e) -> randomFrom(keyStore, null));
+ final SamlMetadataCommand command = new SamlMetadataCommand((e) -> keyStore);
final OptionSet options = command.getParser().parse(new String[]{
"-signing-cert", certPath.toString(),
"-signing-key", keyPath.toString()
@@ -453,8 +471,7 @@ public class SamlMetadataCommandTests extends SamlTestCase {
final Settings settings = settingsBuilder.build();
final Environment env = TestEnvironment.newEnvironment(settings);
- final MockTerminal terminal = new MockTerminal();
-
+ final MockTerminal terminal = getTerminalPossiblyWithPassword(usedKeyStore);
final EntityDescriptor descriptor = command.buildEntityDescriptor(terminal, options, env);
command.possiblySignDescriptor(terminal, options, descriptor, env);
assertThat(descriptor, notNullValue());
@@ -464,13 +481,14 @@ public class SamlMetadataCommandTests extends SamlTestCase {
}
public void testSigningMetadataWithPasswordProtectedPem() throws Exception {
+ final KeyStoreWrapper usedKeyStore = randomFrom(keyStore, passwordProtectedKeystore);
//Use same keypair for signing the metadata
final Path signingKeyPath = getDataPath("saml_with_password.key");
final Path certPath = getDataPath("saml.crt");
final Path keyPath = getDataPath("saml.key");
- final SamlMetadataCommand command = new SamlMetadataCommand((e) -> keyStore);
+ final SamlMetadataCommand command = new SamlMetadataCommand((e) -> usedKeyStore);
final OptionSet options = command.getParser().parse(new String[]{
"-signing-cert", certPath.toString(),
"-signing-key", signingKeyPath.toString(),
@@ -494,8 +512,7 @@ public class SamlMetadataCommandTests extends SamlTestCase {
final Settings settings = settingsBuilder.build();
final Environment env = TestEnvironment.newEnvironment(settings);
- final MockTerminal terminal = new MockTerminal();
-
+ final MockTerminal terminal = getTerminalPossiblyWithPassword(usedKeyStore);
final EntityDescriptor descriptor = command.buildEntityDescriptor(terminal, options, env);
command.possiblySignDescriptor(terminal, options, descriptor, env);
assertThat(descriptor, notNullValue());
@@ -505,13 +522,14 @@ public class SamlMetadataCommandTests extends SamlTestCase {
}
public void testSigningMetadataWithPasswordProtectedPemInTerminal() throws Exception {
+ final KeyStoreWrapper usedKeyStore = randomFrom(keyStore, passwordProtectedKeystore);
//Use same keypair for signing the metadata
final Path signingKeyPath = getDataPath("saml_with_password.key");
final Path certPath = getDataPath("saml.crt");
final Path keyPath = getDataPath("saml.key");
- final SamlMetadataCommand command = new SamlMetadataCommand((e) -> randomFrom(keyStore, null));
+ final SamlMetadataCommand command = new SamlMetadataCommand((e) -> usedKeyStore);
final OptionSet options = command.getParser().parse(new String[]{
"-signing-cert", certPath.toString(),
"-signing-key", signingKeyPath.toString()
@@ -534,8 +552,7 @@ public class SamlMetadataCommandTests extends SamlTestCase {
final Settings settings = settingsBuilder.build();
final Environment env = TestEnvironment.newEnvironment(settings);
- final MockTerminal terminal = new MockTerminal();
-
+ final MockTerminal terminal = getTerminalPossiblyWithPassword(usedKeyStore);
terminal.addSecretInput("saml");
final EntityDescriptor descriptor = command.buildEntityDescriptor(terminal, options, env);
@@ -547,6 +564,7 @@ public class SamlMetadataCommandTests extends SamlTestCase {
}
public void testDefaultOptionsWithSigningAndMultipleEncryptionKeys() throws Exception {
+ final KeyStoreWrapper usedKeyStore = randomFrom(keyStore, passwordProtectedKeystore);
final Path dir = createTempDir();
final Path ksEncryptionFile = dir.resolve("saml-encryption.p12");
@@ -578,7 +596,7 @@ public class SamlMetadataCommandTests extends SamlTestCase {
secureSettings.setString(RealmSettings.PREFIX + "saml.my_saml.encryption.keystore.secure_password", "ks-password");
secureSettings.setString(RealmSettings.PREFIX + "saml.my_saml.encryption.keystore.secure_key_password", "key-password");
- final SamlMetadataCommand command = new SamlMetadataCommand((e) -> keyStore);
+ final SamlMetadataCommand command = new SamlMetadataCommand((e) -> usedKeyStore);
final OptionSet options = command.getParser().parse(new String[0]);
final boolean useSigningCredentials = randomBoolean();
@@ -603,8 +621,7 @@ public class SamlMetadataCommandTests extends SamlTestCase {
final Settings settings = settingsBuilder.build();
final Environment env = TestEnvironment.newEnvironment(settings);
- final MockTerminal terminal = new MockTerminal();
-
+ final MockTerminal terminal = getTerminalPossiblyWithPassword(usedKeyStore);
// What is the friendly name for "principal" attribute
// "urn:oid:0.9.2342.19200300.100.1.1" [default: principal]
terminal.addTextInput("");
@@ -679,6 +696,27 @@ public class SamlMetadataCommandTests extends SamlTestCase {
}
}
+ public void testWrongKeystorePassword() {
+ final Path certPath = getDataPath("saml.crt");
+ final Path keyPath = getDataPath("saml.key");
+
+ final SamlMetadataCommand command = new SamlMetadataCommand((e) -> passwordProtectedKeystore);
+ final OptionSet options = command.getParser().parse(new String[]{
+ "-signing-cert", certPath.toString(),
+ "-signing-key", keyPath.toString()
+ });
+ final Settings settings = Settings.builder().put("path.home", createTempDir()).build();
+ final Environment env = TestEnvironment.newEnvironment(settings);
+
+ final MockTerminal terminal = new MockTerminal();
+ terminal.addSecretInput("wrong-password");
+
+ UserException e = expectThrows(UserException.class, () -> {
+ command.buildEntityDescriptor(terminal, options, env);
+ });
+ assertThat(e.getMessage(), CoreMatchers.containsString("Wrong password for elasticsearch.keystore"));
+ }
+
private String getAliasName(final Tuple certKeyPair) {
// Keys are pre-generated with the same name, so add the serial no to the alias so that keystore entries won't be overwritten
return certKeyPair.v1().getSubjectX500Principal().getName().toLowerCase(Locale.US) + "-"+
@@ -700,4 +738,12 @@ public class SamlMetadataCommandTests extends SamlTestCase {
return false;
}
}
+
+ private MockTerminal getTerminalPossiblyWithPassword(KeyStoreWrapper keyStore) {
+ final MockTerminal terminal = new MockTerminal();
+ if (keyStore.hasPassword()) {
+ terminal.addSecretInput("keystore-password");
+ }
+ return terminal;
+ }
}