diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClient.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClient.java index b8bda5a36fa..e7191d0dfe0 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClient.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClient.java @@ -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 requestBodySupplier, + CheckedConsumer 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() { diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java index 04a265e405e..8ba7840bf22 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java @@ -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,44 +26,52 @@ 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 USERS = Arrays.asList(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME); - private final Function clientFunction; + private final CheckedFunction clientFunction; private final CheckedFunction keyStoreFunction; private CommandLineHttpClient client; SetupPasswordTool() { - 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"); - } - return keyStoreWrapper; - }); + this((environment) -> new CommandLineHttpClient(environment.settings(), environment), (environment) -> { + KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.load(environment.configFile()); + if (keyStoreWrapper == null) { + throw new UserException(ExitCodes.CONFIG, + "Elasticsearch keystore file is missing [" + KeyStoreWrapper.keystorePath(environment.configFile()) + "]"); + } + return keyStoreWrapper; + }); } - SetupPasswordTool(Function clientFunction, - CheckedFunction keyStoreFunction) { + SetupPasswordTool(CheckedFunction clientFunction, + CheckedFunction keyStoreFunction) { super("Sets the passwords for reserved users"); subcommands.put("auto", newAutoSetup()); subcommands.put("interactive", newInteractiveSetup()); @@ -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 { - SecureString password1 = new SecureString(terminal.readSecret("Enter password for [" + user + "]: ")); - try (SecureString password2 = new SecureString(terminal.readSecret("Reenter password for [" + user + "]: "))) { - if (password1.equals(password2) == false) { + // 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(); - throw new UserException(ExitCodes.USAGE, "Passwords for user [" + user + "] do not match"); + 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(); + continue; + } + } + return password1; } - 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 { @@ -197,7 +224,7 @@ public class SetupPasswordTool extends MultiCommand { url = providedUrl == null ? client.getDefaultURL() : providedUrl; setShouldPrompt(options); - // TODO: We currently do not support keystore passwords + // TODO: We currently do not support keystore passwords keyStore.decrypt(new char[0]); Settings build = Settings.builder().setSecureSettings(keyStore).build(); elasticUserPassword = ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD.get(build); @@ -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 passwordFn, - BiConsumer callback) throws Exception { - for (String user : USERS) { - changePassword(terminal, url, user, passwordFn, callback); - } - } - - private void changePassword(Terminal terminal, String url, String user, - CheckedFunction passwordFn, - BiConsumer 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 { - XContentBuilder xContentBuilder = JsonXContent.contentBuilder(); - xContentBuilder.startObject().field("password", password.toString()).endObject(); - return xContentBuilder.string(); + /** + * 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", 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 passwordFn, + CheckedBiConsumer successCallback) throws Exception { + Map 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 superUserEntry = null; + for (Map.Entry 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()); + } } } } diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java index 3d451d699ed..89b08eb0dc4 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java @@ -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 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 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 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> 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)); - } - - 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()); + 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> 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")); } - - verifyZeroInteractions(httpClient); } private String parsePassword(String value) throws IOException {