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 47e18dfa285..b8bda5a36fa 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 @@ -10,6 +10,7 @@ import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.lease.Releasables; +import org.elasticsearch.common.network.InetAddresses; import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; @@ -26,14 +27,18 @@ import javax.net.ssl.HttpsURLConnection; import java.io.IOException; import java.io.InputStream; 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; import java.security.PrivilegedAction; +import java.util.Collections; import java.util.List; +import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_PORT; import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_PUBLISH_HOST; import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_PUBLISH_PORT; import static org.elasticsearch.xpack.security.Security.setting; @@ -103,12 +108,34 @@ public class CommandLineHttpClient { } } - public String getDefaultURL() { + String getDefaultURL() { final String scheme = XPackSettings.HTTP_SSL_ENABLED.get(settings) ? "https" : "http"; List httpPublishHost = SETTING_HTTP_PUBLISH_HOST.get(settings); - final String host = - (httpPublishHost.isEmpty() ? NetworkService.GLOBAL_NETWORK_PUBLISHHOST_SETTING.get(settings) : httpPublishHost).get(0); - final int port = SETTING_HTTP_PUBLISH_PORT.get(settings); - return scheme + "://" + host + ":" + port; + if (httpPublishHost.isEmpty()) { + httpPublishHost = NetworkService.GLOBAL_NETWORK_PUBLISHHOST_SETTING.get(settings); + } + + // we cannot do custom name resolution here... + NetworkService networkService = new NetworkService(Collections.emptyList()); + try { + InetAddress publishAddress = networkService.resolvePublishHostAddresses(httpPublishHost.toArray(Strings.EMPTY_ARRAY)); + int port = SETTING_HTTP_PUBLISH_PORT.get(settings); + if (port <= 0) { + int[] ports = SETTING_HTTP_PORT.get(settings).ports(); + if (ports.length > 0) { + port = ports[0]; + } + + // this sucks but a port can be specified with a value of 0, we'll never be able to connect to it so just default to + // what we know + if (port <= 0) { + throw new IllegalStateException("unable to determine http port from settings, please use the -u option to provide the" + + " url"); + } + } + return scheme + "://" + InetAddresses.toUriString(publishAddress) + ":" + port; + } catch (IOException e) { + throw new UncheckedIOException("failed to resolve default URL", e); + } } } 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 ea1c029cac1..e9a26d994c3 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 @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.security.authc.esnative.tool; +import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; import org.elasticsearch.cli.EnvironmentAwareCommand; @@ -77,6 +78,11 @@ public class SetupPasswordTool extends MultiCommand { exit(new SetupPasswordTool().main(args, Terminal.DEFAULT)); } + // Visible for testing + OptionParser getParser() { + return this.parser; + } + /** * This class sets the passwords using automatically generated random passwords. The passwords will be * printed to the console. diff --git a/qa/security-setup-password-tests/build.gradle b/qa/security-setup-password-tests/build.gradle new file mode 100644 index 00000000000..f019e70a2c5 --- /dev/null +++ b/qa/security-setup-password-tests/build.gradle @@ -0,0 +1,28 @@ +apply plugin: 'elasticsearch.standalone-rest-test' +apply plugin: 'elasticsearch.rest-test' + +dependencies { + testCompile project(path: ':x-pack-elasticsearch:plugin', configuration: 'runtime') + testCompile project(path: ':x-pack-elasticsearch:plugin', configuration: 'testArtifacts') +} + +integTestRunner { + systemProperty 'tests.security.manager', 'false' +} + +integTestCluster { + plugin ':x-pack-elasticsearch:plugin' + keystoreSetting 'bootstrap.password', 'x-pack-test-password' + setupCommand 'setupTestAdmin', + 'bin/x-pack/users', 'useradd', "test_admin", '-p', 'x-pack-test-password', '-r', "superuser" + waitCondition = { node, ant -> + File tmpFile = new File(node.cwd, 'wait.success') + ant.get(src: "http://${node.httpUri()}/_cluster/health?wait_for_nodes=>=${numNodes}&wait_for_status=yellow", + dest: tmpFile.toString(), + username: 'test_admin', + password: 'x-pack-test-password', + ignoreerrors: true, + retries: 10) + return tmpFile.exists() + } +} diff --git a/qa/security-setup-password-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolIT.java b/qa/security-setup-password-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolIT.java new file mode 100644 index 00000000000..da15bcbc669 --- /dev/null +++ b/qa/security-setup-password-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolIT.java @@ -0,0 +1,125 @@ +/* + * 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.MockTerminal; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.http.HttpHost; +import org.elasticsearch.client.http.message.BasicHeader; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.SuppressForbidden; +import org.elasticsearch.common.io.PathUtils; +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.util.concurrent.ThreadContext; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.elasticsearch.xpack.security.SecurityClusterClientYamlTestCase; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; + +public class SetupPasswordToolIT extends ESRestTestCase { + + @Override + protected Settings restClientSettings() { + String token = basicAuthHeaderValue("test_admin", new SecureString("x-pack-test-password".toCharArray())); + return Settings.builder() + .put(ThreadContext.PREFIX + ".Authorization", token) + .build(); + } + + @SuppressWarnings("unchecked") + public void testSetupPasswordToolAutoSetup() throws Exception { + SecurityClusterClientYamlTestCase.waitForSecurity(); + + final String testConfigDir = System.getProperty("tests.config.dir"); + logger.info("--> CONF: {}", testConfigDir); + final Path configPath = PathUtils.get(testConfigDir); + setSystemPropsForTool(configPath); + + Response nodesResponse = client().performRequest("GET", "/_nodes/http"); + Map nodesMap = entityAsMap(nodesResponse); + + Map nodes = (Map) nodesMap.get("nodes"); + Map firstNode = (Map) nodes.entrySet().iterator().next().getValue(); + Map firstNodeHttp = (Map) firstNode.get("http"); + String nodePublishAddress = (String) firstNodeHttp.get("publish_address"); + final int lastColonIndex = nodePublishAddress.lastIndexOf(':'); + InetAddress actualPublishAddress = InetAddresses.forString(nodePublishAddress.substring(0, lastColonIndex)); + InetAddress expectedPublishAddress = new NetworkService(Collections.emptyList()).resolvePublishHostAddresses(Strings.EMPTY_ARRAY); + final int port = Integer.valueOf(nodePublishAddress.substring(lastColonIndex + 1)); + + List lines = Files.readAllLines(configPath.resolve("elasticsearch.yml")); + lines = lines.stream().filter(s -> s.startsWith("http.port") == false && s.startsWith("http.publish_port") == false) + .collect(Collectors.toList()); + lines.add(randomFrom("http.port", "http.publish_port") + ": " + port); + if (expectedPublishAddress.equals(actualPublishAddress) == false) { + lines.add("http.publish_address: " + InetAddresses.toAddrString(actualPublishAddress)); + } + Files.write(configPath.resolve("elasticsearch.yml"), lines, StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING); + + MockTerminal mockTerminal = new MockTerminal(); + SetupPasswordTool tool = new SetupPasswordTool(); + final int status; + if (randomBoolean()) { + mockTerminal.addTextInput("y"); // answer yes to continue prompt + status = tool.main(new String[] { "auto" }, mockTerminal); + } else { + status = tool.main(new String[] { "auto", "--batch" }, mockTerminal); + } + assertEquals(0, status); + String output = mockTerminal.getOutput(); + logger.info("CLI TOOL OUTPUT:\n{}", output); + String[] outputLines = output.split("\\n"); + Map userPasswordMap = new HashMap<>(); + Arrays.asList(outputLines).forEach(line -> { + if (line.startsWith("PASSWORD ")) { + String[] pieces = line.split(" "); + String user = pieces[1]; + String password = pieces[pieces.length - 1]; + logger.info("user [{}] password [{}]", user, password); + userPasswordMap.put(user, password); + } + }); + + assertEquals(3, userPasswordMap.size()); + userPasswordMap.entrySet().forEach(entry -> { + final String basicHeader = "Basic " + + Base64.getEncoder().encodeToString((entry.getKey() + ":" + entry.getValue()).getBytes(StandardCharsets.UTF_8)); + try { + Response authenticateResponse = client().performRequest("GET", "/_xpack/security/_authenticate", + new BasicHeader("Authorization", basicHeader)); + assertEquals(200, authenticateResponse.getStatusLine().getStatusCode()); + Map userInfoMap = entityAsMap(authenticateResponse); + assertEquals(entry.getKey(), userInfoMap.get("username")); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + + @SuppressForbidden(reason = "need to set sys props for CLI tool") + private void setSystemPropsForTool(Path configPath) { + System.setProperty("es.path.conf", configPath.toString()); + System.setProperty("es.path.home", configPath.getParent().toString()); + } +}