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;
|
||||
|
||||
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() {
|
||||
|
|
|
@ -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<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) -> {
|
||||
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<Environment, CommandLineHttpClient> clientFunction,
|
||||
CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction) {
|
||||
SetupPasswordTool(CheckedFunction<Environment, CommandLineHttpClient, Exception> clientFunction,
|
||||
CheckedFunction<Environment, KeyStoreWrapper, Exception> 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<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 {
|
||||
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<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.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));
|
||||
}
|
||||
|
||||
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<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"));
|
||||
}
|
||||
|
||||
verifyZeroInteractions(httpClient);
|
||||
}
|
||||
|
||||
private String parsePassword(String value) throws IOException {
|
||||
|
|
Loading…
Reference in New Issue