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:
Albert Zaharovits 2017-09-21 11:26:28 +03:00 committed by GitHub
parent 70687fbef3
commit c84c48fa01
3 changed files with 288 additions and 145 deletions

View File

@ -6,7 +6,8 @@
package org.elasticsearch.xpack.security.authc.esnative.tool;
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.SuppressForbidden;
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.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.PortsRange;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.env.Environment;
import org.elasticsearch.http.HttpTransportSettings;
import org.elasticsearch.xpack.XPackSettings;
import org.elasticsearch.xpack.common.socket.SocketAccess;
import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken;
@ -30,7 +29,6 @@ import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
@ -57,16 +55,30 @@ public class CommandLineHttpClient {
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")
public String postURL(String method, String urlString, String user, SecureString password, @Nullable String bodyString)
throws Exception {
URI uri = new URI(urlString);
URL url = uri.toURL();
public int postURL(String method, URL url, String user, SecureString password, CheckedSupplier<String, Exception> requestBodySupplier,
CheckedConsumer<InputStream, Exception> responseConsumer) throws Exception {
HttpURLConnection conn;
// 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."));
final SSLService sslService = new SSLService(settings, env);
final HttpsURLConnection httpsConn = (HttpsURLConnection) url.openConnection();
@ -85,6 +97,7 @@ public class CommandLineHttpClient {
String token = UsernamePasswordToken.basicAuthHeaderValue(user, password);
conn.setRequestProperty("Authorization", token);
conn.setRequestProperty("Content-Type", XContentType.JSON.mediaType());
String bodyString = requestBodySupplier.get();
conn.setDoOutput(bodyString != null); // set true if we are sending a body
SocketAccess.doPrivileged(conn::connect);
if (bodyString != null) {
@ -95,17 +108,19 @@ public class CommandLineHttpClient {
throw e;
}
}
// this throws IOException if there is a network problem
final int ans = conn.getResponseCode();
try (InputStream inputStream = conn.getInputStream()) {
byte[] bytes = Streams.readAll(inputStream);
return new String(bytes, StandardCharsets.UTF_8);
responseConsumer.accept(inputStream);
} catch (IOException e) {
// this IOException is if the HTTP response code is 'BAD' (>= 400)
try (InputStream errorStream = conn.getErrorStream()) {
byte[] bytes = Streams.readAll(errorStream);
throw new IOException(new String(bytes, StandardCharsets.UTF_8), e);
responseConsumer.accept(errorStream);
}
} finally {
conn.disconnect();
Releasables.closeWhileHandlingException(conn::disconnect);
}
return ans;
}
String getDefaultURL() {

View File

@ -8,12 +8,16 @@ package org.elasticsearch.xpack.security.authc.esnative.tool;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.bouncycastle.util.io.Streams;
import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.MultiCommand;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.Terminal.Verbosity;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.common.Booleans;
import org.elasticsearch.common.CheckedBiConsumer;
import org.elasticsearch.common.CheckedFunction;
import org.elasticsearch.common.settings.KeyStoreWrapper;
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.env.Environment;
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.KibanaUser;
import org.elasticsearch.xpack.security.user.LogstashSystemUser;
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.util.Arrays;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.HashMap;
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
* password is set, it will set the remaining user passwords. This tool will only work if the passwords have
* not already been set by something else.
* A tool to set passwords of reserved users (elastic, kibana and
* logstash_system). Can run in `interactive` or `auto` mode. In `auto` mode
* 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 {
private static final char[] CHARS = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" +
"~!@#$%^&*-_=+?").toCharArray();
private static final String[] USERS = new String[]{ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME};
private static final char[] CHARS = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&*-_=+?").toCharArray();
public static final List<String> USERS = Arrays.asList(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 CommandLineHttpClient client;
SetupPasswordTool() {
this((environment) -> new CommandLineHttpClient(environment.settings(), environment),
(environment) -> {
this((environment) -> new CommandLineHttpClient(environment.settings(), environment), (environment) -> {
KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.load(environment.configFile());
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;
});
}
SetupPasswordTool(Function<Environment, CommandLineHttpClient> clientFunction,
SetupPasswordTool(CheckedFunction<Environment, CommandLineHttpClient, Exception> clientFunction,
CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction) {
super("Sets the passwords for reserved users");
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
* printed to the console.
* This class sets the passwords using automatically generated random passwords.
* The passwords will be printed to the console.
*/
class AutoSetup extends SetupCommand {
@ -97,9 +109,10 @@ public class SetupPasswordTool extends MultiCommand {
@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
setupOptions(options, env);
checkElasticKeystorePasswordValid(terminal);
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.");
boolean shouldContinue = terminal.promptYesNo("Please confirm that you would like to continue", false);
terminal.println("\n");
@ -109,7 +122,7 @@ public class SetupPasswordTool extends MultiCommand {
}
SecureRandom secureRandom = new SecureRandom();
changePasswords(terminal, (user) -> generatePassword(secureRandom, user),
changePasswords((user) -> generatePassword(secureRandom, user),
(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 {
@ -140,9 +153,10 @@ public class SetupPasswordTool extends MultiCommand {
@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
setupOptions(options, env);
checkElasticKeystorePasswordValid(terminal);
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.");
boolean shouldContinue = terminal.promptYesNo("Please confirm that you would like to continue", false);
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));
}
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 + "]: "));
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 + "]: "))) {
if (password1.equals(password2) == false) {
terminal.println("Passwords do not match.");
terminal.println("Try again.");
password1.close();
throw new UserException(ExitCodes.USAGE, "Passwords for user [" + user + "] do not match");
continue;
}
}
return password1;
}
}
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 {
@ -205,9 +232,9 @@ public class SetupPasswordTool extends MultiCommand {
}
private void setParser() {
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 "
+ "change password process").withOptionalArg();
urlOption = parser.acceptsAll(Arrays.asList("u", "url"), "The url for the change password request.").withOptionalArg();
noPromptOption = parser.acceptsAll(Arrays.asList("b", "batch"),
"If enabled, run the change password process without prompting the user.").withOptionalArg();
}
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 {
for (String user : USERS) {
changePassword(terminal, url, user, passwordFn, callback);
}
}
private void changePassword(Terminal terminal, String url, String user,
CheckedFunction<String, SecureString, UserException> passwordFn,
BiConsumer<String, SecureString> callback) throws Exception {
boolean isSuperUser = user.equals(elasticUser);
SecureString password = passwordFn.apply(user);
/**
* Validates the bootstrap password from the local keystore by making an
* '_authenticate' call. Returns silently if server is reachable and password is
* valid. Throws {@link UserException} otherwise.
*
* @param terminal
* where to write verbose info.
*/
void checkElasticKeystorePasswordValid(Terminal terminal) throws Exception {
URL route = new URL(url + "/_xpack/security/_authenticate?pretty");
try {
String route = url + "/_xpack/security/user/" + user + "/_password";
client.postURL("PUT", route, elasticUser, elasticUserPassword, buildPayload(password));
callback.accept(user, password);
if (isSuperUser) {
elasticUserPassword = password;
}
} catch (Exception e) {
terminal.println("Exception making http rest request for user [" + user + "]");
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();
terminal.println(Verbosity.VERBOSE, "Testing if bootstrap password is valid for " + route.toString());
int httpCode = client.postURL("GET", route, elasticUser, elasticUserPassword, () -> null, is -> {
byte[] bytes = Streams.readAll(is);
terminal.println(Verbosity.VERBOSE, new String(bytes, StandardCharsets.UTF_8));
});
// keystore password is not valid
if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw new UserException(ExitCodes.CONFIG, "Failed to verify bootstrap password.");
}
} 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.startObject().field("password", password.toString()).endObject();
xContentBuilder.startObject().field("password", supplierPassword.toString()).endObject();
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());
}
}
}
}

View File

@ -10,6 +10,8 @@ import org.elasticsearch.cli.CommandTestCase;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
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.SecureString;
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.env.Environment;
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.KibanaUser;
import org.elasticsearch.xpack.security.user.LogstashSystemUser;
import org.hamcrest.CoreMatchers;
import org.junit.Before;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mockito;
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.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.contains;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
public class SetupPasswordToolTests extends CommandTestCase {
private final String pathHomeParameter = "-Epath.home=" + createTempDir();
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 KeyStoreWrapper keyStore;
private List<String> usersInSetOrder;
@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
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(),
@ -68,14 +71,24 @@ public class SetupPasswordToolTests extends CommandTestCase {
when(keyStore.getSettingNames()).thenReturn(Collections.singleton(KeyStoreWrapper.SEED_SETTING.getKey()));
when(keyStore.getString(KeyStoreWrapper.SEED_SETTING.getKey())).thenReturn(bootstrapPassword);
}
when(httpClient.getDefaultURL()).thenReturn("http://localhost:9200");
terminal.addSecretInput(ep);
terminal.addSecretInput(ep);
terminal.addSecretInput(kp);
terminal.addSecretInput(kp);
terminal.addSecretInput(lp);
terminal.addSecretInput(lp);
when(httpClient.postURL(anyString(), any(URL.class), anyString(), any(SecureString.class), any(CheckedSupplier.class),
any(CheckedConsumer.class))).thenReturn(HttpURLConnection.HTTP_OK);
// elastic user is updated last
usersInSetOrder = new ArrayList<>(SetupPasswordTool.USERS);
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
@ -106,78 +119,110 @@ public class SetupPasswordToolTests extends CommandTestCase {
}
public void testAutoSetup() throws Exception {
String url = httpClient.getDefaultURL();
execute("auto", pathHomeParameter, "-b", "true");
verify(keyStore).decrypt(new char[0]);
ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class);
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};
SecureString newPassword = new SecureString(parsePassword(passwordCaptor.getValue()).toCharArray());
for (String user : users) {
String urlWithRoute = "http://localhost:9200/_xpack/security/user/" + user + "/_password";
inOrder.verify(httpClient).postURL(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(newPassword), anyString());
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");
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 {
String url = "http://localhost:9202";
execute("auto", pathHomeParameter, "-u", url, "-b");
ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class);
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};
SecureString newPassword = new SecureString(parsePassword(passwordCaptor.getValue()).toCharArray());
for (String user : users) {
String urlWithRoute = url + "/_xpack/security/user/" + user + "/_password";
inOrder.verify(httpClient).postURL(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(newPassword), anyString());
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");
inOrder.verify(httpClient).postURL(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(bootstrapPassword),
any(CheckedSupplier.class), any(CheckedConsumer.class));
}
}
public void testInteractiveSetup() throws Exception {
String url = httpClient.getDefaultURL();
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);
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";
inOrder.verify(httpClient).postURL(eq("PUT"), eq(kibanaUrl), eq(ElasticUser.NAME), eq(newPassword), contains(kp));
String logstashUrl = "http://localhost:9200/_xpack/security/user/" + LogstashSystemUser.NAME + "/_password";
inOrder.verify(httpClient).postURL(eq("PUT"), eq(logstashUrl), eq(ElasticUser.NAME), eq(newPassword), contains(lp));
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 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 {