Usability improvement for the password bootstrap tool (reserved users) (elastic/x-pack-elasticsearch#2444)
Generate or prompt for passwords of the reserved users, validating strength and typos, then issue change requests, keeping elastic user change request last. Closes: elastic/x-pack-elasticsearch#2424 Original commit: elastic/x-pack-elasticsearch@1f827d393c
This commit is contained in:
parent
70687fbef3
commit
c84c48fa01
|
@ -6,7 +6,8 @@
|
||||||
package org.elasticsearch.xpack.security.authc.esnative.tool;
|
package org.elasticsearch.xpack.security.authc.esnative.tool;
|
||||||
|
|
||||||
import org.bouncycastle.util.io.Streams;
|
import org.bouncycastle.util.io.Streams;
|
||||||
import org.elasticsearch.common.Nullable;
|
import org.elasticsearch.common.CheckedConsumer;
|
||||||
|
import org.elasticsearch.common.CheckedSupplier;
|
||||||
import org.elasticsearch.common.Strings;
|
import org.elasticsearch.common.Strings;
|
||||||
import org.elasticsearch.common.SuppressForbidden;
|
import org.elasticsearch.common.SuppressForbidden;
|
||||||
import org.elasticsearch.common.lease.Releasables;
|
import org.elasticsearch.common.lease.Releasables;
|
||||||
|
@ -14,10 +15,8 @@ import org.elasticsearch.common.network.InetAddresses;
|
||||||
import org.elasticsearch.common.network.NetworkService;
|
import org.elasticsearch.common.network.NetworkService;
|
||||||
import org.elasticsearch.common.settings.SecureString;
|
import org.elasticsearch.common.settings.SecureString;
|
||||||
import org.elasticsearch.common.settings.Settings;
|
import org.elasticsearch.common.settings.Settings;
|
||||||
import org.elasticsearch.common.transport.PortsRange;
|
|
||||||
import org.elasticsearch.common.xcontent.XContentType;
|
import org.elasticsearch.common.xcontent.XContentType;
|
||||||
import org.elasticsearch.env.Environment;
|
import org.elasticsearch.env.Environment;
|
||||||
import org.elasticsearch.http.HttpTransportSettings;
|
|
||||||
import org.elasticsearch.xpack.XPackSettings;
|
import org.elasticsearch.xpack.XPackSettings;
|
||||||
import org.elasticsearch.xpack.common.socket.SocketAccess;
|
import org.elasticsearch.xpack.common.socket.SocketAccess;
|
||||||
import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken;
|
import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken;
|
||||||
|
@ -30,7 +29,6 @@ import java.io.OutputStream;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.AccessController;
|
import java.security.AccessController;
|
||||||
|
@ -57,16 +55,30 @@ public class CommandLineHttpClient {
|
||||||
this.env = env;
|
this.env = env;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We do not install the security manager when calling from the commandline.
|
/**
|
||||||
// However, doPrivileged blocks will be necessary for any test code that calls this.
|
* General purpose HTTP(S) call with JSON Content-Type and Authorization Header.
|
||||||
|
* SSL settings are read from the settings file, if any.
|
||||||
|
*
|
||||||
|
* @param user
|
||||||
|
* user in the authorization header.
|
||||||
|
* @param password
|
||||||
|
* password in the authorization header.
|
||||||
|
* @param requestBodySupplier
|
||||||
|
* supplier for the JSON string body of the request.
|
||||||
|
* @param responseConsumer
|
||||||
|
* consumer of the response Input Stream.
|
||||||
|
* @return HTTP protocol response code.
|
||||||
|
*
|
||||||
|
* @SuppressForbidden We do not install the security manager when calling from
|
||||||
|
* the commandline. However, doPrivileged blocks will be
|
||||||
|
* necessary for any test code that calls this.
|
||||||
|
*/
|
||||||
@SuppressForbidden(reason = "We call connect in doPrivileged and provide SocketPermission")
|
@SuppressForbidden(reason = "We call connect in doPrivileged and provide SocketPermission")
|
||||||
public String postURL(String method, String urlString, String user, SecureString password, @Nullable String bodyString)
|
public int postURL(String method, URL url, String user, SecureString password, CheckedSupplier<String, Exception> requestBodySupplier,
|
||||||
throws Exception {
|
CheckedConsumer<InputStream, Exception> responseConsumer) throws Exception {
|
||||||
URI uri = new URI(urlString);
|
|
||||||
URL url = uri.toURL();
|
|
||||||
HttpURLConnection conn;
|
HttpURLConnection conn;
|
||||||
// If using SSL, need a custom service because it's likely a self-signed certificate
|
// If using SSL, need a custom service because it's likely a self-signed certificate
|
||||||
if ("https".equalsIgnoreCase(uri.getScheme())) {
|
if ("https".equalsIgnoreCase(url.getProtocol())) {
|
||||||
Settings sslSettings = settings.getByPrefix(setting("http.ssl."));
|
Settings sslSettings = settings.getByPrefix(setting("http.ssl."));
|
||||||
final SSLService sslService = new SSLService(settings, env);
|
final SSLService sslService = new SSLService(settings, env);
|
||||||
final HttpsURLConnection httpsConn = (HttpsURLConnection) url.openConnection();
|
final HttpsURLConnection httpsConn = (HttpsURLConnection) url.openConnection();
|
||||||
|
@ -85,6 +97,7 @@ public class CommandLineHttpClient {
|
||||||
String token = UsernamePasswordToken.basicAuthHeaderValue(user, password);
|
String token = UsernamePasswordToken.basicAuthHeaderValue(user, password);
|
||||||
conn.setRequestProperty("Authorization", token);
|
conn.setRequestProperty("Authorization", token);
|
||||||
conn.setRequestProperty("Content-Type", XContentType.JSON.mediaType());
|
conn.setRequestProperty("Content-Type", XContentType.JSON.mediaType());
|
||||||
|
String bodyString = requestBodySupplier.get();
|
||||||
conn.setDoOutput(bodyString != null); // set true if we are sending a body
|
conn.setDoOutput(bodyString != null); // set true if we are sending a body
|
||||||
SocketAccess.doPrivileged(conn::connect);
|
SocketAccess.doPrivileged(conn::connect);
|
||||||
if (bodyString != null) {
|
if (bodyString != null) {
|
||||||
|
@ -95,17 +108,19 @@ public class CommandLineHttpClient {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// this throws IOException if there is a network problem
|
||||||
|
final int ans = conn.getResponseCode();
|
||||||
try (InputStream inputStream = conn.getInputStream()) {
|
try (InputStream inputStream = conn.getInputStream()) {
|
||||||
byte[] bytes = Streams.readAll(inputStream);
|
responseConsumer.accept(inputStream);
|
||||||
return new String(bytes, StandardCharsets.UTF_8);
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
// this IOException is if the HTTP response code is 'BAD' (>= 400)
|
||||||
try (InputStream errorStream = conn.getErrorStream()) {
|
try (InputStream errorStream = conn.getErrorStream()) {
|
||||||
byte[] bytes = Streams.readAll(errorStream);
|
responseConsumer.accept(errorStream);
|
||||||
throw new IOException(new String(bytes, StandardCharsets.UTF_8), e);
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
conn.disconnect();
|
Releasables.closeWhileHandlingException(conn::disconnect);
|
||||||
}
|
}
|
||||||
|
return ans;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getDefaultURL() {
|
String getDefaultURL() {
|
||||||
|
|
|
@ -8,12 +8,16 @@ package org.elasticsearch.xpack.security.authc.esnative.tool;
|
||||||
import joptsimple.OptionParser;
|
import joptsimple.OptionParser;
|
||||||
import joptsimple.OptionSet;
|
import joptsimple.OptionSet;
|
||||||
import joptsimple.OptionSpec;
|
import joptsimple.OptionSpec;
|
||||||
|
|
||||||
|
import org.bouncycastle.util.io.Streams;
|
||||||
import org.elasticsearch.cli.EnvironmentAwareCommand;
|
import org.elasticsearch.cli.EnvironmentAwareCommand;
|
||||||
import org.elasticsearch.cli.ExitCodes;
|
import org.elasticsearch.cli.ExitCodes;
|
||||||
import org.elasticsearch.cli.MultiCommand;
|
import org.elasticsearch.cli.MultiCommand;
|
||||||
import org.elasticsearch.cli.Terminal;
|
import org.elasticsearch.cli.Terminal;
|
||||||
|
import org.elasticsearch.cli.Terminal.Verbosity;
|
||||||
import org.elasticsearch.cli.UserException;
|
import org.elasticsearch.cli.UserException;
|
||||||
import org.elasticsearch.common.Booleans;
|
import org.elasticsearch.common.Booleans;
|
||||||
|
import org.elasticsearch.common.CheckedBiConsumer;
|
||||||
import org.elasticsearch.common.CheckedFunction;
|
import org.elasticsearch.common.CheckedFunction;
|
||||||
import org.elasticsearch.common.settings.KeyStoreWrapper;
|
import org.elasticsearch.common.settings.KeyStoreWrapper;
|
||||||
import org.elasticsearch.common.settings.SecureString;
|
import org.elasticsearch.common.settings.SecureString;
|
||||||
|
@ -22,43 +26,51 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
||||||
import org.elasticsearch.env.Environment;
|
import org.elasticsearch.env.Environment;
|
||||||
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
|
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
|
||||||
|
import org.elasticsearch.xpack.security.support.Validation;
|
||||||
import org.elasticsearch.xpack.security.user.ElasticUser;
|
import org.elasticsearch.xpack.security.user.ElasticUser;
|
||||||
import org.elasticsearch.xpack.security.user.KibanaUser;
|
import org.elasticsearch.xpack.security.user.KibanaUser;
|
||||||
import org.elasticsearch.xpack.security.user.LogstashSystemUser;
|
import org.elasticsearch.xpack.security.user.LogstashSystemUser;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.ConnectException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.HashMap;
|
||||||
import java.util.function.Function;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A tool to set passwords of internal users. It first sets the elastic user password. After the elastic user
|
* A tool to set passwords of reserved users (elastic, kibana and
|
||||||
* password is set, it will set the remaining user passwords. This tool will only work if the passwords have
|
* logstash_system). Can run in `interactive` or `auto` mode. In `auto` mode
|
||||||
* not already been set by something else.
|
* generates random passwords and prints them on the console. In `interactive`
|
||||||
|
* mode prompts for each individual user's password. This tool only runs once,
|
||||||
|
* if successful. After the elastic user password is set you have to use the
|
||||||
|
* `security` API to manipulate passwords.
|
||||||
*/
|
*/
|
||||||
public class SetupPasswordTool extends MultiCommand {
|
public class SetupPasswordTool extends MultiCommand {
|
||||||
|
|
||||||
private static final char[] CHARS = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" +
|
private static final char[] CHARS = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&*-_=+?").toCharArray();
|
||||||
"~!@#$%^&*-_=+?").toCharArray();
|
public static final List<String> USERS = Arrays.asList(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME);
|
||||||
private static final String[] USERS = new String[]{ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME};
|
|
||||||
|
|
||||||
private final Function<Environment, CommandLineHttpClient> clientFunction;
|
private final CheckedFunction<Environment, CommandLineHttpClient, Exception> clientFunction;
|
||||||
private final CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction;
|
private final CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction;
|
||||||
private CommandLineHttpClient client;
|
private CommandLineHttpClient client;
|
||||||
|
|
||||||
SetupPasswordTool() {
|
SetupPasswordTool() {
|
||||||
this((environment) -> new CommandLineHttpClient(environment.settings(), environment),
|
this((environment) -> new CommandLineHttpClient(environment.settings(), environment), (environment) -> {
|
||||||
(environment) -> {
|
|
||||||
KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.load(environment.configFile());
|
KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.load(environment.configFile());
|
||||||
if (keyStoreWrapper == null) {
|
if (keyStoreWrapper == null) {
|
||||||
throw new UserException(ExitCodes.CONFIG, "Keystore does not exist");
|
throw new UserException(ExitCodes.CONFIG,
|
||||||
|
"Elasticsearch keystore file is missing [" + KeyStoreWrapper.keystorePath(environment.configFile()) + "]");
|
||||||
}
|
}
|
||||||
return keyStoreWrapper;
|
return keyStoreWrapper;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
SetupPasswordTool(Function<Environment, CommandLineHttpClient> clientFunction,
|
SetupPasswordTool(CheckedFunction<Environment, CommandLineHttpClient, Exception> clientFunction,
|
||||||
CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction) {
|
CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction) {
|
||||||
super("Sets the passwords for reserved users");
|
super("Sets the passwords for reserved users");
|
||||||
subcommands.put("auto", newAutoSetup());
|
subcommands.put("auto", newAutoSetup());
|
||||||
|
@ -85,8 +97,8 @@ public class SetupPasswordTool extends MultiCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class sets the passwords using automatically generated random passwords. The passwords will be
|
* This class sets the passwords using automatically generated random passwords.
|
||||||
* printed to the console.
|
* The passwords will be printed to the console.
|
||||||
*/
|
*/
|
||||||
class AutoSetup extends SetupCommand {
|
class AutoSetup extends SetupCommand {
|
||||||
|
|
||||||
|
@ -97,9 +109,10 @@ public class SetupPasswordTool extends MultiCommand {
|
||||||
@Override
|
@Override
|
||||||
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
|
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
|
||||||
setupOptions(options, env);
|
setupOptions(options, env);
|
||||||
|
checkElasticKeystorePasswordValid(terminal);
|
||||||
|
|
||||||
if (shouldPrompt) {
|
if (shouldPrompt) {
|
||||||
terminal.println("Initiating the setup of reserved user " + Arrays.toString(USERS) + " passwords.");
|
terminal.println("Initiating the setup of reserved user " + String.join(",", USERS) + " passwords.");
|
||||||
terminal.println("The passwords will be randomly generated and printed to the console.");
|
terminal.println("The passwords will be randomly generated and printed to the console.");
|
||||||
boolean shouldContinue = terminal.promptYesNo("Please confirm that you would like to continue", false);
|
boolean shouldContinue = terminal.promptYesNo("Please confirm that you would like to continue", false);
|
||||||
terminal.println("\n");
|
terminal.println("\n");
|
||||||
|
@ -109,7 +122,7 @@ public class SetupPasswordTool extends MultiCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
SecureRandom secureRandom = new SecureRandom();
|
SecureRandom secureRandom = new SecureRandom();
|
||||||
changePasswords(terminal, (user) -> generatePassword(secureRandom, user),
|
changePasswords((user) -> generatePassword(secureRandom, user),
|
||||||
(user, password) -> changedPasswordCallback(terminal, user, password));
|
(user, password) -> changedPasswordCallback(terminal, user, password));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +142,7 @@ public class SetupPasswordTool extends MultiCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class sets the passwords using password entered manually by the user from the console.
|
* This class sets the passwords using input prompted on the console
|
||||||
*/
|
*/
|
||||||
class InteractiveSetup extends SetupCommand {
|
class InteractiveSetup extends SetupCommand {
|
||||||
|
|
||||||
|
@ -140,9 +153,10 @@ public class SetupPasswordTool extends MultiCommand {
|
||||||
@Override
|
@Override
|
||||||
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
|
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
|
||||||
setupOptions(options, env);
|
setupOptions(options, env);
|
||||||
|
checkElasticKeystorePasswordValid(terminal);
|
||||||
|
|
||||||
if (shouldPrompt) {
|
if (shouldPrompt) {
|
||||||
terminal.println("Initiating the setup of reserved user " + Arrays.toString(USERS) + " passwords.");
|
terminal.println("Initiating the setup of reserved user " + String.join(",", USERS) + " passwords.");
|
||||||
terminal.println("You will be prompted to enter passwords as the process progresses.");
|
terminal.println("You will be prompted to enter passwords as the process progresses.");
|
||||||
boolean shouldContinue = terminal.promptYesNo("Please confirm that you would like to continue", false);
|
boolean shouldContinue = terminal.promptYesNo("Please confirm that you would like to continue", false);
|
||||||
terminal.println("\n");
|
terminal.println("\n");
|
||||||
|
@ -151,28 +165,41 @@ public class SetupPasswordTool extends MultiCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
changePasswords(terminal, user -> promptForPassword(terminal, user),
|
changePasswords(user -> promptForPassword(terminal, user),
|
||||||
(user, password) -> changedPasswordCallback(terminal, user, password));
|
(user, password) -> changedPasswordCallback(terminal, user, password));
|
||||||
}
|
}
|
||||||
|
|
||||||
private SecureString promptForPassword(Terminal terminal, String user) throws UserException {
|
private SecureString promptForPassword(Terminal terminal, String user) throws UserException {
|
||||||
|
// loop for two consecutive good passwords
|
||||||
|
while (true) {
|
||||||
SecureString password1 = new SecureString(terminal.readSecret("Enter password for [" + user + "]: "));
|
SecureString password1 = new SecureString(terminal.readSecret("Enter password for [" + user + "]: "));
|
||||||
|
Validation.Error err = Validation.Users.validatePassword(password1.getChars());
|
||||||
|
if (err != null) {
|
||||||
|
terminal.println(err.toString());
|
||||||
|
terminal.println("Try again.");
|
||||||
|
password1.close();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
try (SecureString password2 = new SecureString(terminal.readSecret("Reenter password for [" + user + "]: "))) {
|
try (SecureString password2 = new SecureString(terminal.readSecret("Reenter password for [" + user + "]: "))) {
|
||||||
if (password1.equals(password2) == false) {
|
if (password1.equals(password2) == false) {
|
||||||
|
terminal.println("Passwords do not match.");
|
||||||
|
terminal.println("Try again.");
|
||||||
password1.close();
|
password1.close();
|
||||||
throw new UserException(ExitCodes.USAGE, "Passwords for user [" + user + "] do not match");
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return password1;
|
return password1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void changedPasswordCallback(Terminal terminal, String user, SecureString password) {
|
private void changedPasswordCallback(Terminal terminal, String user, SecureString password) {
|
||||||
terminal.println("Changed password for user " + user + "\n");
|
terminal.println("Changed password for user [" + user + "]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract class that provides functionality common to both the auto and interactive setup modes.
|
* 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 EnvironmentAwareCommand {
|
||||||
|
|
||||||
|
@ -205,9 +232,9 @@ public class SetupPasswordTool extends MultiCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setParser() {
|
private void setParser() {
|
||||||
urlOption = parser.acceptsAll(Arrays.asList("u", "url"), "The url for the change password request").withOptionalArg();
|
urlOption = parser.acceptsAll(Arrays.asList("u", "url"), "The url for the change password request.").withOptionalArg();
|
||||||
noPromptOption = parser.acceptsAll(Arrays.asList("b", "batch"), "Whether the user should be prompted to initiate the "
|
noPromptOption = parser.acceptsAll(Arrays.asList("b", "batch"),
|
||||||
+ "change password process").withOptionalArg();
|
"If enabled, run the change password process without prompting the user.").withOptionalArg();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setShouldPrompt(OptionSet options) {
|
private void setShouldPrompt(OptionSet options) {
|
||||||
|
@ -219,42 +246,98 @@ public class SetupPasswordTool extends MultiCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void changePasswords(Terminal terminal, CheckedFunction<String, SecureString, UserException> passwordFn,
|
/**
|
||||||
BiConsumer<String, SecureString> callback) throws Exception {
|
* Validates the bootstrap password from the local keystore by making an
|
||||||
for (String user : USERS) {
|
* '_authenticate' call. Returns silently if server is reachable and password is
|
||||||
changePassword(terminal, url, user, passwordFn, callback);
|
* valid. Throws {@link UserException} otherwise.
|
||||||
}
|
*
|
||||||
}
|
* @param terminal
|
||||||
|
* where to write verbose info.
|
||||||
private void changePassword(Terminal terminal, String url, String user,
|
*/
|
||||||
CheckedFunction<String, SecureString, UserException> passwordFn,
|
void checkElasticKeystorePasswordValid(Terminal terminal) throws Exception {
|
||||||
BiConsumer<String, SecureString> callback) throws Exception {
|
URL route = new URL(url + "/_xpack/security/_authenticate?pretty");
|
||||||
boolean isSuperUser = user.equals(elasticUser);
|
|
||||||
SecureString password = passwordFn.apply(user);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String route = url + "/_xpack/security/user/" + user + "/_password";
|
terminal.println(Verbosity.VERBOSE, "Testing if bootstrap password is valid for " + route.toString());
|
||||||
client.postURL("PUT", route, elasticUser, elasticUserPassword, buildPayload(password));
|
int httpCode = client.postURL("GET", route, elasticUser, elasticUserPassword, () -> null, is -> {
|
||||||
callback.accept(user, password);
|
byte[] bytes = Streams.readAll(is);
|
||||||
if (isSuperUser) {
|
terminal.println(Verbosity.VERBOSE, new String(bytes, StandardCharsets.UTF_8));
|
||||||
elasticUserPassword = password;
|
});
|
||||||
}
|
// keystore password is not valid
|
||||||
} catch (Exception e) {
|
if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
|
||||||
terminal.println("Exception making http rest request for user [" + user + "]");
|
throw new UserException(ExitCodes.CONFIG, "Failed to verify bootstrap password.");
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
// We do not close the password if it is the super user as we are going to use the super user
|
|
||||||
// password in the followup requests to change other user passwords
|
|
||||||
if (isSuperUser == false) {
|
|
||||||
password.close();
|
|
||||||
}
|
}
|
||||||
|
} catch (ConnectException e) {
|
||||||
|
throw new UserException(ExitCodes.CONFIG,
|
||||||
|
"Failed to connect to elasticsearch at " + route.toString() + ". Is the URL correct and elasticsearch running?", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildPayload(SecureString password) throws IOException {
|
/**
|
||||||
|
* Sets one user's password using the elastic superUser credentials.
|
||||||
|
*
|
||||||
|
* @param user
|
||||||
|
* The user who's password will change.
|
||||||
|
* @param password
|
||||||
|
* the new password of the user.
|
||||||
|
*/
|
||||||
|
private void changeUserPassword(String user, SecureString password) throws Exception {
|
||||||
|
URL route = new URL(url + "/_xpack/security/user/" + user + "/_password");
|
||||||
|
try {
|
||||||
|
// supplier should own his resources
|
||||||
|
SecureString supplierPassword = password.clone();
|
||||||
|
client.postURL("PUT", route, elasticUser, elasticUserPassword, () -> {
|
||||||
|
try {
|
||||||
XContentBuilder xContentBuilder = JsonXContent.contentBuilder();
|
XContentBuilder xContentBuilder = JsonXContent.contentBuilder();
|
||||||
xContentBuilder.startObject().field("password", password.toString()).endObject();
|
xContentBuilder.startObject().field("password", supplierPassword.toString()).endObject();
|
||||||
return xContentBuilder.string();
|
return xContentBuilder.string();
|
||||||
|
} finally {
|
||||||
|
supplierPassword.close();
|
||||||
|
}
|
||||||
|
}, is -> {
|
||||||
|
});
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UserException(ExitCodes.TEMP_FAILURE, "Failed to set password for user [" + user + "].", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects passwords for all the users, then issues set requests. Fails on the
|
||||||
|
* first failed request. In this case rerun the tool to redo all the operations.
|
||||||
|
*
|
||||||
|
* @param passwordFn
|
||||||
|
* Function to generate or prompt for each user's password.
|
||||||
|
* @param successCallback
|
||||||
|
* Callback for each successful operation
|
||||||
|
*/
|
||||||
|
void changePasswords(CheckedFunction<String, SecureString, UserException> passwordFn,
|
||||||
|
CheckedBiConsumer<String, SecureString, Exception> successCallback) throws Exception {
|
||||||
|
Map<String, SecureString> passwordsMap = new HashMap<>(USERS.size());
|
||||||
|
try {
|
||||||
|
for (String user : USERS) {
|
||||||
|
passwordsMap.put(user, passwordFn.apply(user));
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Change elastic user last. This tool will not run after the elastic user
|
||||||
|
* password is changed even if changing password for any subsequent user fails.
|
||||||
|
* Stay safe and change elastic last.
|
||||||
|
*/
|
||||||
|
Map.Entry<String, SecureString> superUserEntry = null;
|
||||||
|
for (Map.Entry<String, SecureString> entry : passwordsMap.entrySet()) {
|
||||||
|
if (entry.getKey().equals(elasticUser)) {
|
||||||
|
superUserEntry = entry;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
changeUserPassword(entry.getKey(), entry.getValue());
|
||||||
|
successCallback.accept(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
// change elastic superuser
|
||||||
|
if (superUserEntry != null) {
|
||||||
|
changeUserPassword(superUserEntry.getKey(), superUserEntry.getValue());
|
||||||
|
successCallback.accept(superUserEntry.getKey(), superUserEntry.getValue());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
passwordsMap.forEach((user, pass) -> pass.close());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ import org.elasticsearch.cli.CommandTestCase;
|
||||||
import org.elasticsearch.cli.ExitCodes;
|
import org.elasticsearch.cli.ExitCodes;
|
||||||
import org.elasticsearch.cli.Terminal;
|
import org.elasticsearch.cli.Terminal;
|
||||||
import org.elasticsearch.cli.UserException;
|
import org.elasticsearch.cli.UserException;
|
||||||
|
import org.elasticsearch.common.CheckedConsumer;
|
||||||
|
import org.elasticsearch.common.CheckedSupplier;
|
||||||
import org.elasticsearch.common.settings.KeyStoreWrapper;
|
import org.elasticsearch.common.settings.KeyStoreWrapper;
|
||||||
import org.elasticsearch.common.settings.SecureString;
|
import org.elasticsearch.common.settings.SecureString;
|
||||||
import org.elasticsearch.common.settings.Settings;
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
@ -18,47 +20,48 @@ import org.elasticsearch.common.xcontent.XContentParser;
|
||||||
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
||||||
import org.elasticsearch.env.Environment;
|
import org.elasticsearch.env.Environment;
|
||||||
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
|
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
|
||||||
|
import org.elasticsearch.xpack.security.support.Validation;
|
||||||
import org.elasticsearch.xpack.security.user.ElasticUser;
|
import org.elasticsearch.xpack.security.user.ElasticUser;
|
||||||
import org.elasticsearch.xpack.security.user.KibanaUser;
|
import org.hamcrest.CoreMatchers;
|
||||||
import org.elasticsearch.xpack.security.user.LogstashSystemUser;
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.InOrder;
|
import org.mockito.InOrder;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.GeneralSecurityException;
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.mockito.Matchers.anyString;
|
import static org.mockito.Matchers.anyString;
|
||||||
import static org.mockito.Matchers.contains;
|
|
||||||
import static org.mockito.Matchers.eq;
|
import static org.mockito.Matchers.eq;
|
||||||
|
import static org.mockito.Matchers.any;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class SetupPasswordToolTests extends CommandTestCase {
|
public class SetupPasswordToolTests extends CommandTestCase {
|
||||||
|
|
||||||
private final String pathHomeParameter = "-Epath.home=" + createTempDir();
|
private final String pathHomeParameter = "-Epath.home=" + createTempDir();
|
||||||
private SecureString bootstrapPassword;
|
private SecureString bootstrapPassword;
|
||||||
private final String ep = "elastic-password";
|
|
||||||
private final String kp = "kibana-password";
|
|
||||||
private final String lp = "logstash-password";
|
|
||||||
private CommandLineHttpClient httpClient;
|
private CommandLineHttpClient httpClient;
|
||||||
private KeyStoreWrapper keyStore;
|
private KeyStoreWrapper keyStore;
|
||||||
|
private List<String> usersInSetOrder;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setSecretsAndKeyStore() throws GeneralSecurityException {
|
public void setSecretsAndKeyStore() throws Exception {
|
||||||
// sometimes we fall back to the keystore seed as this is the default when a new node starts
|
// sometimes we fall back to the keystore seed as this is the default when a new node starts
|
||||||
boolean useFallback = randomBoolean();
|
boolean useFallback = randomBoolean();
|
||||||
bootstrapPassword = useFallback ? new SecureString("0xCAFEBABE".toCharArray()) :
|
bootstrapPassword = useFallback ? new SecureString("0xCAFEBABE".toCharArray()) :
|
||||||
new SecureString("bootstrap-password".toCharArray());
|
new SecureString("bootstrap-password".toCharArray());
|
||||||
this.keyStore = mock(KeyStoreWrapper.class);
|
this.keyStore = mock(KeyStoreWrapper.class);
|
||||||
this.httpClient = mock(CommandLineHttpClient.class);
|
this.httpClient = mock(CommandLineHttpClient.class);
|
||||||
|
|
||||||
when(keyStore.isLoaded()).thenReturn(true);
|
when(keyStore.isLoaded()).thenReturn(true);
|
||||||
if (useFallback) {
|
if (useFallback) {
|
||||||
when(keyStore.getSettingNames()).thenReturn(new HashSet<>(Arrays.asList(ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.getKey(),
|
when(keyStore.getSettingNames()).thenReturn(new HashSet<>(Arrays.asList(ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.getKey(),
|
||||||
|
@ -68,14 +71,24 @@ public class SetupPasswordToolTests extends CommandTestCase {
|
||||||
when(keyStore.getSettingNames()).thenReturn(Collections.singleton(KeyStoreWrapper.SEED_SETTING.getKey()));
|
when(keyStore.getSettingNames()).thenReturn(Collections.singleton(KeyStoreWrapper.SEED_SETTING.getKey()));
|
||||||
when(keyStore.getString(KeyStoreWrapper.SEED_SETTING.getKey())).thenReturn(bootstrapPassword);
|
when(keyStore.getString(KeyStoreWrapper.SEED_SETTING.getKey())).thenReturn(bootstrapPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
when(httpClient.getDefaultURL()).thenReturn("http://localhost:9200");
|
when(httpClient.getDefaultURL()).thenReturn("http://localhost:9200");
|
||||||
|
|
||||||
terminal.addSecretInput(ep);
|
when(httpClient.postURL(anyString(), any(URL.class), anyString(), any(SecureString.class), any(CheckedSupplier.class),
|
||||||
terminal.addSecretInput(ep);
|
any(CheckedConsumer.class))).thenReturn(HttpURLConnection.HTTP_OK);
|
||||||
terminal.addSecretInput(kp);
|
|
||||||
terminal.addSecretInput(kp);
|
// elastic user is updated last
|
||||||
terminal.addSecretInput(lp);
|
usersInSetOrder = new ArrayList<>(SetupPasswordTool.USERS);
|
||||||
terminal.addSecretInput(lp);
|
for (int i = 0; i < usersInSetOrder.size() - 1; i++) {
|
||||||
|
if (ElasticUser.NAME.equals(usersInSetOrder.get(i))) {
|
||||||
|
Collections.swap(usersInSetOrder, i, i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String user : SetupPasswordTool.USERS) {
|
||||||
|
terminal.addSecretInput(user + "-password");
|
||||||
|
terminal.addSecretInput(user + "-password");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -106,78 +119,110 @@ public class SetupPasswordToolTests extends CommandTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testAutoSetup() throws Exception {
|
public void testAutoSetup() throws Exception {
|
||||||
|
String url = httpClient.getDefaultURL();
|
||||||
execute("auto", pathHomeParameter, "-b", "true");
|
execute("auto", pathHomeParameter, "-b", "true");
|
||||||
|
|
||||||
verify(keyStore).decrypt(new char[0]);
|
verify(keyStore).decrypt(new char[0]);
|
||||||
|
|
||||||
ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class);
|
|
||||||
|
|
||||||
InOrder inOrder = Mockito.inOrder(httpClient);
|
InOrder inOrder = Mockito.inOrder(httpClient);
|
||||||
String elasticUrl = "http://localhost:9200/_xpack/security/user/elastic/_password";
|
|
||||||
inOrder.verify(httpClient).postURL(eq("PUT"), eq(elasticUrl), eq(ElasticUser.NAME), eq(bootstrapPassword),
|
|
||||||
passwordCaptor.capture());
|
|
||||||
|
|
||||||
String[] users = {KibanaUser.NAME, LogstashSystemUser.NAME};
|
URL checkUrl = new URL(url + "/_xpack/security/_authenticate?pretty");
|
||||||
SecureString newPassword = new SecureString(parsePassword(passwordCaptor.getValue()).toCharArray());
|
inOrder.verify(httpClient).postURL(eq("GET"), eq(checkUrl), eq(ElasticUser.NAME), eq(bootstrapPassword), any(CheckedSupplier.class),
|
||||||
for (String user : users) {
|
any(CheckedConsumer.class));
|
||||||
String urlWithRoute = "http://localhost:9200/_xpack/security/user/" + user + "/_password";
|
for (String user : usersInSetOrder) {
|
||||||
inOrder.verify(httpClient).postURL(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(newPassword), anyString());
|
URL urlWithRoute = new URL(url + "/_xpack/security/user/" + user + "/_password");
|
||||||
|
inOrder.verify(httpClient).postURL(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(bootstrapPassword),
|
||||||
|
any(CheckedSupplier.class), any(CheckedConsumer.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testAuthnFail() throws Exception {
|
||||||
|
URL authnURL = new URL(httpClient.getDefaultURL() + "/_xpack/security/_authenticate?pretty");
|
||||||
|
when(httpClient.postURL(eq("GET"), eq(authnURL), eq(ElasticUser.NAME), any(SecureString.class), any(CheckedSupplier.class),
|
||||||
|
any(CheckedConsumer.class))).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED);
|
||||||
|
|
||||||
|
try {
|
||||||
|
execute(randomBoolean() ? "auto" : "interactive", pathHomeParameter);
|
||||||
|
fail("Should have thrown exception");
|
||||||
|
} catch (UserException e) {
|
||||||
|
assertEquals(ExitCodes.CONFIG, e.exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testUrlOption() throws Exception {
|
public void testUrlOption() throws Exception {
|
||||||
String url = "http://localhost:9202";
|
String url = "http://localhost:9202";
|
||||||
execute("auto", pathHomeParameter, "-u", url, "-b");
|
execute("auto", pathHomeParameter, "-u", url, "-b");
|
||||||
|
|
||||||
ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class);
|
|
||||||
|
|
||||||
InOrder inOrder = Mockito.inOrder(httpClient);
|
InOrder inOrder = Mockito.inOrder(httpClient);
|
||||||
String elasticUrl = url + "/_xpack/security/user/elastic/_password";
|
|
||||||
inOrder.verify(httpClient).postURL(eq("PUT"), eq(elasticUrl), eq(ElasticUser.NAME), eq(bootstrapPassword),
|
|
||||||
passwordCaptor.capture());
|
|
||||||
|
|
||||||
String[] users = {KibanaUser.NAME, LogstashSystemUser.NAME};
|
URL checkUrl = new URL(url + "/_xpack/security/_authenticate?pretty");
|
||||||
SecureString newPassword = new SecureString(parsePassword(passwordCaptor.getValue()).toCharArray());
|
inOrder.verify(httpClient).postURL(eq("GET"), eq(checkUrl), eq(ElasticUser.NAME), eq(bootstrapPassword), any(CheckedSupplier.class),
|
||||||
for (String user : users) {
|
any(CheckedConsumer.class));
|
||||||
String urlWithRoute = url + "/_xpack/security/user/" + user + "/_password";
|
for (String user : usersInSetOrder) {
|
||||||
inOrder.verify(httpClient).postURL(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(newPassword), anyString());
|
URL urlWithRoute = new URL(url + "/_xpack/security/user/" + user + "/_password");
|
||||||
|
inOrder.verify(httpClient).postURL(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(bootstrapPassword),
|
||||||
|
any(CheckedSupplier.class), any(CheckedConsumer.class));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testInteractiveSetup() throws Exception {
|
public void testInteractiveSetup() throws Exception {
|
||||||
|
String url = httpClient.getDefaultURL();
|
||||||
|
|
||||||
terminal.addTextInput("Y");
|
terminal.addTextInput("Y");
|
||||||
|
execute("interactive", pathHomeParameter);
|
||||||
|
|
||||||
|
InOrder inOrder = Mockito.inOrder(httpClient);
|
||||||
|
|
||||||
|
URL checkUrl = new URL(url + "/_xpack/security/_authenticate?pretty");
|
||||||
|
inOrder.verify(httpClient).postURL(eq("GET"), eq(checkUrl), eq(ElasticUser.NAME), eq(bootstrapPassword), any(CheckedSupplier.class),
|
||||||
|
any(CheckedConsumer.class));
|
||||||
|
for (String user : usersInSetOrder) {
|
||||||
|
URL urlWithRoute = new URL(url + "/_xpack/security/user/" + user + "/_password");
|
||||||
|
ArgumentCaptor<CheckedSupplier<String, Exception>> passwordCaptor = ArgumentCaptor.forClass((Class) CheckedSupplier.class);
|
||||||
|
inOrder.verify(httpClient).postURL(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(bootstrapPassword),
|
||||||
|
passwordCaptor.capture(), any(CheckedConsumer.class));
|
||||||
|
assertThat(passwordCaptor.getValue().get(), CoreMatchers.containsString(user + "-password"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testInteractivePasswordsFatFingers() throws Exception {
|
||||||
|
String url = httpClient.getDefaultURL();
|
||||||
|
|
||||||
|
terminal.reset();
|
||||||
|
terminal.addTextInput("Y");
|
||||||
|
for (String user : SetupPasswordTool.USERS) {
|
||||||
|
// fail in strength and match
|
||||||
|
int failCount = randomIntBetween(3, 10);
|
||||||
|
while (failCount-- > 0) {
|
||||||
|
String password1 = randomAlphaOfLength(randomIntBetween(3, 10));
|
||||||
|
terminal.addSecretInput(password1);
|
||||||
|
Validation.Error err = Validation.Users.validatePassword(password1.toCharArray());
|
||||||
|
if (err == null) {
|
||||||
|
// passes strength validation, fail by mismatch
|
||||||
|
terminal.addSecretInput(password1 + "typo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// two good passwords
|
||||||
|
terminal.addSecretInput(user + "-password");
|
||||||
|
terminal.addSecretInput(user + "-password");
|
||||||
|
}
|
||||||
|
|
||||||
execute("interactive", pathHomeParameter);
|
execute("interactive", pathHomeParameter);
|
||||||
|
|
||||||
InOrder inOrder = Mockito.inOrder(httpClient);
|
InOrder inOrder = Mockito.inOrder(httpClient);
|
||||||
String elasticUrl = "http://localhost:9200/_xpack/security/user/elastic/_password";
|
|
||||||
SecureString newPassword = new SecureString(ep.toCharArray());
|
|
||||||
inOrder.verify(httpClient).postURL(eq("PUT"), eq(elasticUrl), eq(ElasticUser.NAME), eq(bootstrapPassword), contains(ep));
|
|
||||||
|
|
||||||
String kibanaUrl = "http://localhost:9200/_xpack/security/user/" + KibanaUser.NAME + "/_password";
|
URL checkUrl = new URL(url + "/_xpack/security/_authenticate?pretty");
|
||||||
inOrder.verify(httpClient).postURL(eq("PUT"), eq(kibanaUrl), eq(ElasticUser.NAME), eq(newPassword), contains(kp));
|
inOrder.verify(httpClient).postURL(eq("GET"), eq(checkUrl), eq(ElasticUser.NAME), eq(bootstrapPassword), any(CheckedSupplier.class),
|
||||||
String logstashUrl = "http://localhost:9200/_xpack/security/user/" + LogstashSystemUser.NAME + "/_password";
|
any(CheckedConsumer.class));
|
||||||
inOrder.verify(httpClient).postURL(eq("PUT"), eq(logstashUrl), eq(ElasticUser.NAME), eq(newPassword), contains(lp));
|
for (String user : usersInSetOrder) {
|
||||||
|
URL urlWithRoute = new URL(url + "/_xpack/security/user/" + user + "/_password");
|
||||||
|
ArgumentCaptor<CheckedSupplier<String, Exception>> passwordCaptor = ArgumentCaptor.forClass((Class) CheckedSupplier.class);
|
||||||
|
inOrder.verify(httpClient).postURL(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(bootstrapPassword),
|
||||||
|
passwordCaptor.capture(), any(CheckedConsumer.class));
|
||||||
|
assertThat(passwordCaptor.getValue().get(), CoreMatchers.containsString(user + "-password"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testInteractivePasswordsNotMatching() throws Exception {
|
|
||||||
String ep = "elastic-password";
|
|
||||||
|
|
||||||
terminal.reset();
|
|
||||||
terminal.addTextInput("Y");
|
|
||||||
terminal.addSecretInput(ep);
|
|
||||||
terminal.addSecretInput(ep + "typo");
|
|
||||||
String url = "http://localhost:9200";
|
|
||||||
|
|
||||||
try {
|
|
||||||
execute("interactive", pathHomeParameter, "-u", url);
|
|
||||||
fail("Should have thrown exception");
|
|
||||||
} catch (UserException e) {
|
|
||||||
assertEquals(ExitCodes.USAGE, e.exitCode);
|
|
||||||
assertEquals("Passwords for user [elastic] do not match", e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyZeroInteractions(httpClient);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String parsePassword(String value) throws IOException {
|
private String parsePassword(String value) throws IOException {
|
||||||
|
|
Loading…
Reference in New Issue