From 236f64a70ec81ffd994632fd490fe35dc11f7100 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Mon, 11 Dec 2017 15:45:34 -0500 Subject: [PATCH] SQL: Fix SSL for JDBC and CLI for real this time (elastic/x-pack-elasticsearch#3277) Previously I'd added tests for JDBC and CLI that I *thought* used SSL but they didn't! I wasn't careful... Testing changes: * Actually enable SSL/HTTPS in the `qa:sql:security:ssl` subproject. * Rework how `RemoteCli` handles security. This allows us to configure SSL, the keystore, and the username and password in a much less error prone way. * Fix up JDBC tests to properly use SSL. * Allow the `CliFixture` to specify the keystore location. * Switch `CliFixture` and `RemoteCli` from sending the password in the connection string to filling out the prompt for it. * Have `CliFixture` also send the keystore password when a keystore is configured. This makes the following production code changes: * Allow the CLI to configure the keystore location with the `-k`/`-keystore_location` parameters. * If the keystore location is configured then the CLI will prompt for the password. * Allow the configuration of urls starting with `https`. * Improve the exception thrown when the URL doesn't parse by adding a suppressed exception with the original parse error, before we tried to add `http://` to the front of it. Original commit: elastic/x-pack-elasticsearch@97fac4a3b411e990b91a345ef4626958897405df --- qa/sql/security/no-ssl/build.gradle | 4 + .../xpack/qa/sql/security/CliErrorsIT.java | 10 +- .../xpack/qa/sql/security/CliFetchSizeIT.java | 10 +- .../xpack/qa/sql/security/CliSecurityIT.java | 67 ++- .../xpack/qa/sql/security/CliSelectIT.java | 10 +- .../xpack/qa/sql/security/CliShowIT.java | 10 +- .../qa/sql/security/JdbcConnectionIT.java | 5 + .../xpack/qa/sql/security/JdbcCsvSpecIT.java | 5 + .../sql/security/JdbcDatabaseMetaDataIT.java | 5 + .../xpack/qa/sql/security/JdbcErrorsIT.java | 5 + .../qa/sql/security/JdbcFetchSizeIT.java | 5 + .../xpack/qa/sql/security/JdbcSecurityIT.java | 32 +- .../qa/sql/security/JdbcShowTablesIT.java | 5 + .../qa/sql/security/JdbcSimpleExampleIT.java | 5 + .../xpack/qa/sql/security/JdbcSqlSpecIT.java | 5 + .../xpack/qa/sql/security/RestSqlIT.java | 33 +- .../qa/sql/security/RestSqlSecurityIT.java | 1 - .../qa/sql/security/SqlSecurityTestCase.java | 7 + .../src/test/resources/plugin-security.policy | 4 + qa/sql/security/ssl/build.gradle | 395 ++++++++++++++++-- .../qa/sql/cli/CliIntegrationTestCase.java | 10 +- .../xpack/qa/sql/cli/RemoteCli.java | 100 ++++- .../qa/sql/jdbc/JdbcIntegrationTestCase.java | 11 +- .../org/elasticsearch/xpack/sql/cli/Cli.java | 26 +- .../xpack/sql/cli/ConnectionBuilder.java | 51 ++- .../xpack/sql/cli/ConnectionBuilderTests.java | 47 ++- .../sql/jdbc/jdbc/JdbcConfiguration.java | 12 +- .../xpack/sql/client/shared/SslConfig.java | 19 +- .../xpack/sql/client/shared/UriUtils.java | 10 +- .../src/test/resources/plugin-security.policy | 6 - .../xpack/sql/cli/fixture/CliFixture.java | 6 +- 31 files changed, 790 insertions(+), 131 deletions(-) delete mode 100644 sql/shared-client/src/test/resources/plugin-security.policy diff --git a/qa/sql/security/no-ssl/build.gradle b/qa/sql/security/no-ssl/build.gradle index 95c61b7a8b7..9ea95bb4a57 100644 --- a/qa/sql/security/no-ssl/build.gradle +++ b/qa/sql/security/no-ssl/build.gradle @@ -1,3 +1,7 @@ +integTestRunner { + systemProperty 'tests.ssl.enabled', 'false' +} + integTestCluster { waitCondition = { node, ant -> File tmpFile = new File(node.cwd, 'wait.success') diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliErrorsIT.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliErrorsIT.java index 5f4ce2e356c..b59c9f91282 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliErrorsIT.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliErrorsIT.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.qa.sql.security; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.xpack.qa.sql.cli.ErrorsTestCase; +import org.elasticsearch.xpack.qa.sql.cli.RemoteCli.SecurityConfig; public class CliErrorsIT extends ErrorsTestCase { @Override @@ -15,7 +16,12 @@ public class CliErrorsIT extends ErrorsTestCase { } @Override - protected String esUrlPrefix() { - return CliSecurityIT.adminEsUrlPrefix(); + protected String getProtocol() { + return RestSqlIT.SSL_ENABLED ? "https" : "http"; + } + + @Override + protected SecurityConfig securityConfig() { + return CliSecurityIT.adminSecurityConfig(); } } diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliFetchSizeIT.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliFetchSizeIT.java index 91147cc65c2..8420f8a2a7c 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliFetchSizeIT.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliFetchSizeIT.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.qa.sql.security; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.xpack.qa.sql.cli.FetchSizeTestCase; +import org.elasticsearch.xpack.qa.sql.cli.RemoteCli.SecurityConfig; public class CliFetchSizeIT extends FetchSizeTestCase { @Override @@ -15,7 +16,12 @@ public class CliFetchSizeIT extends FetchSizeTestCase { } @Override - protected String esUrlPrefix() { - return CliSecurityIT.adminEsUrlPrefix(); + protected String getProtocol() { + return RestSqlIT.SSL_ENABLED ? "https" : "http"; + } + + @Override + protected SecurityConfig securityConfig() { + return CliSecurityIT.adminSecurityConfig(); } } diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliSecurityIT.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliSecurityIT.java index e20b490d721..76b66ebb8ee 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliSecurityIT.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliSecurityIT.java @@ -6,8 +6,12 @@ package org.elasticsearch.xpack.qa.sql.security; import org.elasticsearch.common.CheckedConsumer; +import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.xpack.qa.sql.cli.RemoteCli; - +import org.elasticsearch.xpack.qa.sql.cli.RemoteCli.SecurityConfig; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -19,18 +23,43 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.startsWith; public class CliSecurityIT extends SqlSecurityTestCase { - static final String NO_INIT_CONNECTION_CHECK_PREFIX = "-c false "; - static String adminEsUrlPrefix() { - return "test_admin:x-pack-test-password@"; + static SecurityConfig adminSecurityConfig() { + String keystoreLocation; + String keystorePassword; + if (RestSqlIT.SSL_ENABLED) { + Path keyStore; + try { + keyStore = PathUtils.get(RestSqlIT.class.getResource("/test-node.jks").toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException("exception while reading the store", e); + } + if (!Files.exists(keyStore)) { + throw new IllegalStateException("Keystore file [" + keyStore + "] does not exist."); + } + keystoreLocation = keyStore.toAbsolutePath().toString(); + keystorePassword = "keypass"; + } else { + keystoreLocation = null; + keystorePassword = null; + } + return new SecurityConfig(RestSqlIT.SSL_ENABLED, "test_admin", "x-pack-test-password", keystoreLocation, keystorePassword); } /** * Perform security test actions using the CLI. */ private static class CliActions implements Actions { + private SecurityConfig userSecurity(String user) { + SecurityConfig admin = adminSecurityConfig(); + if (user == null) { + return admin; + } + return new SecurityConfig(RestSqlIT.SSL_ENABLED, user, "testpass", admin.keystoreLocation(), admin.keystorePassword()); + } + @Override public void queryWorksAsAdmin() throws Exception { - try (RemoteCli cli = new RemoteCli(adminEsUrlPrefix() + elasticsearchAddress())) { + try (RemoteCli cli = new RemoteCli(elasticsearchAddress(), true, adminSecurityConfig())) { assertThat(cli.command("SELECT * FROM test ORDER BY a"), containsString("a | b | c")); assertEquals("---------------+---------------+---------------", cli.readLine()); assertThat(cli.readLine(), containsString("1 |2 |3")); @@ -56,7 +85,7 @@ public class CliSecurityIT extends SqlSecurityTestCase { public void expectMatchesAdmin(String adminSql, String user, String userSql, CheckedConsumer customizer) throws Exception { List adminResult = new ArrayList<>(); - try (RemoteCli cli = new RemoteCli(adminEsUrlPrefix() + elasticsearchAddress())) { + try (RemoteCli cli = new RemoteCli(elasticsearchAddress(), true, adminSecurityConfig())) { customizer.accept(cli); adminResult.add(cli.command(adminSql)); String line; @@ -68,7 +97,7 @@ public class CliSecurityIT extends SqlSecurityTestCase { } Iterator expected = adminResult.iterator(); - try (RemoteCli cli = new RemoteCli(userPrefix(user) + elasticsearchAddress())) { + try (RemoteCli cli = new RemoteCli(elasticsearchAddress(), true, userSecurity(user))) { customizer.accept(cli); assertTrue(expected.hasNext()); assertEquals(expected.next(), cli.command(userSql)); @@ -86,7 +115,7 @@ public class CliSecurityIT extends SqlSecurityTestCase { @Override public void expectDescribe(Map columns, String user) throws Exception { - try (RemoteCli cli = new RemoteCli(userPrefix(user) + elasticsearchAddress())) { + try (RemoteCli cli = new RemoteCli(elasticsearchAddress(), true, userSecurity(user))) { assertThat(cli.command("DESCRIBE test"), containsString("column | type")); assertEquals("---------------+---------------", cli.readLine()); for (Map.Entry column : columns.entrySet()) { @@ -98,7 +127,7 @@ public class CliSecurityIT extends SqlSecurityTestCase { @Override public void expectShowTables(List tables, String user) throws Exception { - try (RemoteCli cli = new RemoteCli(userPrefix(user) + elasticsearchAddress())) { + try (RemoteCli cli = new RemoteCli(elasticsearchAddress(), true, userSecurity(user))) { assertThat(cli.command("SHOW TABLES"), containsString("table")); assertEquals("---------------", cli.readLine()); for (String table : tables) { @@ -110,7 +139,7 @@ public class CliSecurityIT extends SqlSecurityTestCase { @Override public void expectUnknownIndex(String user, String sql) throws Exception { - try (RemoteCli cli = new RemoteCli(userPrefix(user) + elasticsearchAddress())) { + try (RemoteCli cli = new RemoteCli(elasticsearchAddress(), true, userSecurity(user))) { assertThat(cli.command(sql), containsString("Bad request")); assertThat(cli.readLine(), containsString("Unknown index")); } @@ -118,29 +147,25 @@ public class CliSecurityIT extends SqlSecurityTestCase { @Override public void expectForbidden(String user, String sql) throws Exception { - // Skip initial check to make sure it doesn't trip - try (RemoteCli cli = new RemoteCli(NO_INIT_CONNECTION_CHECK_PREFIX + userPrefix(user) + elasticsearchAddress())) { + /* + * Cause the CLI to skip its connection test on startup so we + * can get a forbidden exception when we run the query. + */ + try (RemoteCli cli = new RemoteCli(elasticsearchAddress(), false, userSecurity(user))) { assertThat(cli.command(sql), containsString("is unauthorized for user [" + user + "]")); } } @Override public void expectUnknownColumn(String user, String sql, String column) throws Exception { - try (RemoteCli cli = new RemoteCli(userPrefix(user) + elasticsearchAddress())) { + try (RemoteCli cli = new RemoteCli(elasticsearchAddress(), true, userSecurity(user))) { assertThat(cli.command(sql), containsString("[1;31mBad request")); assertThat(cli.readLine(), containsString("Unknown column [" + column + "][1;23;31m][0m")); } } - - private String userPrefix(String user) { - if (user == null) { - return adminEsUrlPrefix(); - } - return user + ":testpass@"; - } } public CliSecurityIT() { super(new CliActions()); } -} \ No newline at end of file +} diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliSelectIT.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliSelectIT.java index d1b12a1a3fb..706fbf13f66 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliSelectIT.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliSelectIT.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.qa.sql.security; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.xpack.qa.sql.cli.RemoteCli.SecurityConfig; import org.elasticsearch.xpack.qa.sql.cli.SelectTestCase; public class CliSelectIT extends SelectTestCase { @@ -15,7 +16,12 @@ public class CliSelectIT extends SelectTestCase { } @Override - protected String esUrlPrefix() { - return CliSecurityIT.adminEsUrlPrefix(); + protected String getProtocol() { + return RestSqlIT.SSL_ENABLED ? "https" : "http"; + } + + @Override + protected SecurityConfig securityConfig() { + return CliSecurityIT.adminSecurityConfig(); } } diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliShowIT.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliShowIT.java index 121c333795b..f1f9d2a6258 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliShowIT.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/CliShowIT.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.qa.sql.security; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.xpack.qa.sql.cli.RemoteCli.SecurityConfig; import org.elasticsearch.xpack.qa.sql.cli.ShowTestCase; public class CliShowIT extends ShowTestCase { @@ -15,7 +16,12 @@ public class CliShowIT extends ShowTestCase { } @Override - protected String esUrlPrefix() { - return CliSecurityIT.adminEsUrlPrefix(); + protected String getProtocol() { + return RestSqlIT.SSL_ENABLED ? "https" : "http"; + } + + @Override + protected SecurityConfig securityConfig() { + return CliSecurityIT.adminSecurityConfig(); } } diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcConnectionIT.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcConnectionIT.java index 1c66ff51d64..08aa73f68b9 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcConnectionIT.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcConnectionIT.java @@ -16,6 +16,11 @@ public class JdbcConnectionIT extends ConnectionTestCase { return RestSqlIT.securitySettings(); } + @Override + protected String getProtocol() { + return RestSqlIT.SSL_ENABLED ? "https" : "http"; + } + @Override protected Properties connectionProperties() { Properties properties = super.connectionProperties(); diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcCsvSpecIT.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcCsvSpecIT.java index 2f322d91feb..3375b663404 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcCsvSpecIT.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcCsvSpecIT.java @@ -20,6 +20,11 @@ public class JdbcCsvSpecIT extends CsvSpecTestCase { return RestSqlIT.securitySettings(); } + @Override + protected String getProtocol() { + return RestSqlIT.SSL_ENABLED ? "https" : "http"; + } + @Override protected Properties connectionProperties() { Properties sp = super.connectionProperties(); diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcDatabaseMetaDataIT.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcDatabaseMetaDataIT.java index a12428d91f9..f4aafe4090b 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcDatabaseMetaDataIT.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcDatabaseMetaDataIT.java @@ -16,6 +16,11 @@ public class JdbcDatabaseMetaDataIT extends DatabaseMetaDataTestCase { return RestSqlIT.securitySettings(); } + @Override + protected String getProtocol() { + return RestSqlIT.SSL_ENABLED ? "https" : "http"; + } + @Override protected Properties connectionProperties() { Properties properties = super.connectionProperties(); diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcErrorsIT.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcErrorsIT.java index ceab2673ca9..2ed8ac7941f 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcErrorsIT.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcErrorsIT.java @@ -16,6 +16,11 @@ public class JdbcErrorsIT extends ErrorsTestCase { return RestSqlIT.securitySettings(); } + @Override + protected String getProtocol() { + return RestSqlIT.SSL_ENABLED ? "https" : "http"; + } + @Override protected Properties connectionProperties() { Properties properties = super.connectionProperties(); diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcFetchSizeIT.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcFetchSizeIT.java index f78743fb7f7..ac239193e99 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcFetchSizeIT.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcFetchSizeIT.java @@ -16,6 +16,11 @@ public class JdbcFetchSizeIT extends FetchSizeTestCase { return RestSqlIT.securitySettings(); } + @Override + protected String getProtocol() { + return RestSqlIT.SSL_ENABLED ? "https" : "http"; + } + @Override protected Properties connectionProperties() { Properties properties = super.connectionProperties(); diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcSecurityIT.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcSecurityIT.java index 80ba2fe3449..2391946ceaa 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcSecurityIT.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcSecurityIT.java @@ -8,7 +8,11 @@ package org.elasticsearch.xpack.qa.sql.security; import org.elasticsearch.action.admin.indices.get.GetIndexAction; import org.elasticsearch.common.CheckedConsumer; import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.xpack.qa.sql.jdbc.LocalH2; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; @@ -21,6 +25,7 @@ import java.util.Properties; import static org.elasticsearch.xpack.qa.sql.jdbc.JdbcAssert.assertResultSets; import static org.elasticsearch.xpack.qa.sql.jdbc.JdbcIntegrationTestCase.elasticsearchAddress; import static org.elasticsearch.xpack.qa.sql.jdbc.JdbcIntegrationTestCase.randomKnownTimeZone; +import static org.elasticsearch.xpack.qa.sql.security.RestSqlIT.SSL_ENABLED; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; @@ -31,6 +36,7 @@ public class JdbcSecurityIT extends SqlSecurityTestCase { properties.put("user", "test_admin"); properties.put("password", "x-pack-test-password"); // end::admin_properties + addSslPropertiesIfNeeded(properties); return properties; } @@ -38,7 +44,8 @@ public class JdbcSecurityIT extends SqlSecurityTestCase { Properties props = new Properties(); props.put("timezone", randomKnownTimeZone()); props.putAll(properties); - return DriverManager.getConnection("jdbc:es://" + elasticsearchAddress(), props); + String scheme = SSL_ENABLED ? "https" : "http"; + return DriverManager.getConnection("jdbc:es://" + scheme + "://" + elasticsearchAddress(), props); } static Properties userProperties(String user) { @@ -48,9 +55,32 @@ public class JdbcSecurityIT extends SqlSecurityTestCase { Properties prop = new Properties(); prop.put("user", user); prop.put("password", "testpass"); + addSslPropertiesIfNeeded(prop); return prop; } + private static void addSslPropertiesIfNeeded(Properties properties) { + if (false == SSL_ENABLED) { + return; + } + Path keyStore; + try { + keyStore = PathUtils.get(RestSqlIT.class.getResource("/test-node.jks").toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException("exception while reading the store", e); + } + if (!Files.exists(keyStore)) { + throw new IllegalStateException("Keystore file [" + keyStore + "] does not exist."); + } + String keyStoreStr = keyStore.toAbsolutePath().toString(); + + properties.put("ssl", "true"); + properties.put("ssl.keystore.location", keyStoreStr); + properties.put("ssl.keystore.pass", "keypass"); + properties.put("ssl.truststore.location", keyStoreStr); + properties.put("ssl.truststore.pass", "keypass"); + } + static void expectActionMatchesAdmin(CheckedFunction adminAction, String user, CheckedFunction userAction) throws Exception { try (Connection adminConnection = es(adminProperties()); diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcShowTablesIT.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcShowTablesIT.java index 879a8b29728..ab76b3f33a1 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcShowTablesIT.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcShowTablesIT.java @@ -16,6 +16,11 @@ public class JdbcShowTablesIT extends ShowTablesTestCase { return RestSqlIT.securitySettings(); } + @Override + protected String getProtocol() { + return RestSqlIT.SSL_ENABLED ? "https" : "http"; + } + @Override protected Properties connectionProperties() { Properties sp = super.connectionProperties(); diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcSimpleExampleIT.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcSimpleExampleIT.java index 4a0cb3aee6f..b01fe72333b 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcSimpleExampleIT.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcSimpleExampleIT.java @@ -16,6 +16,11 @@ public class JdbcSimpleExampleIT extends SimpleExampleTestCase { return RestSqlIT.securitySettings(); } + @Override + protected String getProtocol() { + return RestSqlIT.SSL_ENABLED ? "https" : "http"; + } + @Override protected Properties connectionProperties() { Properties properties = super.connectionProperties(); diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcSqlSpecIT.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcSqlSpecIT.java index 749ba367ea4..609847f513e 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcSqlSpecIT.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/JdbcSqlSpecIT.java @@ -20,6 +20,11 @@ public class JdbcSqlSpecIT extends SqlSpecTestCase { return RestSqlIT.securitySettings(); } + @Override + protected String getProtocol() { + return RestSqlIT.SSL_ENABLED ? "https" : "http"; + } + @Override protected Properties connectionProperties() { Properties sp = super.connectionProperties(); diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/RestSqlIT.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/RestSqlIT.java index d54ab667510..bbcc47bb0da 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/RestSqlIT.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/RestSqlIT.java @@ -5,27 +5,54 @@ */ package org.elasticsearch.xpack.qa.sql.security; +import org.elasticsearch.common.Booleans; +import org.elasticsearch.common.io.PathUtils; 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.qa.sql.rest.RestSqlTestCase; import static org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; + /** * Integration test for the rest sql action. The one that speaks json directly to a * user rather than to the JDBC driver or CLI. */ public class RestSqlIT extends RestSqlTestCase { + static final boolean SSL_ENABLED = Booleans.parseBoolean(System.getProperty("tests.ssl.enabled")); + static Settings securitySettings() { String token = basicAuthHeaderValue("test_admin", new SecureString("x-pack-test-password".toCharArray())); - return Settings.builder() - .put(ThreadContext.PREFIX + ".Authorization", token) - .build(); + Settings.Builder builder = Settings.builder() + .put(ThreadContext.PREFIX + ".Authorization", token); + if (SSL_ENABLED) { + Path keyStore; + try { + keyStore = PathUtils.get(RestSqlIT.class.getResource("/test-node.jks").toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException("exception while reading the store", e); + } + if (!Files.exists(keyStore)) { + throw new IllegalStateException("Keystore file [" + keyStore + "] does not exist."); + } + builder.put(ESRestTestCase.TRUSTSTORE_PATH, keyStore) + .put(ESRestTestCase.TRUSTSTORE_PASSWORD, "keypass"); + } + return builder.build(); } @Override protected Settings restClientSettings() { return securitySettings(); } + + @Override + protected String getProtocol() { + return RestSqlIT.SSL_ENABLED ? "https" : "http"; + } } diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/RestSqlSecurityIT.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/RestSqlSecurityIT.java index 86a3bb20fa8..57283b892e2 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/RestSqlSecurityIT.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/RestSqlSecurityIT.java @@ -26,7 +26,6 @@ import java.util.Map; import static java.util.Collections.singletonMap; import static org.elasticsearch.xpack.qa.sql.rest.RestSqlTestCase.columnInfo; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; diff --git a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/SqlSecurityTestCase.java b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/SqlSecurityTestCase.java index 6f9f188d776..fed2e93dc07 100644 --- a/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/SqlSecurityTestCase.java +++ b/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/SqlSecurityTestCase.java @@ -49,6 +49,8 @@ import static org.hamcrest.Matchers.hasItems; public abstract class SqlSecurityTestCase extends ESRestTestCase { /** * Actions taken by this test. + *

+ * For methods that take {@code user} a {@code null} user means "use the admin". */ protected interface Actions { void queryWorksAsAdmin() throws Exception; @@ -181,6 +183,11 @@ public abstract class SqlSecurityTestCase extends ESRestTestCase { } } + @Override + protected String getProtocol() { + return RestSqlIT.SSL_ENABLED ? "https" : "http"; + } + public void testQueryWorksAsAdmin() throws Exception { actions.queryWorksAsAdmin(); new AuditLogAsserter() diff --git a/qa/sql/security/src/test/resources/plugin-security.policy b/qa/sql/security/src/test/resources/plugin-security.policy index ac22b9e7d73..d013547b9fd 100644 --- a/qa/sql/security/src/test/resources/plugin-security.policy +++ b/qa/sql/security/src/test/resources/plugin-security.policy @@ -1,4 +1,8 @@ grant { // Needed to read the audit log file permission java.io.FilePermission "${tests.audit.logfile}", "read"; + + //// Required by ssl subproject: + // Required for the net client to setup ssl rather than use global ssl. + permission java.lang.RuntimePermission "setFactory"; }; diff --git a/qa/sql/security/ssl/build.gradle b/qa/sql/security/ssl/build.gradle index 7b4019b776c..ffe66b816df 100644 --- a/qa/sql/security/ssl/build.gradle +++ b/qa/sql/security/ssl/build.gradle @@ -1,61 +1,366 @@ +import org.elasticsearch.gradle.LoggedExec +import org.elasticsearch.gradle.MavenFilteringHack import org.elasticsearch.gradle.test.NodeInfo +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory import java.nio.charset.StandardCharsets +import java.security.KeyStore +import java.security.SecureRandom -String outputDir = "generated-resources/${project.name}" -task copyTestNodeKeystore(type: Copy) { - from project(':x-pack-elasticsearch:plugin') - .file('src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks') - into outputDir +// Tell the tests we're running with ssl enabled +integTestRunner { + systemProperty 'tests.ssl.enabled', 'true' } +// needed to be consistent with ssl host checking +Object san = new SanEvaluator() + +// location of generated keystores and certificates +File keystoreDir = new File(project.buildDir, 'keystore') + +// Generate the node's keystore +File nodeKeystore = new File(keystoreDir, 'test-node.jks') +task createNodeKeyStore(type: LoggedExec) { + doFirst { + if (nodeKeystore.parentFile.exists() == false) { + nodeKeystore.parentFile.mkdirs() + } + if (nodeKeystore.exists()) { + delete nodeKeystore + } + } + executable = new File(project.javaHome, 'bin/keytool') + standardInput = new ByteArrayInputStream('FirstName LastName\nUnit\nOrganization\nCity\nState\nNL\nyes\n\n'.getBytes('UTF-8')) + args '-genkey', + '-alias', 'test-node', + '-keystore', nodeKeystore, + '-keyalg', 'RSA', + '-keysize', '2048', + '-validity', '712', + '-dname', 'CN=smoke-test-plugins-ssl', + '-keypass', 'keypass', + '-storepass', 'keypass', + '-ext', san +} + +// Generate the client's keystore +File clientKeyStore = new File(keystoreDir, 'test-client.jks') +task createClientKeyStore(type: LoggedExec) { + doFirst { + if (clientKeyStore.parentFile.exists() == false) { + clientKeyStore.parentFile.mkdirs() + } + if (clientKeyStore.exists()) { + delete clientKeyStore + } + } + executable = new File(project.javaHome, 'bin/keytool') + standardInput = new ByteArrayInputStream('FirstName LastName\nUnit\nOrganization\nCity\nState\nNL\nyes\n\n'.getBytes('UTF-8')) + args '-genkey', + '-alias', 'test-client', + '-keystore', clientKeyStore, + '-keyalg', 'RSA', + '-keysize', '2048', + '-validity', '712', + '-dname', 'CN=smoke-test-plugins-ssl', + '-keypass', 'keypass', + '-storepass', 'keypass', + '-ext', san +} + +// Export the node's certificate +File nodeCertificate = new File(keystoreDir, 'test-node.cert') +task exportNodeCertificate(type: LoggedExec) { + doFirst { + if (nodeCertificate.parentFile.exists() == false) { + nodeCertificate.parentFile.mkdirs() + } + if (nodeCertificate.exists()) { + delete nodeCertificate + } + } + executable = new File(project.javaHome, 'bin/keytool') + args '-export', + '-alias', 'test-node', + '-keystore', nodeKeystore, + '-storepass', 'keypass', + '-file', nodeCertificate +} + +// Import the node certificate in the client's keystore +task importNodeCertificateInClientKeyStore(type: LoggedExec) { + dependsOn exportNodeCertificate + executable = new File(project.javaHome, 'bin/keytool') + args '-import', + '-alias', 'test-node', + '-keystore', clientKeyStore, + '-storepass', 'keypass', + '-file', nodeCertificate, + '-noprompt' +} + +// Export the client's certificate +File clientCertificate = new File(keystoreDir, 'test-client.cert') +task exportClientCertificate(type: LoggedExec) { + doFirst { + if (clientCertificate.parentFile.exists() == false) { + clientCertificate.parentFile.mkdirs() + } + if (clientCertificate.exists()) { + delete clientCertificate + } + } + executable = new File(project.javaHome, 'bin/keytool') + args '-export', + '-alias', 'test-client', + '-keystore', clientKeyStore, + '-storepass', 'keypass', + '-file', clientCertificate +} + +// Import the client certificate in the node's keystore +task importClientCertificateInNodeKeyStore(type: LoggedExec) { + dependsOn exportClientCertificate + executable = new File(project.javaHome, 'bin/keytool') + args '-import', + '-alias', 'test-client', + '-keystore', nodeKeystore, + '-storepass', 'keypass', + '-file', clientCertificate, + '-noprompt' +} + +forbiddenPatterns { + exclude '**/*.cert' +} + +// Add keystores to test classpath: it expects it there +sourceSets.test.resources.srcDir(keystoreDir) +processTestResources.dependsOn( + createNodeKeyStore, createClientKeyStore, + importNodeCertificateInClientKeyStore, importClientCertificateInNodeKeyStore +) + +integTestCluster.dependsOn(importClientCertificateInNodeKeyStore) + + integTestCluster { + // The setup that we actually want + setting 'xpack.security.http.ssl.enabled', 'true' + setting 'xpack.security.transport.ssl.enabled', 'true' + + // ceremony to set up ssl + setting 'xpack.ssl.keystore.path', 'test-node.jks' + keystoreSetting 'xpack.ssl.keystore.secure_password', 'keypass' + + // copy keystores into config/ + extraConfigFile nodeKeystore.name, nodeKeystore + extraConfigFile clientKeyStore.name, clientKeyStore + // Override the wait condition to work properly with security and SSL waitCondition = { NodeInfo node, AntBuilder ant -> File tmpFile = new File(node.cwd, 'wait.success') - - // wait up to two minutes - final long stopTime = System.currentTimeMillis() + (2 * 60000L); - Exception lastException = null; - - while (System.currentTimeMillis() < stopTime) { - lastException = null; - // we use custom wait logic here as the elastic user is not available immediately and ant.get will fail when a 401 is returned - HttpURLConnection httpURLConnection = null; - try { - httpURLConnection = (HttpURLConnection) new URL("http://${node.httpUri()}/_cluster/health?wait_for_nodes=${numNodes}&wait_for_status=yellow").openConnection(); - httpURLConnection.setRequestProperty("Authorization", "Basic " + - Base64.getEncoder().encodeToString("test_admin:x-pack-test-password".getBytes(StandardCharsets.UTF_8))); - httpURLConnection.setRequestMethod("GET"); - httpURLConnection.setConnectTimeout(1000); - httpURLConnection.setReadTimeout(30000); // read needs to wait for nodes! - httpURLConnection.connect(); - if (httpURLConnection.getResponseCode() == 200) { - tmpFile.withWriter StandardCharsets.UTF_8.name(), { - it.write(httpURLConnection.getInputStream().getText(StandardCharsets.UTF_8.name())) - } - break; - } - } catch (Exception e) { - logger.debug("failed to call cluster health", e) - lastException = e - } finally { - if (httpURLConnection != null) { - httpURLConnection.disconnect(); - } + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(clientKeyStore.newInputStream(), 'keypass'.toCharArray()); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, 'keypass'.toCharArray()); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keyStore); + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom()); + for (int i = 0; i < 10; i++) { + // we use custom wait logic here for HTTPS + HttpsURLConnection httpURLConnection = null; + try { + httpURLConnection = (HttpsURLConnection) new URL("https://${node.httpUri()}/_cluster/health?wait_for_nodes=${numNodes}&wait_for_status=yellow").openConnection(); + httpURLConnection.setSSLSocketFactory(sslContext.getSocketFactory()); + httpURLConnection.setRequestProperty("Authorization", "Basic " + + Base64.getEncoder().encodeToString("test_admin:x-pack-test-password".getBytes(StandardCharsets.UTF_8))); + httpURLConnection.setRequestMethod("GET"); + httpURLConnection.connect(); + if (httpURLConnection.getResponseCode() == 200) { + tmpFile.withWriter StandardCharsets.UTF_8.name(), { + it.write(httpURLConnection.getInputStream().getText(StandardCharsets.UTF_8.name())) + } } + } catch (IOException e) { + if (i == 9) { + logger.error("final attempt of calling cluster health failed", e) + } else { + logger.debug("failed to call cluster health", e) + } + } finally { + if (httpURLConnection != null) { + httpURLConnection.disconnect(); + } + } - // did not start, so wait a bit before trying again - Thread.sleep(500L); - } - if (tmpFile.exists() == false && lastException != null) { - logger.error("final attempt of calling cluster health failed", lastException) + // did not start, so wait a bit before trying again + Thread.sleep(500L); } + return tmpFile.exists() } - setting 'xpack.security.transport.ssl.enabled', 'true' - setting 'xpack.ssl.keystore.path', 'testnode.jks' - keystoreSetting 'xpack.ssl.keystore.secure_password', 'testnode' - dependsOn copyTestNodeKeystore - extraConfigFile 'testnode.jks', new File(outputDir + '/testnode.jks') +} + + + + +/** A lazy evaluator to find the san to use for certificate generation. */ +class SanEvaluator { + + private static String san = null + + String toString() { + synchronized (SanEvaluator.class) { + if (san == null) { + san = getSubjectAlternativeNameString() + } + } + return san + } + + // Code stolen from NetworkUtils/InetAddresses/NetworkAddress to support SAN + /** Return all interfaces (and subinterfaces) on the system */ + private static List getInterfaces() throws SocketException { + List all = new ArrayList<>(); + addAllInterfaces(all, Collections.list(NetworkInterface.getNetworkInterfaces())); + Collections.sort(all, new Comparator() { + @Override + public int compare(NetworkInterface left, NetworkInterface right) { + return Integer.compare(left.getIndex(), right.getIndex()); + } + }); + return all; + } + + /** Helper for getInterfaces, recursively adds subinterfaces to {@code target} */ + private static void addAllInterfaces(List target, List level) { + if (!level.isEmpty()) { + target.addAll(level); + for (NetworkInterface intf : level) { + addAllInterfaces(target, Collections.list(intf.getSubInterfaces())); + } + } + } + + private static String getSubjectAlternativeNameString() { + List list = new ArrayList<>(); + for (NetworkInterface intf : getInterfaces()) { + if (intf.isUp()) { + // NOTE: some operating systems (e.g. BSD stack) assign a link local address to the loopback interface + // while technically not a loopback address, some of these treat them as one (e.g. OS X "localhost") so we must too, + // otherwise things just won't work out of box. So we include all addresses from loopback interfaces. + for (InetAddress address : Collections.list(intf.getInetAddresses())) { + if (intf.isLoopback() || address.isLoopbackAddress()) { + list.add(address); + } + } + } + } + if (list.isEmpty()) { + throw new IllegalArgumentException("no up-and-running loopback addresses found, got " + getInterfaces()); + } + + StringBuilder builder = new StringBuilder("san="); + for (int i = 0; i < list.size(); i++) { + InetAddress address = list.get(i); + String hostAddress; + if (address instanceof Inet6Address) { + hostAddress = compressedIPV6Address((Inet6Address)address); + } else { + hostAddress = address.getHostAddress(); + } + builder.append("ip:").append(hostAddress); + String hostname = address.getHostName(); + if (hostname.equals(address.getHostAddress()) == false) { + builder.append(",dns:").append(hostname); + } + + if (i != (list.size() - 1)) { + builder.append(","); + } + } + + return builder.toString(); + } + + private static String compressedIPV6Address(Inet6Address inet6Address) { + byte[] bytes = inet6Address.getAddress(); + int[] hextets = new int[8]; + for (int i = 0; i < hextets.length; i++) { + hextets[i] = (bytes[2 * i] & 255) << 8 | bytes[2 * i + 1] & 255; + } + compressLongestRunOfZeroes(hextets); + return hextetsToIPv6String(hextets); + } + + /** + * Identify and mark the longest run of zeroes in an IPv6 address. + * + *

Only runs of two or more hextets are considered. In case of a tie, the + * leftmost run wins. If a qualifying run is found, its hextets are replaced + * by the sentinel value -1. + * + * @param hextets {@code int[]} mutable array of eight 16-bit hextets + */ + private static void compressLongestRunOfZeroes(int[] hextets) { + int bestRunStart = -1; + int bestRunLength = -1; + int runStart = -1; + for (int i = 0; i < hextets.length + 1; i++) { + if (i < hextets.length && hextets[i] == 0) { + if (runStart < 0) { + runStart = i; + } + } else if (runStart >= 0) { + int runLength = i - runStart; + if (runLength > bestRunLength) { + bestRunStart = runStart; + bestRunLength = runLength; + } + runStart = -1; + } + } + if (bestRunLength >= 2) { + Arrays.fill(hextets, bestRunStart, bestRunStart + bestRunLength, -1); + } + } + + /** + * Convert a list of hextets into a human-readable IPv6 address. + * + *

In order for "::" compression to work, the input should contain negative + * sentinel values in place of the elided zeroes. + * + * @param hextets {@code int[]} array of eight 16-bit hextets, or -1s + */ + private static String hextetsToIPv6String(int[] hextets) { + /* + * While scanning the array, handle these state transitions: + * start->num => "num" start->gap => "::" + * num->num => ":num" num->gap => "::" + * gap->num => "num" gap->gap => "" + */ + StringBuilder buf = new StringBuilder(39); + boolean lastWasNumber = false; + for (int i = 0; i < hextets.length; i++) { + boolean thisIsNumber = hextets[i] >= 0; + if (thisIsNumber) { + if (lastWasNumber) { + buf.append(':'); + } + buf.append(Integer.toHexString(hextets[i])); + } else { + if (i == 0 || lastWasNumber) { + buf.append("::"); + } + } + lastWasNumber = thisIsNumber; + } + return buf.toString(); + } } diff --git a/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/CliIntegrationTestCase.java b/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/CliIntegrationTestCase.java index 241d3b6a76b..d890b5ac76d 100644 --- a/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/CliIntegrationTestCase.java +++ b/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/CliIntegrationTestCase.java @@ -17,6 +17,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.test.rest.ESRestTestCase; import org.elasticsearch.transport.client.PreBuiltTransportClient; +import org.elasticsearch.xpack.qa.sql.cli.RemoteCli.SecurityConfig; import org.elasticsearch.xpack.qa.sql.embed.CliHttpServer; import org.elasticsearch.xpack.qa.sql.rest.RestSqlTestCase; import org.junit.After; @@ -65,7 +66,7 @@ public abstract class CliIntegrationTestCase extends ESRestTestCase { */ @Before public void startCli() throws IOException { - cli = new RemoteCli(esUrlPrefix() + ES.get()); + cli = new RemoteCli(ES.get(), true, securityConfig()); } @After @@ -79,11 +80,10 @@ public abstract class CliIntegrationTestCase extends ESRestTestCase { } /** - * Prefix to the Elasticsearch URL. Override to add - * authentication support. + * Override to add security configuration to the cli. */ - protected String esUrlPrefix() { - return ""; + protected SecurityConfig securityConfig() { + return null; } protected void index(String index, CheckedConsumer body) throws IOException { diff --git a/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/RemoteCli.java b/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/RemoteCli.java index 35f8335116c..5b805933f5e 100644 --- a/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/RemoteCli.java +++ b/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/RemoteCli.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.qa.sql.cli; import org.apache.logging.log4j.Logger; import org.elasticsearch.SpecialPermission; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.logging.Loggers; import java.io.BufferedReader; @@ -23,7 +24,9 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.List; +import java.util.function.Predicate; +import static org.elasticsearch.test.ESTestCase.randomBoolean; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.not; @@ -57,7 +60,9 @@ public class RemoteCli implements Closeable { private final PrintWriter out; private final BufferedReader in; - public RemoteCli(String elasticsearchAddress) throws IOException { + public RemoteCli(String elasticsearchAddress, boolean checkConnectionOnStartup, + @Nullable SecurityConfig security) throws IOException { + // Connect SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(new SpecialPermission()); @@ -75,10 +80,39 @@ public class RemoteCli implements Closeable { }); logger.info("connected"); socket.setSoTimeout(10000); - out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true); - out.println(elasticsearchAddress); in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); + + // Start the CLI + String command; + if (security == null) { + command = elasticsearchAddress; + } else { + command = security.user + "@" + elasticsearchAddress; + if (security.https) { + command = "https://" + command; + } else if (randomBoolean()) { + command = "http://" + command; + } + if (security.keystoreLocation != null) { + command = command + " -keystore_location " + security.keystoreLocation; + } + } + if (false == checkConnectionOnStartup) { + command += " -check false"; + } + out.println(command); + + // Feed it passwords if needed + if (security != null && security.keystoreLocation != null) { + assertEquals("keystore password: ", readUntil(s -> s.endsWith(": "))); + out.println(security.keystorePassword); + } + if (security != null) { + assertEquals("password: ", readUntil(s -> s.endsWith(": "))); + out.println(security.password); + } + // Throw out the logo and warnings about making a dumb terminal while (false == readLine().contains("SQL")); // Throw out the empty line before all the good stuff @@ -136,4 +170,64 @@ public class RemoteCli implements Closeable { logger.info("in : {}", line); return line; } + + private String readUntil(Predicate end) throws IOException { + StringBuilder b = new StringBuilder(); + String result; + do { + int c = in.read(); + if (c == -1) { + throw new IOException("got eof before end"); + } + b.append((char) c); + result = b.toString(); + } while (false == end.test(result)); + logger.info("in : {}", result); + return result; + } + + public static class SecurityConfig { + private final boolean https; + private final String user; + private final String password; + @Nullable + private final String keystoreLocation; + @Nullable + private final String keystorePassword; + + public SecurityConfig(boolean https, String user, String password, + @Nullable String keystoreLocation, @Nullable String keystorePassword) { + if (user == null) { + throw new IllegalArgumentException( + "[user] is required. Send [null] instead of a SecurityConfig to run without security."); + } + if (password == null) { + throw new IllegalArgumentException( + "[password] is required. Send [null] instead of a SecurityConfig to run without security."); + } + if (keystoreLocation == null) { + if (keystorePassword != null) { + throw new IllegalArgumentException("[keystorePassword] cannot be specified if [keystoreLocation] is not specified"); + } + } else { + if (keystorePassword == null) { + throw new IllegalArgumentException("[keystorePassword] is required if [keystoreLocation] is specified"); + } + } + + this.https = https; + this.user = user; + this.password = password; + this.keystoreLocation = keystoreLocation; + this.keystorePassword = keystorePassword; + } + + public String keystoreLocation() { + return keystoreLocation; + } + + public String keystorePassword() { + return keystorePassword; + } + } } diff --git a/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/JdbcIntegrationTestCase.java b/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/JdbcIntegrationTestCase.java index 01d1efd7850..ee3192c9f2f 100644 --- a/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/JdbcIntegrationTestCase.java +++ b/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/JdbcIntegrationTestCase.java @@ -70,7 +70,8 @@ public abstract class JdbcIntegrationTestCase extends ESRestTestCase { // JDBC only supports a single node at a time so we just give it one. return cluster.split(",")[0]; /* This doesn't include "jdbc:es://" because we want the example in - * esJdbc to be obvious. */ + * esJdbc to be obvious and because we want to use getProtocol to add + * https if we are running against https. */ } public Connection esJdbc() throws SQLException { @@ -81,8 +82,9 @@ public abstract class JdbcIntegrationTestCase extends ESRestTestCase { } protected Connection useDriverManager() throws SQLException { + String elasticsearchAddress = getProtocol() + "://" + elasticsearchAddress(); // tag::connect-dm - String address = "jdbc:es://" + elasticsearchAddress(); // <1> + String address = "jdbc:es://" + elasticsearchAddress; // <1> Properties connectionProperties = connectionProperties(); // <2> Connection connection = DriverManager.getConnection(address, connectionProperties); // end::connect-dm @@ -91,9 +93,10 @@ public abstract class JdbcIntegrationTestCase extends ESRestTestCase { } protected Connection useDataSource() throws SQLException { + String elasticsearchAddress = getProtocol() + "://" + elasticsearchAddress(); // tag::connect-ds JdbcDataSource dataSource = new JdbcDataSource(); - String address = "jdbc:es://" + elasticsearchAddress(); // <1> + String address = "jdbc:es://" + elasticsearchAddress; // <1> dataSource.setUrl(address); Properties connectionProperties = connectionProperties(); // <2> dataSource.setProperties(connectionProperties); @@ -140,4 +143,4 @@ public abstract class JdbcIntegrationTestCase extends ESRestTestCase { Collections.sort(ids); return randomFrom(ids); } -} \ No newline at end of file +} diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/Cli.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/Cli.java index 292312e7816..3ac065f53d9 100644 --- a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/Cli.java +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/Cli.java @@ -26,12 +26,16 @@ import org.elasticsearch.xpack.sql.client.shared.Version; import java.io.IOException; import java.net.ConnectException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.List; import java.util.logging.LogManager; public class Cli extends Command { private final OptionSpec debugOption; + private final OptionSpec keystoreLocation; private final OptionSpec checkOption; private final OptionSpec connectionString; @@ -41,6 +45,12 @@ public class Cli extends Command { "Enable debug logging") .withRequiredArg().ofType(Boolean.class) .defaultsTo(Boolean.parseBoolean(System.getProperty("cli.debug", "false"))); + this.keystoreLocation = parser.acceptsAll( + Arrays.asList("k", "keystore_location"), + "Location of a keystore to use when setting up SSL. " + + "If specified then the CLI will prompt for a keystore password. " + + "If specified when the uri isn't https then an error is thrown.") + .withRequiredArg().ofType(String.class); this.checkOption = parser.acceptsAll(Arrays.asList("c", "check"), "Enable initial connection check on startup") .withRequiredArg().ofType(Boolean.class) @@ -77,15 +87,21 @@ public class Cli extends Command { @Override protected void execute(org.elasticsearch.cli.Terminal terminal, OptionSet options) throws Exception { boolean debug = debugOption.value(options); - boolean check = checkOption.value(options); + boolean checkConnection = checkOption.value(options); List args = connectionString.values(options); if (args.size() > 1) { throw new UserException(ExitCodes.USAGE, "expecting a single uri"); } - execute(args.size() == 1 ? args.get(0) : null, debug, check); + String uri = args.size() == 1 ? args.get(0) : null; + args = keystoreLocation.values(options); + if (args.size() > 1) { + throw new UserException(ExitCodes.USAGE, "expecting a single keystore file"); + } + String keystoreLocationValue = args.size() == 1 ? args.get(0) : null; + execute(uri, debug, keystoreLocationValue, checkConnection); } - private void execute(String uri, boolean debug, boolean check) throws Exception { + private void execute(String uri, boolean debug, String keystoreLocation, boolean checkConnection) throws Exception { CliCommand cliCommand = new CliCommands( new PrintLogoCommand(), new ClearScreenCliCommand(), @@ -96,10 +112,10 @@ public class Cli extends Command { ); try (CliTerminal cliTerminal = new JLineTerminal()) { ConnectionBuilder connectionBuilder = new ConnectionBuilder(cliTerminal); - ConnectionConfiguration con = connectionBuilder.buildConnection(uri); + ConnectionConfiguration con = connectionBuilder.buildConnection(uri, keystoreLocation); CliSession cliSession = new CliSession(new CliHttpClient(con)); cliSession.setDebug(debug); - if (check) { + if (checkConnection) { checkConnection(cliSession, cliTerminal, con); } new CliRepl(cliTerminal, cliSession, cliCommand).execute(); diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/ConnectionBuilder.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/ConnectionBuilder.java index 2f1df4cd9ea..8f209e6d98a 100644 --- a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/ConnectionBuilder.java +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/ConnectionBuilder.java @@ -5,10 +5,14 @@ */ package org.elasticsearch.xpack.sql.cli; +import org.elasticsearch.cli.ExitCodes; import org.elasticsearch.cli.UserException; import org.elasticsearch.xpack.sql.client.shared.ConnectionConfiguration; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Properties; import static org.elasticsearch.xpack.sql.client.shared.UriUtils.parseURI; @@ -27,14 +31,19 @@ public class ConnectionBuilder { this.cliTerminal = cliTerminal; } - public ConnectionConfiguration buildConnection(String arg) throws UserException { + /** + * Build the connection. + * @param connectionStringArg the connection string to connect to + * @param keystoreLocation the location of the keystore to configure. If null then use the system keystore. + */ + public ConnectionConfiguration buildConnection(String connectionStringArg, String keystoreLocation) throws UserException { final URI uri; final String connectionString; Properties properties = new Properties(); String user = null; String password = null; - if (arg != null) { - connectionString = arg; + if (connectionStringArg != null) { + connectionString = connectionStringArg; uri = removeQuery(parseURI(connectionString, DEFAULT_URI), connectionString, DEFAULT_URI); user = uri.getUserInfo(); if (user != null) { @@ -49,6 +58,29 @@ public class ConnectionBuilder { connectionString = DEFAULT_CONNECTION_STRING; } + if (keystoreLocation != null) { + if (false == "https".equals(uri.getScheme())) { + throw new UserException(ExitCodes.USAGE, "keystore file specified without https"); + } + Path p = Paths.get(keystoreLocation); + checkIfExists("keystore file", p); + String keystorePassword = cliTerminal.readPassword("keystore password: "); + + /* + * Set both the keystore and truststore settings which is required + * to everything work smoothly. I'm not totally sure why we have + * two settings but that is a problem for another day. + */ + properties.put("ssl.keystore.location", keystoreLocation); + properties.put("ssl.keystore.pass", keystorePassword); + properties.put("ssl.truststore.location", keystoreLocation); + properties.put("ssl.truststore.pass", keystorePassword); + } + + if ("https".equals(uri.getScheme())) { + properties.put("ssl", "true"); + } + if (user != null) { if (password == null) { password = cliTerminal.readPassword("password: "); @@ -57,7 +89,20 @@ public class ConnectionBuilder { properties.setProperty(ConnectionConfiguration.AUTH_PASS, password); } + return newConnectionConfiguration(uri, connectionString, properties); + } + + protected ConnectionConfiguration newConnectionConfiguration(URI uri, String connectionString, Properties properties) { return new ConnectionConfiguration(uri, connectionString, properties); } + protected void checkIfExists(String name, Path p) throws UserException { + if (false == Files.exists(p)) { + throw new UserException(ExitCodes.USAGE, name + " [" + p + "] doesn't exist"); + } + if (false == Files.isRegularFile(p)) { + throw new UserException(ExitCodes.USAGE, name + " [" + p + "] isn't a regular file"); + } + } + } diff --git a/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/ConnectionBuilderTests.java b/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/ConnectionBuilderTests.java index aadf597d07f..d56c821d63a 100644 --- a/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/ConnectionBuilderTests.java +++ b/sql/cli/src/test/java/org/elasticsearch/xpack/sql/cli/ConnectionBuilderTests.java @@ -5,10 +5,14 @@ */ package org.elasticsearch.xpack.sql.cli; +import org.elasticsearch.cli.UserException; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.sql.client.shared.ConnectionConfiguration; - +import org.elasticsearch.xpack.sql.client.shared.SslConfig; import java.net.URI; +import java.nio.file.Path; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicBoolean; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; @@ -22,7 +26,7 @@ public class ConnectionBuilderTests extends ESTestCase { public void testDefaultConnection() throws Exception { CliTerminal testTerminal = mock(CliTerminal.class); ConnectionBuilder connectionBuilder = new ConnectionBuilder(testTerminal); - ConnectionConfiguration con = connectionBuilder.buildConnection(null); + ConnectionConfiguration con = connectionBuilder.buildConnection(null, null); assertNull(con.authUser()); assertNull(con.authPass()); assertEquals("http://localhost:9200/", con.connectionString()); @@ -38,7 +42,7 @@ public class ConnectionBuilderTests extends ESTestCase { public void testBasicConnection() throws Exception { CliTerminal testTerminal = mock(CliTerminal.class); ConnectionBuilder connectionBuilder = new ConnectionBuilder(testTerminal); - ConnectionConfiguration con = connectionBuilder.buildConnection("http://foobar:9242/"); + ConnectionConfiguration con = connectionBuilder.buildConnection("http://foobar:9242/", null); assertNull(con.authUser()); assertNull(con.authPass()); assertEquals("http://foobar:9242/", con.connectionString()); @@ -49,7 +53,7 @@ public class ConnectionBuilderTests extends ESTestCase { public void testUserAndPasswordConnection() throws Exception { CliTerminal testTerminal = mock(CliTerminal.class); ConnectionBuilder connectionBuilder = new ConnectionBuilder(testTerminal); - ConnectionConfiguration con = connectionBuilder.buildConnection("http://user:pass@foobar:9242/"); + ConnectionConfiguration con = connectionBuilder.buildConnection("http://user:pass@foobar:9242/", null); assertEquals("user", con.authUser()); assertEquals("pass", con.authPass()); assertEquals("http://user:pass@foobar:9242/", con.connectionString()); @@ -61,7 +65,7 @@ public class ConnectionBuilderTests extends ESTestCase { CliTerminal testTerminal = mock(CliTerminal.class); when(testTerminal.readPassword("password: ")).thenReturn("password"); ConnectionBuilder connectionBuilder = new ConnectionBuilder(testTerminal); - ConnectionConfiguration con = connectionBuilder.buildConnection("http://user@foobar:9242/"); + ConnectionConfiguration con = connectionBuilder.buildConnection("http://user@foobar:9242/", null); assertEquals("user", con.authUser()); assertEquals("password", con.authPass()); assertEquals("http://user@foobar:9242/", con.connectionString()); @@ -69,4 +73,37 @@ public class ConnectionBuilderTests extends ESTestCase { verify(testTerminal, times(1)).readPassword(any()); verifyNoMoreInteractions(testTerminal); } + + public void testKeystoreAndUserInteractiveConnection() throws Exception { + CliTerminal testTerminal = mock(CliTerminal.class); + when(testTerminal.readPassword("keystore password: ")).thenReturn("keystore password"); + when(testTerminal.readPassword("password: ")).thenReturn("password"); + AtomicBoolean called = new AtomicBoolean(false); + ConnectionBuilder connectionBuilder = new ConnectionBuilder(testTerminal) { + @Override + protected void checkIfExists(String name, Path p) { + // Stubbed so we don't need permission to read the file + } + + @Override + protected ConnectionConfiguration newConnectionConfiguration(URI uri, String connectionString, + Properties properties) { + // Stub building the actual configuration because we don't have permission to read the keystore. + assertEquals("true", properties.get(SslConfig.SSL)); + assertEquals("keystore_location", properties.get(SslConfig.SSL_KEYSTORE_LOCATION)); + assertEquals("keystore password", properties.get(SslConfig.SSL_KEYSTORE_PASS)); + assertEquals("keystore_location", properties.get(SslConfig.SSL_TRUSTSTORE_LOCATION)); + assertEquals("keystore password", properties.get(SslConfig.SSL_TRUSTSTORE_PASS)); + + called.set(true); + return null; + } + }; + assertNull(connectionBuilder.buildConnection("https://user@foobar:9242/", "keystore_location")); + assertTrue(called.get()); + verify(testTerminal, times(2)).readPassword(any()); + verifyNoMoreInteractions(testTerminal); + } + + } diff --git a/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcConfiguration.java b/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcConfiguration.java index bb19c95bca0..60a6127294a 100644 --- a/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcConfiguration.java +++ b/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcConfiguration.java @@ -30,7 +30,7 @@ import static org.elasticsearch.xpack.sql.client.shared.UriUtils.removeQuery; // jdbc:es://[host|ip] // jdbc:es://[host|ip]:port/(prefix) // jdbc:es://[host|ip]:port/(prefix)(?options=value&) -// +// // Additional properties can be specified either through the Properties object or in the URL. In case of duplicates, the URL wins. // @@ -50,7 +50,7 @@ public class JdbcConfiguration extends ConnectionConfiguration { public static final String TIME_ZONE = "timezone"; // follow the JDBC spec and use the JVM default... // to avoid inconsistency, the default is picked up once at startup and reused across connections - // to cater to the principle of least surprise + // to cater to the principle of least surprise // really, the way to move forward is to specify a calendar or the timezone manually static final String TIME_ZONE_DEFAULT = TimeZone.getDefault().getID(); @@ -94,7 +94,7 @@ public class JdbcConfiguration extends ConnectionConfiguration { private static URI parseUrl(String u) throws JdbcSQLException { String url = u; - String format = "jdbc:es://[host[:port]]*/[prefix]*[?[option=value]&]*"; + String format = "jdbc:es://[http|https]?[host[:port]]*/[prefix]*[?[option=value]&]*"; if (!canAccept(u)) { throw new JdbcSQLException("Expected [" + URL_PREFIX + "] url, received [" + u + "]"); } @@ -108,7 +108,7 @@ public class JdbcConfiguration extends ConnectionConfiguration { private static String removeJdbcPrefix(String connectionString) throws JdbcSQLException { if (connectionString.startsWith(URL_PREFIX)) { - return "http://" + connectionString.substring(URL_PREFIX.length()); + return connectionString.substring(URL_PREFIX.length()); } else { throw new JdbcSQLException("Expected [" + URL_PREFIX + "] url, received [" + connectionString + "]"); } @@ -183,7 +183,7 @@ public class JdbcConfiguration extends ConnectionConfiguration { DriverPropertyInfo prop = new DriverPropertyInfo(option, value); info.add(prop); } - + return info.toArray(new DriverPropertyInfo[info.size()]); } -} \ No newline at end of file +} diff --git a/sql/shared-client/src/main/java/org/elasticsearch/xpack/sql/client/shared/SslConfig.java b/sql/shared-client/src/main/java/org/elasticsearch/xpack/sql/client/shared/SslConfig.java index 2c45a0694ee..fb1b0dd7fd3 100644 --- a/sql/shared-client/src/main/java/org/elasticsearch/xpack/sql/client/shared/SslConfig.java +++ b/sql/shared-client/src/main/java/org/elasticsearch/xpack/sql/client/shared/SslConfig.java @@ -26,30 +26,30 @@ import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; -class SslConfig { +public class SslConfig { - private static final String SSL = "ssl"; + public static final String SSL = "ssl"; private static final String SSL_DEFAULT = "false"; - private static final String SSL_PROTOCOL = "ssl.protocol"; + public static final String SSL_PROTOCOL = "ssl.protocol"; private static final String SSL_PROTOCOL_DEFAULT = "TLS"; // SSL alternative - private static final String SSL_KEYSTORE_LOCATION = "ssl.keystore.location"; + public static final String SSL_KEYSTORE_LOCATION = "ssl.keystore.location"; private static final String SSL_KEYSTORE_LOCATION_DEFAULT = ""; - private static final String SSL_KEYSTORE_PASS = "ssl.keystore.pass"; + public static final String SSL_KEYSTORE_PASS = "ssl.keystore.pass"; private static final String SSL_KEYSTORE_PASS_DEFAULT = ""; - private static final String SSL_KEYSTORE_TYPE = "ssl.keystore.type"; + public static final String SSL_KEYSTORE_TYPE = "ssl.keystore.type"; private static final String SSL_KEYSTORE_TYPE_DEFAULT = "JKS"; // PCKS12 - private static final String SSL_TRUSTSTORE_LOCATION = "ssl.truststore.location"; + public static final String SSL_TRUSTSTORE_LOCATION = "ssl.truststore.location"; private static final String SSL_TRUSTSTORE_LOCATION_DEFAULT = ""; - private static final String SSL_TRUSTSTORE_PASS = "ssl.truststore.pass"; + public static final String SSL_TRUSTSTORE_PASS = "ssl.truststore.pass"; private static final String SSL_TRUSTSTORE_PASS_DEFAULT = ""; - private static final String SSL_TRUSTSTORE_TYPE = "ssl.truststore.type"; + public static final String SSL_TRUSTSTORE_TYPE = "ssl.truststore.type"; private static final String SSL_TRUSTSTORE_TYPE_DEFAULT = "JKS"; static final Set OPTION_NAMES = new LinkedHashSet<>(Arrays.asList(SSL, SSL_PROTOCOL, @@ -63,7 +63,6 @@ class SslConfig { private final SSLContext sslContext; SslConfig(Properties settings) { - // ssl enabled = StringUtils.parseBoolean(settings.getProperty(SSL, SSL_DEFAULT)); protocol = settings.getProperty(SSL_PROTOCOL, SSL_PROTOCOL_DEFAULT); keystoreLocation = settings.getProperty(SSL_KEYSTORE_LOCATION, SSL_KEYSTORE_LOCATION_DEFAULT); diff --git a/sql/shared-client/src/main/java/org/elasticsearch/xpack/sql/client/shared/UriUtils.java b/sql/shared-client/src/main/java/org/elasticsearch/xpack/sql/client/shared/UriUtils.java index 47521781433..f8c2e73e6a0 100644 --- a/sql/shared-client/src/main/java/org/elasticsearch/xpack/sql/client/shared/UriUtils.java +++ b/sql/shared-client/src/main/java/org/elasticsearch/xpack/sql/client/shared/UriUtils.java @@ -33,12 +33,14 @@ public final class UriUtils { // check if URI can be parsed correctly without adding scheme // if the connection string is in format host:port or just host, the host is going to be null // if the connection string contains IPv6 localhost [::1] the parsing will fail + URISyntaxException firstException = null; try { uri = new URI(connectionString); if (uri.getHost() == null || uri.getScheme() == null) { uri = null; } } catch (URISyntaxException e) { + firstException = e; uri = null; } @@ -47,7 +49,12 @@ public final class UriUtils { try { return new URI("http://" + connectionString); } catch (URISyntaxException e) { - throw new IllegalArgumentException("Invalid connection configuration [" + connectionString + "]: " + e.getMessage(), e); + IllegalArgumentException ie = + new IllegalArgumentException("Invalid connection configuration [" + connectionString + "]: " + e.getMessage(), e); + if (firstException != null) { + ie.addSuppressed(firstException); + } + throw ie; } } else { // We managed to parse URI and all necessary pieces are present, let's make sure the scheme is correct @@ -70,4 +77,3 @@ public final class UriUtils { } } } - diff --git a/sql/shared-client/src/test/resources/plugin-security.policy b/sql/shared-client/src/test/resources/plugin-security.policy deleted file mode 100644 index 82751bf76de..00000000000 --- a/sql/shared-client/src/test/resources/plugin-security.policy +++ /dev/null @@ -1,6 +0,0 @@ -grant { - // Required for the net client to setup ssl rather than use global ssl. - permission java.lang.RuntimePermission "setFactory"; - // Required to connect to the test ssl server. - permission java.net.SocketPermission "*", "connect,resolve"; -}; diff --git a/test/sql-cli-fixture/src/main/java/org/elasticsearch/xpack/sql/cli/fixture/CliFixture.java b/test/sql-cli-fixture/src/main/java/org/elasticsearch/xpack/sql/cli/fixture/CliFixture.java index 85a5f3d7b8f..f080f28ba0e 100644 --- a/test/sql-cli-fixture/src/main/java/org/elasticsearch/xpack/sql/cli/fixture/CliFixture.java +++ b/test/sql-cli-fixture/src/main/java/org/elasticsearch/xpack/sql/cli/fixture/CliFixture.java @@ -83,8 +83,8 @@ public class CliFixture { try { println("accepting on localhost:" + server.getLocalPort()); Socket s = server.accept(); - String url = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.UTF_8)).readLine(); - if (url == null || url.isEmpty()) { + String line = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.UTF_8)).readLine(); + if (line == null || line.isEmpty()) { continue; } List command = new ArrayList<>(); @@ -100,7 +100,7 @@ public class CliFixture { command.add("-Dorg.jline.terminal.dumb=true"); command.add("-jar"); command.add(cliJar.toString()); - command.addAll(Arrays.asList(url.split(" "))); + command.addAll(Arrays.asList(line.split(" "))); ProcessBuilder cliBuilder = new ProcessBuilder(command); cliBuilder.redirectErrorStream(true); Process process = cliBuilder.start();