diff --git a/plugin/bin/x-pack/setup-passwords b/plugin/bin/x-pack/setup-passwords new file mode 100644 index 00000000000..2c04396a324 --- /dev/null +++ b/plugin/bin/x-pack/setup-passwords @@ -0,0 +1,104 @@ +#!/bin/bash + +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. + +SCRIPT="$0" + +# SCRIPT may be an arbitrarily deep series of symlinks. Loop until we have the concrete path. +while [ -h "$SCRIPT" ] ; do + ls=`ls -ld "$SCRIPT"` + # Drop everything prior to -> + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + SCRIPT="$link" + else + SCRIPT=`dirname "$SCRIPT"`/"$link" + fi +done + +# determine elasticsearch home +ES_HOME=`dirname "$SCRIPT"`/../.. + +# make ELASTICSEARCH_HOME absolute +ES_HOME=`cd "$ES_HOME"; pwd` + +# If an include wasn't specified in the environment, then search for one... +if [ "x$ES_INCLUDE" = "x" ]; then + # Locations (in order) to use when searching for an include file. + for include in /usr/share/elasticsearch/elasticsearch.in.sh \ + /usr/local/share/elasticsearch/elasticsearch.in.sh \ + /opt/elasticsearch/elasticsearch.in.sh \ + ~/.elasticsearch.in.sh \ + "`dirname "$0"`"/../elasticsearch.in.sh \ + "$ES_HOME/bin/elasticsearch.in.sh"; do + if [ -r "$include" ]; then + . "$include" + break + fi + done +# ...otherwise, source the specified include. +elif [ -r "$ES_INCLUDE" ]; then + . "$ES_INCLUDE" +fi + +if [ -x "$JAVA_HOME/bin/java" ]; then + JAVA="$JAVA_HOME/bin/java" +else + JAVA=`which java` +fi + +if [ ! -x "$JAVA" ]; then + echo "Could not find any executable java binary. Please install java in your PATH or set JAVA_HOME" + exit 1 +fi + +if [ -z "$ES_CLASSPATH" ]; then + echo "You must set the ES_CLASSPATH var" >&2 + exit 1 +fi + +if [ -z "$CONF_DIR" ]; then + # Try to read package config files + if [ -f "/etc/sysconfig/elasticsearch" ]; then + CONF_DIR=/etc/elasticsearch + + . "/etc/sysconfig/elasticsearch" + elif [ -f "/etc/default/elasticsearch" ]; then + CONF_DIR=/etc/elasticsearch + + . "/etc/default/elasticsearch" + fi +fi + +export HOSTNAME=`hostname -s` + +# include x-pack jars in classpath +ES_CLASSPATH="$ES_CLASSPATH:$ES_HOME/plugins/x-pack/*" + +# don't let JAVA_TOOL_OPTIONS slip in (e.g. crazy agents in ubuntu) +# works around https://bugs.launchpad.net/ubuntu/+source/jayatana/+bug/1441487 +if [ "x$JAVA_TOOL_OPTIONS" != "x" ]; then + echo "Warning: Ignoring JAVA_TOOL_OPTIONS=$JAVA_TOOL_OPTIONS" + echo "Please pass JVM parameters via ES_JAVA_OPTS instead" + unset JAVA_TOOL_OPTIONS +fi + +# CONF_FILE setting was removed +if [ ! -z "$CONF_FILE" ]; then + echo "CONF_FILE setting is no longer supported. elasticsearch.yml must be placed in the config directory and cannot be renamed." + exit 1 +fi + +declare -a args=("$@") + +if [ -e "$CONF_DIR" ]; then + args=("${args[@]}" -Edefault.path.conf="$CONF_DIR") +fi + +cd "$ES_HOME" > /dev/null +"$JAVA" $ES_JAVA_OPTS -cp "$ES_CLASSPATH" -Des.path.home="$ES_HOME" org.elasticsearch.xpack.security.authc.esnative.tool.SetupPasswordTool "${args[@]}" +status=$? +cd - > /dev/null +exit $status diff --git a/plugin/bin/x-pack/setup-passwords.bat b/plugin/bin/x-pack/setup-passwords.bat new file mode 100644 index 00000000000..ad7494cccd5 --- /dev/null +++ b/plugin/bin/x-pack/setup-passwords.bat @@ -0,0 +1,9 @@ +@echo off + +rem Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +rem or more contributor license agreements. Licensed under the Elastic License; +rem you may not use this file except in compliance with the Elastic License. + +PUSHD "%~dp0" +CALL "%~dp0.in.bat" org.elasticsearch.xpack.security.authc.esnative.tool.SetupPasswordTool %* +POPD \ No newline at end of file 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 new file mode 100644 index 00000000000..9fcaf65fa1e --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClient.java @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authc.esnative.tool; + +import org.bouncycastle.util.io.Streams; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.SuppressForbidden; +import org.elasticsearch.common.lease.Releasables; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.env.Environment; +import org.elasticsearch.xpack.common.socket.SocketAccess; +import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken; +import org.elasticsearch.xpack.ssl.SSLService; + +import javax.net.ssl.HttpsURLConnection; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import static org.elasticsearch.xpack.security.Security.setting; + +/** + * A simple http client for usage in command line tools. This client only uses internal jdk classes and does + * not rely on an external http libraries. + */ +public class CommandLineHttpClient { + + private final Settings settings; + private final Environment env; + + public CommandLineHttpClient(Settings settings, Environment env) { + this.settings = settings; + 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. + @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(); + HttpURLConnection conn; + // If using SSL, need a custom service because it's likely a self-signed certificate + if ("https".equalsIgnoreCase(uri.getScheme())) { + Settings sslSettings = settings.getByPrefix(setting("http.ssl.")); + final SSLService sslService = new SSLService(settings, env); + final HttpsURLConnection httpsConn = (HttpsURLConnection) url.openConnection(); + AccessController.doPrivileged((PrivilegedAction) () -> { + // Requires permission java.lang.RuntimePermission "setFactory"; + httpsConn.setSSLSocketFactory(sslService.sslSocketFactory(sslSettings)); + return null; + }); + conn = httpsConn; + } else { + conn = (HttpURLConnection) url.openConnection(); + } + conn.setRequestMethod(method); + conn.setReadTimeout(30 * 1000); // 30 second timeout + // Add basic-auth header + String token = UsernamePasswordToken.basicAuthHeaderValue(user, password); + conn.setRequestProperty("Authorization", token); + conn.setRequestProperty("Content-Type", XContentType.JSON.mediaType()); + conn.setDoOutput(bodyString != null); // set true if we are sending a body + SocketAccess.doPrivileged(conn::connect); + if (bodyString != null) { + try (OutputStream out = conn.getOutputStream()) { + out.write(bodyString.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + Releasables.closeWhileHandlingException(conn::disconnect); + throw e; + } + } + try (InputStream inputStream = conn.getInputStream()) { + byte[] bytes = Streams.readAll(inputStream); + return new String(bytes, StandardCharsets.UTF_8); + } catch (IOException e) { + try (InputStream errorStream = conn.getErrorStream()) { + byte[] bytes = Streams.readAll(errorStream); + throw new IOException(new String(bytes, StandardCharsets.UTF_8), e); + } + } finally { + conn.disconnect(); + } + } +} 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 new file mode 100644 index 00000000000..b97b468e204 --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java @@ -0,0 +1,227 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authc.esnative.tool; + +import joptsimple.OptionSet; +import joptsimple.OptionSpec; +import org.elasticsearch.cli.EnvironmentAwareCommand; +import org.elasticsearch.cli.ExitCodes; +import org.elasticsearch.cli.MultiCommand; +import org.elasticsearch.cli.Terminal; +import org.elasticsearch.cli.UserException; +import org.elasticsearch.common.Booleans; +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.settings.SecureString; +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.user.BeatsSystemUser; +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.security.SecureRandom; +import java.util.Arrays; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * 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. + */ +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, BeatsSystemUser.NAME}; + + private final Function clientFunction; + private CommandLineHttpClient client; + + SetupPasswordTool() { + this((environment) -> new CommandLineHttpClient(environment.settings(), environment)); + } + + SetupPasswordTool(Function clientFunction) { + super("Sets the passwords for reserved users"); + subcommands.put("auto", new AutoSetup()); + subcommands.put("interactive", new InteractiveSetup()); + this.clientFunction = clientFunction; + } + + public static void main(String[] args) throws Exception { + exit(new SetupPasswordTool().main(args, Terminal.DEFAULT)); + } + + /** + * This class sets the passwords using automatically generated random passwords. The passwords will be + * printed to the console. + */ + private class AutoSetup extends SetupCommand { + + AutoSetup() { + super("Uses randomly generated passwords"); + } + + @Override + protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception { + setupOptions(options, env); + + if (shouldPrompt) { + terminal.println("Initiating the setup of reserved user " + Arrays.toString(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"); + if (shouldContinue == false) { + throw new UserException(ExitCodes.OK, "User cancelled operation"); + } + } + + SecureRandom secureRandom = new SecureRandom(); + changePasswords(terminal, (user) -> generatePassword(secureRandom, user), + (user, password) -> changedPasswordCallback(terminal, user, password)); + } + + private SecureString generatePassword(SecureRandom secureRandom, String user) { + int passwordLength = 20; // Generate 20 character passwords + char[] characters = new char[passwordLength]; + for (int i = 0; i < passwordLength; ++i) { + characters[i] = CHARS[secureRandom.nextInt(CHARS.length)]; + } + return new SecureString(characters); + } + + private void changedPasswordCallback(Terminal terminal, String user, SecureString password) { + terminal.println("Changed password for user " + user + "\n" + "PASSWORD " + user + " = " + password + "\n"); + } + + } + + /** + * This class sets the passwords using password entered manually by the user from the console. + */ + private class InteractiveSetup extends SetupCommand { + + InteractiveSetup() { + super("Uses passwords entered by a user"); + } + + @Override + protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception { + setupOptions(options, env); + + if (shouldPrompt) { + terminal.println("Initiating the setup of reserved user " + Arrays.toString(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"); + if (shouldContinue == false) { + throw new UserException(ExitCodes.OK, "User cancelled operation"); + } + } + + changePasswords(terminal, 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) { + password1.close(); + throw new UserException(ExitCodes.USAGE, "Passwords for user [" + user+ "] do not match"); + } + } + return password1; + } + + private void changedPasswordCallback(Terminal terminal, String user, SecureString password) { + terminal.println("Changed password for user " + user + "\n"); + } + } + + /** + * An abstract class that provides functionality common to both the auto and interactive setup modes. + */ + private abstract class SetupCommand extends EnvironmentAwareCommand { + + boolean shouldPrompt; + + private OptionSpec urlOption; + private OptionSpec noPromptOption; + + private String elasticUser = ElasticUser.NAME; + private SecureString elasticUserPassword = ReservedRealm.DEFAULT_PASSWORD_TEXT; + private String url; + + SetupCommand(String description) { + super(description); + setParser(); + } + + void setupOptions(OptionSet options, Environment env) { + client = clientFunction.apply(env); + String providedUrl = urlOption.value(options); + url = providedUrl == null ? "http://localhost:9200" : providedUrl; + setShouldPrompt(options); + } + + 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(); + } + + private void setShouldPrompt(OptionSet options) { + String optionalNoPrompt = noPromptOption.value(options); + if (options.has(noPromptOption)) { + shouldPrompt = optionalNoPrompt != null && Booleans.parseBoolean(optionalNoPrompt) == false; + } else { + shouldPrompt = true; + } + } + + 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); + try { + String route = url + "/_xpack/security/user/" + user + "/_password"; + String response = 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(); + } + } + } + + private String buildPayload(SecureString password) throws IOException { + XContentBuilder xContentBuilder = JsonXContent.contentBuilder(); + xContentBuilder.startObject().field("password", password.toString()).endObject(); + return xContentBuilder.string(); + } + } +} 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 new file mode 100644 index 00000000000..b3b013bc68a --- /dev/null +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authc.esnative.tool; + +import org.elasticsearch.cli.Command; +import org.elasticsearch.cli.CommandTestCase; +import org.elasticsearch.cli.ExitCodes; +import org.elasticsearch.cli.UserException; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.xpack.security.user.BeatsSystemUser; +import org.elasticsearch.xpack.security.user.ElasticUser; +import org.elasticsearch.xpack.security.user.KibanaUser; +import org.elasticsearch.xpack.security.user.LogstashSystemUser; +import org.junit.Before; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mockito; + +import java.io.IOException; + +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.contains; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class SetupPasswordToolTests extends CommandTestCase { + + private final String pathHomeParameter = "-Epath.home=" + createTempDir(); + private final String ep = "elastic-password"; + private final String kp = "kibana-password"; + private final String lp = "logstash-password"; + private final String bp = "beats-password"; + private CommandLineHttpClient httpClient; + + @Before + public void setSecrets() { + terminal.addSecretInput(ep); + terminal.addSecretInput(ep); + terminal.addSecretInput(kp); + terminal.addSecretInput(kp); + terminal.addSecretInput(lp); + terminal.addSecretInput(lp); + terminal.addSecretInput(bp); + terminal.addSecretInput(bp); + } + + @Override + protected Command newCommand() { + this.httpClient = mock(CommandLineHttpClient.class); + return new SetupPasswordTool((e) -> httpClient); + } + + public void testAutoSetup() throws Exception { + execute("auto", pathHomeParameter, "-b", "true"); + + ArgumentCaptor passwordCaptor = ArgumentCaptor.forClass(String.class); + SecureString defaultPassword = new SecureString("changeme".toCharArray()); + + 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(defaultPassword), passwordCaptor.capture()); + + String[] users = {KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.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()); + } + } + + public void testUrlOption() throws Exception { + String url = "http://localhost:9202"; + execute("auto", pathHomeParameter, "-u", url, "-b"); + + ArgumentCaptor passwordCaptor = ArgumentCaptor.forClass(String.class); + SecureString defaultPassword = new SecureString("changeme".toCharArray()); + + 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(defaultPassword), passwordCaptor.capture()); + + String[] users = {KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.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()); + } + } + + public void testInteractiveSetup() throws Exception { + terminal.addTextInput("Y"); + + execute("interactive", pathHomeParameter); + + SecureString defaultPassword = new SecureString("changeme".toCharArray()); + + 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(defaultPassword), 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)); + String beatsUrl = "http://localhost:9200/_xpack/security/user/" + BeatsSystemUser.NAME + "/_password"; + inOrder.verify(httpClient).postURL(eq("PUT"), eq(beatsUrl), eq(ElasticUser.NAME), eq(newPassword), contains(bp)); + } + + 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 { + try (XContentParser parser = JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, value)) { + XContentParser.Token token = parser.nextToken(); + if (token == XContentParser.Token.START_OBJECT) { + if (parser.nextToken() == XContentParser.Token.FIELD_NAME) { + if (parser.nextToken() == XContentParser.Token.VALUE_STRING) { + return parser.text(); + } + } + } + } + throw new RuntimeException("Did not properly parse password."); + } +}