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@97fac4a3b4
This commit is contained in:
Nik Everett 2017-12-11 15:45:34 -05:00 committed by GitHub
parent 4bebc307c3
commit 236f64a70e
31 changed files with 790 additions and 131 deletions

View File

@ -1,3 +1,7 @@
integTestRunner {
systemProperty 'tests.ssl.enabled', 'false'
}
integTestCluster { integTestCluster {
waitCondition = { node, ant -> waitCondition = { node, ant ->
File tmpFile = new File(node.cwd, 'wait.success') File tmpFile = new File(node.cwd, 'wait.success')

View File

@ -7,6 +7,7 @@ package org.elasticsearch.xpack.qa.sql.security;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.xpack.qa.sql.cli.ErrorsTestCase; import org.elasticsearch.xpack.qa.sql.cli.ErrorsTestCase;
import org.elasticsearch.xpack.qa.sql.cli.RemoteCli.SecurityConfig;
public class CliErrorsIT extends ErrorsTestCase { public class CliErrorsIT extends ErrorsTestCase {
@Override @Override
@ -15,7 +16,12 @@ public class CliErrorsIT extends ErrorsTestCase {
} }
@Override @Override
protected String esUrlPrefix() { protected String getProtocol() {
return CliSecurityIT.adminEsUrlPrefix(); return RestSqlIT.SSL_ENABLED ? "https" : "http";
}
@Override
protected SecurityConfig securityConfig() {
return CliSecurityIT.adminSecurityConfig();
} }
} }

View File

@ -7,6 +7,7 @@ package org.elasticsearch.xpack.qa.sql.security;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.xpack.qa.sql.cli.FetchSizeTestCase; import org.elasticsearch.xpack.qa.sql.cli.FetchSizeTestCase;
import org.elasticsearch.xpack.qa.sql.cli.RemoteCli.SecurityConfig;
public class CliFetchSizeIT extends FetchSizeTestCase { public class CliFetchSizeIT extends FetchSizeTestCase {
@Override @Override
@ -15,7 +16,12 @@ public class CliFetchSizeIT extends FetchSizeTestCase {
} }
@Override @Override
protected String esUrlPrefix() { protected String getProtocol() {
return CliSecurityIT.adminEsUrlPrefix(); return RestSqlIT.SSL_ENABLED ? "https" : "http";
}
@Override
protected SecurityConfig securityConfig() {
return CliSecurityIT.adminSecurityConfig();
} }
} }

View File

@ -6,8 +6,12 @@
package org.elasticsearch.xpack.qa.sql.security; package org.elasticsearch.xpack.qa.sql.security;
import org.elasticsearch.common.CheckedConsumer; 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;
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.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@ -19,18 +23,43 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.startsWith;
public class CliSecurityIT extends SqlSecurityTestCase { public class CliSecurityIT extends SqlSecurityTestCase {
static final String NO_INIT_CONNECTION_CHECK_PREFIX = "-c false "; static SecurityConfig adminSecurityConfig() {
static String adminEsUrlPrefix() { String keystoreLocation;
return "test_admin:x-pack-test-password@"; 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. * Perform security test actions using the CLI.
*/ */
private static class CliActions implements Actions { 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 @Override
public void queryWorksAsAdmin() throws Exception { 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")); assertThat(cli.command("SELECT * FROM test ORDER BY a"), containsString("a | b | c"));
assertEquals("---------------+---------------+---------------", cli.readLine()); assertEquals("---------------+---------------+---------------", cli.readLine());
assertThat(cli.readLine(), containsString("1 |2 |3")); 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, public void expectMatchesAdmin(String adminSql, String user, String userSql,
CheckedConsumer<RemoteCli, Exception> customizer) throws Exception { CheckedConsumer<RemoteCli, Exception> customizer) throws Exception {
List<String> adminResult = new ArrayList<>(); List<String> adminResult = new ArrayList<>();
try (RemoteCli cli = new RemoteCli(adminEsUrlPrefix() + elasticsearchAddress())) { try (RemoteCli cli = new RemoteCli(elasticsearchAddress(), true, adminSecurityConfig())) {
customizer.accept(cli); customizer.accept(cli);
adminResult.add(cli.command(adminSql)); adminResult.add(cli.command(adminSql));
String line; String line;
@ -68,7 +97,7 @@ public class CliSecurityIT extends SqlSecurityTestCase {
} }
Iterator<String> expected = adminResult.iterator(); Iterator<String> expected = adminResult.iterator();
try (RemoteCli cli = new RemoteCli(userPrefix(user) + elasticsearchAddress())) { try (RemoteCli cli = new RemoteCli(elasticsearchAddress(), true, userSecurity(user))) {
customizer.accept(cli); customizer.accept(cli);
assertTrue(expected.hasNext()); assertTrue(expected.hasNext());
assertEquals(expected.next(), cli.command(userSql)); assertEquals(expected.next(), cli.command(userSql));
@ -86,7 +115,7 @@ public class CliSecurityIT extends SqlSecurityTestCase {
@Override @Override
public void expectDescribe(Map<String, String> columns, String user) throws Exception { public void expectDescribe(Map<String, String> 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")); assertThat(cli.command("DESCRIBE test"), containsString("column | type"));
assertEquals("---------------+---------------", cli.readLine()); assertEquals("---------------+---------------", cli.readLine());
for (Map.Entry<String, String> column : columns.entrySet()) { for (Map.Entry<String, String> column : columns.entrySet()) {
@ -98,7 +127,7 @@ public class CliSecurityIT extends SqlSecurityTestCase {
@Override @Override
public void expectShowTables(List<String> tables, String user) throws Exception { public void expectShowTables(List<String> 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")); assertThat(cli.command("SHOW TABLES"), containsString("table"));
assertEquals("---------------", cli.readLine()); assertEquals("---------------", cli.readLine());
for (String table : tables) { for (String table : tables) {
@ -110,7 +139,7 @@ public class CliSecurityIT extends SqlSecurityTestCase {
@Override @Override
public void expectUnknownIndex(String user, String sql) throws Exception { 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.command(sql), containsString("Bad request"));
assertThat(cli.readLine(), containsString("Unknown index")); assertThat(cli.readLine(), containsString("Unknown index"));
} }
@ -118,26 +147,22 @@ public class CliSecurityIT extends SqlSecurityTestCase {
@Override @Override
public void expectForbidden(String user, String sql) throws Exception { 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 + "]")); assertThat(cli.command(sql), containsString("is unauthorized for user [" + user + "]"));
} }
} }
@Override @Override
public void expectUnknownColumn(String user, String sql, String column) throws Exception { 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.command(sql), containsString("[1;31mBad request"));
assertThat(cli.readLine(), containsString("Unknown column [" + column + "][1;23;31m][0m")); 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() { public CliSecurityIT() {

View File

@ -6,6 +6,7 @@
package org.elasticsearch.xpack.qa.sql.security; package org.elasticsearch.xpack.qa.sql.security;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.xpack.qa.sql.cli.RemoteCli.SecurityConfig;
import org.elasticsearch.xpack.qa.sql.cli.SelectTestCase; import org.elasticsearch.xpack.qa.sql.cli.SelectTestCase;
public class CliSelectIT extends SelectTestCase { public class CliSelectIT extends SelectTestCase {
@ -15,7 +16,12 @@ public class CliSelectIT extends SelectTestCase {
} }
@Override @Override
protected String esUrlPrefix() { protected String getProtocol() {
return CliSecurityIT.adminEsUrlPrefix(); return RestSqlIT.SSL_ENABLED ? "https" : "http";
}
@Override
protected SecurityConfig securityConfig() {
return CliSecurityIT.adminSecurityConfig();
} }
} }

View File

@ -6,6 +6,7 @@
package org.elasticsearch.xpack.qa.sql.security; package org.elasticsearch.xpack.qa.sql.security;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.xpack.qa.sql.cli.RemoteCli.SecurityConfig;
import org.elasticsearch.xpack.qa.sql.cli.ShowTestCase; import org.elasticsearch.xpack.qa.sql.cli.ShowTestCase;
public class CliShowIT extends ShowTestCase { public class CliShowIT extends ShowTestCase {
@ -15,7 +16,12 @@ public class CliShowIT extends ShowTestCase {
} }
@Override @Override
protected String esUrlPrefix() { protected String getProtocol() {
return CliSecurityIT.adminEsUrlPrefix(); return RestSqlIT.SSL_ENABLED ? "https" : "http";
}
@Override
protected SecurityConfig securityConfig() {
return CliSecurityIT.adminSecurityConfig();
} }
} }

View File

@ -16,6 +16,11 @@ public class JdbcConnectionIT extends ConnectionTestCase {
return RestSqlIT.securitySettings(); return RestSqlIT.securitySettings();
} }
@Override
protected String getProtocol() {
return RestSqlIT.SSL_ENABLED ? "https" : "http";
}
@Override @Override
protected Properties connectionProperties() { protected Properties connectionProperties() {
Properties properties = super.connectionProperties(); Properties properties = super.connectionProperties();

View File

@ -20,6 +20,11 @@ public class JdbcCsvSpecIT extends CsvSpecTestCase {
return RestSqlIT.securitySettings(); return RestSqlIT.securitySettings();
} }
@Override
protected String getProtocol() {
return RestSqlIT.SSL_ENABLED ? "https" : "http";
}
@Override @Override
protected Properties connectionProperties() { protected Properties connectionProperties() {
Properties sp = super.connectionProperties(); Properties sp = super.connectionProperties();

View File

@ -16,6 +16,11 @@ public class JdbcDatabaseMetaDataIT extends DatabaseMetaDataTestCase {
return RestSqlIT.securitySettings(); return RestSqlIT.securitySettings();
} }
@Override
protected String getProtocol() {
return RestSqlIT.SSL_ENABLED ? "https" : "http";
}
@Override @Override
protected Properties connectionProperties() { protected Properties connectionProperties() {
Properties properties = super.connectionProperties(); Properties properties = super.connectionProperties();

View File

@ -16,6 +16,11 @@ public class JdbcErrorsIT extends ErrorsTestCase {
return RestSqlIT.securitySettings(); return RestSqlIT.securitySettings();
} }
@Override
protected String getProtocol() {
return RestSqlIT.SSL_ENABLED ? "https" : "http";
}
@Override @Override
protected Properties connectionProperties() { protected Properties connectionProperties() {
Properties properties = super.connectionProperties(); Properties properties = super.connectionProperties();

View File

@ -16,6 +16,11 @@ public class JdbcFetchSizeIT extends FetchSizeTestCase {
return RestSqlIT.securitySettings(); return RestSqlIT.securitySettings();
} }
@Override
protected String getProtocol() {
return RestSqlIT.SSL_ENABLED ? "https" : "http";
}
@Override @Override
protected Properties connectionProperties() { protected Properties connectionProperties() {
Properties properties = super.connectionProperties(); Properties properties = super.connectionProperties();

View File

@ -8,7 +8,11 @@ package org.elasticsearch.xpack.qa.sql.security;
import org.elasticsearch.action.admin.indices.get.GetIndexAction; import org.elasticsearch.action.admin.indices.get.GetIndexAction;
import org.elasticsearch.common.CheckedConsumer; import org.elasticsearch.common.CheckedConsumer;
import org.elasticsearch.common.CheckedFunction; import org.elasticsearch.common.CheckedFunction;
import org.elasticsearch.common.io.PathUtils;
import org.elasticsearch.xpack.qa.sql.jdbc.LocalH2; 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.Connection;
import java.sql.DriverManager; import java.sql.DriverManager;
import java.sql.ResultSet; 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.JdbcAssert.assertResultSets;
import static org.elasticsearch.xpack.qa.sql.jdbc.JdbcIntegrationTestCase.elasticsearchAddress; 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.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.contains;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
@ -31,6 +36,7 @@ public class JdbcSecurityIT extends SqlSecurityTestCase {
properties.put("user", "test_admin"); properties.put("user", "test_admin");
properties.put("password", "x-pack-test-password"); properties.put("password", "x-pack-test-password");
// end::admin_properties // end::admin_properties
addSslPropertiesIfNeeded(properties);
return properties; return properties;
} }
@ -38,7 +44,8 @@ public class JdbcSecurityIT extends SqlSecurityTestCase {
Properties props = new Properties(); Properties props = new Properties();
props.put("timezone", randomKnownTimeZone()); props.put("timezone", randomKnownTimeZone());
props.putAll(properties); 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) { static Properties userProperties(String user) {
@ -48,9 +55,32 @@ public class JdbcSecurityIT extends SqlSecurityTestCase {
Properties prop = new Properties(); Properties prop = new Properties();
prop.put("user", user); prop.put("user", user);
prop.put("password", "testpass"); prop.put("password", "testpass");
addSslPropertiesIfNeeded(prop);
return 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<Connection, ResultSet, SQLException> adminAction, static void expectActionMatchesAdmin(CheckedFunction<Connection, ResultSet, SQLException> adminAction,
String user, CheckedFunction<Connection, ResultSet, SQLException> userAction) throws Exception { String user, CheckedFunction<Connection, ResultSet, SQLException> userAction) throws Exception {
try (Connection adminConnection = es(adminProperties()); try (Connection adminConnection = es(adminProperties());

View File

@ -16,6 +16,11 @@ public class JdbcShowTablesIT extends ShowTablesTestCase {
return RestSqlIT.securitySettings(); return RestSqlIT.securitySettings();
} }
@Override
protected String getProtocol() {
return RestSqlIT.SSL_ENABLED ? "https" : "http";
}
@Override @Override
protected Properties connectionProperties() { protected Properties connectionProperties() {
Properties sp = super.connectionProperties(); Properties sp = super.connectionProperties();

View File

@ -16,6 +16,11 @@ public class JdbcSimpleExampleIT extends SimpleExampleTestCase {
return RestSqlIT.securitySettings(); return RestSqlIT.securitySettings();
} }
@Override
protected String getProtocol() {
return RestSqlIT.SSL_ENABLED ? "https" : "http";
}
@Override @Override
protected Properties connectionProperties() { protected Properties connectionProperties() {
Properties properties = super.connectionProperties(); Properties properties = super.connectionProperties();

View File

@ -20,6 +20,11 @@ public class JdbcSqlSpecIT extends SqlSpecTestCase {
return RestSqlIT.securitySettings(); return RestSqlIT.securitySettings();
} }
@Override
protected String getProtocol() {
return RestSqlIT.SSL_ENABLED ? "https" : "http";
}
@Override @Override
protected Properties connectionProperties() { protected Properties connectionProperties() {
Properties sp = super.connectionProperties(); Properties sp = super.connectionProperties();

View File

@ -5,27 +5,54 @@
*/ */
package org.elasticsearch.xpack.qa.sql.security; 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.SecureString;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.test.rest.ESRestTestCase;
import org.elasticsearch.xpack.qa.sql.rest.RestSqlTestCase; import org.elasticsearch.xpack.qa.sql.rest.RestSqlTestCase;
import static org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; 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 * Integration test for the rest sql action. The one that speaks json directly to a
* user rather than to the JDBC driver or CLI. * user rather than to the JDBC driver or CLI.
*/ */
public class RestSqlIT extends RestSqlTestCase { public class RestSqlIT extends RestSqlTestCase {
static final boolean SSL_ENABLED = Booleans.parseBoolean(System.getProperty("tests.ssl.enabled"));
static Settings securitySettings() { static Settings securitySettings() {
String token = basicAuthHeaderValue("test_admin", new SecureString("x-pack-test-password".toCharArray())); String token = basicAuthHeaderValue("test_admin", new SecureString("x-pack-test-password".toCharArray()));
return Settings.builder() Settings.Builder builder = Settings.builder()
.put(ThreadContext.PREFIX + ".Authorization", token) .put(ThreadContext.PREFIX + ".Authorization", token);
.build(); 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 @Override
protected Settings restClientSettings() { protected Settings restClientSettings() {
return securitySettings(); return securitySettings();
} }
@Override
protected String getProtocol() {
return RestSqlIT.SSL_ENABLED ? "https" : "http";
}
} }

View File

@ -26,7 +26,6 @@ import java.util.Map;
import static java.util.Collections.singletonMap; import static java.util.Collections.singletonMap;
import static org.elasticsearch.xpack.qa.sql.rest.RestSqlTestCase.columnInfo; import static org.elasticsearch.xpack.qa.sql.rest.RestSqlTestCase.columnInfo;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;

View File

@ -49,6 +49,8 @@ import static org.hamcrest.Matchers.hasItems;
public abstract class SqlSecurityTestCase extends ESRestTestCase { public abstract class SqlSecurityTestCase extends ESRestTestCase {
/** /**
* Actions taken by this test. * Actions taken by this test.
* <p>
* For methods that take {@code user} a {@code null} user means "use the admin".
*/ */
protected interface Actions { protected interface Actions {
void queryWorksAsAdmin() throws Exception; 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 { public void testQueryWorksAsAdmin() throws Exception {
actions.queryWorksAsAdmin(); actions.queryWorksAsAdmin();
new AuditLogAsserter() new AuditLogAsserter()

View File

@ -1,4 +1,8 @@
grant { grant {
// Needed to read the audit log file // Needed to read the audit log file
permission java.io.FilePermission "${tests.audit.logfile}", "read"; 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";
}; };

View File

@ -1,61 +1,366 @@
import org.elasticsearch.gradle.LoggedExec
import org.elasticsearch.gradle.MavenFilteringHack
import org.elasticsearch.gradle.test.NodeInfo 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.nio.charset.StandardCharsets
import java.security.KeyStore
import java.security.SecureRandom
String outputDir = "generated-resources/${project.name}" // Tell the tests we're running with ssl enabled
task copyTestNodeKeystore(type: Copy) { integTestRunner {
from project(':x-pack-elasticsearch:plugin') systemProperty 'tests.ssl.enabled', 'true'
.file('src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks')
into outputDir
} }
// 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 { 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 // Override the wait condition to work properly with security and SSL
waitCondition = { NodeInfo node, AntBuilder ant -> waitCondition = { NodeInfo node, AntBuilder ant ->
File tmpFile = new File(node.cwd, 'wait.success') File tmpFile = new File(node.cwd, 'wait.success')
KeyStore keyStore = KeyStore.getInstance("JKS");
// wait up to two minutes keyStore.load(clientKeyStore.newInputStream(), 'keypass'.toCharArray());
final long stopTime = System.currentTimeMillis() + (2 * 60000L); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
Exception lastException = null; kmf.init(keyStore, 'keypass'.toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
while (System.currentTimeMillis() < stopTime) { tmf.init(keyStore);
lastException = null; SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
// we use custom wait logic here as the elastic user is not available immediately and ant.get will fail when a 401 is returned sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
HttpURLConnection httpURLConnection = null; for (int i = 0; i < 10; i++) {
try { // we use custom wait logic here for HTTPS
httpURLConnection = (HttpURLConnection) new URL("http://${node.httpUri()}/_cluster/health?wait_for_nodes=${numNodes}&wait_for_status=yellow").openConnection(); HttpsURLConnection httpURLConnection = null;
httpURLConnection.setRequestProperty("Authorization", "Basic " + try {
Base64.getEncoder().encodeToString("test_admin:x-pack-test-password".getBytes(StandardCharsets.UTF_8))); httpURLConnection = (HttpsURLConnection) new URL("https://${node.httpUri()}/_cluster/health?wait_for_nodes=${numNodes}&wait_for_status=yellow").openConnection();
httpURLConnection.setRequestMethod("GET"); httpURLConnection.setSSLSocketFactory(sslContext.getSocketFactory());
httpURLConnection.setConnectTimeout(1000); httpURLConnection.setRequestProperty("Authorization", "Basic " +
httpURLConnection.setReadTimeout(30000); // read needs to wait for nodes! Base64.getEncoder().encodeToString("test_admin:x-pack-test-password".getBytes(StandardCharsets.UTF_8)));
httpURLConnection.connect(); httpURLConnection.setRequestMethod("GET");
if (httpURLConnection.getResponseCode() == 200) { httpURLConnection.connect();
tmpFile.withWriter StandardCharsets.UTF_8.name(), { if (httpURLConnection.getResponseCode() == 200) {
it.write(httpURLConnection.getInputStream().getText(StandardCharsets.UTF_8.name())) 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();
}
} }
} 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 // did not start, so wait a bit before trying again
Thread.sleep(500L); Thread.sleep(500L);
}
if (tmpFile.exists() == false && lastException != null) {
logger.error("final attempt of calling cluster health failed", lastException)
} }
return tmpFile.exists() 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<NetworkInterface> getInterfaces() throws SocketException {
List<NetworkInterface> all = new ArrayList<>();
addAllInterfaces(all, Collections.list(NetworkInterface.getNetworkInterfaces()));
Collections.sort(all, new Comparator<NetworkInterface>() {
@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<NetworkInterface> target, List<NetworkInterface> level) {
if (!level.isEmpty()) {
target.addAll(level);
for (NetworkInterface intf : level) {
addAllInterfaces(target, Collections.list(intf.getSubInterfaces()));
}
}
}
private static String getSubjectAlternativeNameString() {
List<InetAddress> 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.
*
* <p>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.
*
* <p>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();
}
} }

View File

@ -17,6 +17,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.test.rest.ESRestTestCase; import org.elasticsearch.test.rest.ESRestTestCase;
import org.elasticsearch.transport.client.PreBuiltTransportClient; 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.embed.CliHttpServer;
import org.elasticsearch.xpack.qa.sql.rest.RestSqlTestCase; import org.elasticsearch.xpack.qa.sql.rest.RestSqlTestCase;
import org.junit.After; import org.junit.After;
@ -65,7 +66,7 @@ public abstract class CliIntegrationTestCase extends ESRestTestCase {
*/ */
@Before @Before
public void startCli() throws IOException { public void startCli() throws IOException {
cli = new RemoteCli(esUrlPrefix() + ES.get()); cli = new RemoteCli(ES.get(), true, securityConfig());
} }
@After @After
@ -79,11 +80,10 @@ public abstract class CliIntegrationTestCase extends ESRestTestCase {
} }
/** /**
* Prefix to the Elasticsearch URL. Override to add * Override to add security configuration to the cli.
* authentication support.
*/ */
protected String esUrlPrefix() { protected SecurityConfig securityConfig() {
return ""; return null;
} }
protected void index(String index, CheckedConsumer<XContentBuilder, IOException> body) throws IOException { protected void index(String index, CheckedConsumer<XContentBuilder, IOException> body) throws IOException {

View File

@ -7,6 +7,7 @@ package org.elasticsearch.xpack.qa.sql.cli;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.elasticsearch.SpecialPermission; import org.elasticsearch.SpecialPermission;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.logging.Loggers;
import java.io.BufferedReader; import java.io.BufferedReader;
@ -23,7 +24,9 @@ import java.security.AccessController;
import java.security.PrivilegedAction; import java.security.PrivilegedAction;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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.empty;
import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
@ -57,7 +60,9 @@ public class RemoteCli implements Closeable {
private final PrintWriter out; private final PrintWriter out;
private final BufferedReader in; 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(); SecurityManager sm = System.getSecurityManager();
if (sm != null) { if (sm != null) {
sm.checkPermission(new SpecialPermission()); sm.checkPermission(new SpecialPermission());
@ -75,10 +80,39 @@ public class RemoteCli implements Closeable {
}); });
logger.info("connected"); logger.info("connected");
socket.setSoTimeout(10000); socket.setSoTimeout(10000);
out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true); out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
out.println(elasticsearchAddress);
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); 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 // Throw out the logo and warnings about making a dumb terminal
while (false == readLine().contains("SQL")); while (false == readLine().contains("SQL"));
// Throw out the empty line before all the good stuff // Throw out the empty line before all the good stuff
@ -136,4 +170,64 @@ public class RemoteCli implements Closeable {
logger.info("in : {}", line); logger.info("in : {}", line);
return line; return line;
} }
private String readUntil(Predicate<String> 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;
}
}
} }

View File

@ -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. // JDBC only supports a single node at a time so we just give it one.
return cluster.split(",")[0]; return cluster.split(",")[0];
/* This doesn't include "jdbc:es://" because we want the example in /* 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 { public Connection esJdbc() throws SQLException {
@ -81,8 +82,9 @@ public abstract class JdbcIntegrationTestCase extends ESRestTestCase {
} }
protected Connection useDriverManager() throws SQLException { protected Connection useDriverManager() throws SQLException {
String elasticsearchAddress = getProtocol() + "://" + elasticsearchAddress();
// tag::connect-dm // tag::connect-dm
String address = "jdbc:es://" + elasticsearchAddress(); // <1> String address = "jdbc:es://" + elasticsearchAddress; // <1>
Properties connectionProperties = connectionProperties(); // <2> Properties connectionProperties = connectionProperties(); // <2>
Connection connection = DriverManager.getConnection(address, connectionProperties); Connection connection = DriverManager.getConnection(address, connectionProperties);
// end::connect-dm // end::connect-dm
@ -91,9 +93,10 @@ public abstract class JdbcIntegrationTestCase extends ESRestTestCase {
} }
protected Connection useDataSource() throws SQLException { protected Connection useDataSource() throws SQLException {
String elasticsearchAddress = getProtocol() + "://" + elasticsearchAddress();
// tag::connect-ds // tag::connect-ds
JdbcDataSource dataSource = new JdbcDataSource(); JdbcDataSource dataSource = new JdbcDataSource();
String address = "jdbc:es://" + elasticsearchAddress(); // <1> String address = "jdbc:es://" + elasticsearchAddress; // <1>
dataSource.setUrl(address); dataSource.setUrl(address);
Properties connectionProperties = connectionProperties(); // <2> Properties connectionProperties = connectionProperties(); // <2>
dataSource.setProperties(connectionProperties); dataSource.setProperties(connectionProperties);

View File

@ -26,12 +26,16 @@ import org.elasticsearch.xpack.sql.client.shared.Version;
import java.io.IOException; import java.io.IOException;
import java.net.ConnectException; 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.Arrays;
import java.util.List; import java.util.List;
import java.util.logging.LogManager; import java.util.logging.LogManager;
public class Cli extends Command { public class Cli extends Command {
private final OptionSpec<Boolean> debugOption; private final OptionSpec<Boolean> debugOption;
private final OptionSpec<String> keystoreLocation;
private final OptionSpec<Boolean> checkOption; private final OptionSpec<Boolean> checkOption;
private final OptionSpec<String> connectionString; private final OptionSpec<String> connectionString;
@ -41,6 +45,12 @@ public class Cli extends Command {
"Enable debug logging") "Enable debug logging")
.withRequiredArg().ofType(Boolean.class) .withRequiredArg().ofType(Boolean.class)
.defaultsTo(Boolean.parseBoolean(System.getProperty("cli.debug", "false"))); .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"), this.checkOption = parser.acceptsAll(Arrays.asList("c", "check"),
"Enable initial connection check on startup") "Enable initial connection check on startup")
.withRequiredArg().ofType(Boolean.class) .withRequiredArg().ofType(Boolean.class)
@ -77,15 +87,21 @@ public class Cli extends Command {
@Override @Override
protected void execute(org.elasticsearch.cli.Terminal terminal, OptionSet options) throws Exception { protected void execute(org.elasticsearch.cli.Terminal terminal, OptionSet options) throws Exception {
boolean debug = debugOption.value(options); boolean debug = debugOption.value(options);
boolean check = checkOption.value(options); boolean checkConnection = checkOption.value(options);
List<String> args = connectionString.values(options); List<String> args = connectionString.values(options);
if (args.size() > 1) { if (args.size() > 1) {
throw new UserException(ExitCodes.USAGE, "expecting a single uri"); 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( CliCommand cliCommand = new CliCommands(
new PrintLogoCommand(), new PrintLogoCommand(),
new ClearScreenCliCommand(), new ClearScreenCliCommand(),
@ -96,10 +112,10 @@ public class Cli extends Command {
); );
try (CliTerminal cliTerminal = new JLineTerminal()) { try (CliTerminal cliTerminal = new JLineTerminal()) {
ConnectionBuilder connectionBuilder = new ConnectionBuilder(cliTerminal); ConnectionBuilder connectionBuilder = new ConnectionBuilder(cliTerminal);
ConnectionConfiguration con = connectionBuilder.buildConnection(uri); ConnectionConfiguration con = connectionBuilder.buildConnection(uri, keystoreLocation);
CliSession cliSession = new CliSession(new CliHttpClient(con)); CliSession cliSession = new CliSession(new CliHttpClient(con));
cliSession.setDebug(debug); cliSession.setDebug(debug);
if (check) { if (checkConnection) {
checkConnection(cliSession, cliTerminal, con); checkConnection(cliSession, cliTerminal, con);
} }
new CliRepl(cliTerminal, cliSession, cliCommand).execute(); new CliRepl(cliTerminal, cliSession, cliCommand).execute();

View File

@ -5,10 +5,14 @@
*/ */
package org.elasticsearch.xpack.sql.cli; package org.elasticsearch.xpack.sql.cli;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.UserException; import org.elasticsearch.cli.UserException;
import org.elasticsearch.xpack.sql.client.shared.ConnectionConfiguration; import org.elasticsearch.xpack.sql.client.shared.ConnectionConfiguration;
import java.net.URI; import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties; import java.util.Properties;
import static org.elasticsearch.xpack.sql.client.shared.UriUtils.parseURI; import static org.elasticsearch.xpack.sql.client.shared.UriUtils.parseURI;
@ -27,14 +31,19 @@ public class ConnectionBuilder {
this.cliTerminal = cliTerminal; 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 URI uri;
final String connectionString; final String connectionString;
Properties properties = new Properties(); Properties properties = new Properties();
String user = null; String user = null;
String password = null; String password = null;
if (arg != null) { if (connectionStringArg != null) {
connectionString = arg; connectionString = connectionStringArg;
uri = removeQuery(parseURI(connectionString, DEFAULT_URI), connectionString, DEFAULT_URI); uri = removeQuery(parseURI(connectionString, DEFAULT_URI), connectionString, DEFAULT_URI);
user = uri.getUserInfo(); user = uri.getUserInfo();
if (user != null) { if (user != null) {
@ -49,6 +58,29 @@ public class ConnectionBuilder {
connectionString = DEFAULT_CONNECTION_STRING; 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 (user != null) {
if (password == null) { if (password == null) {
password = cliTerminal.readPassword("password: "); password = cliTerminal.readPassword("password: ");
@ -57,7 +89,20 @@ public class ConnectionBuilder {
properties.setProperty(ConnectionConfiguration.AUTH_PASS, password); 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); 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");
}
}
} }

View File

@ -5,10 +5,14 @@
*/ */
package org.elasticsearch.xpack.sql.cli; package org.elasticsearch.xpack.sql.cli;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.sql.client.shared.ConnectionConfiguration; import org.elasticsearch.xpack.sql.client.shared.ConnectionConfiguration;
import org.elasticsearch.xpack.sql.client.shared.SslConfig;
import java.net.URI; 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.Matchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@ -22,7 +26,7 @@ public class ConnectionBuilderTests extends ESTestCase {
public void testDefaultConnection() throws Exception { public void testDefaultConnection() throws Exception {
CliTerminal testTerminal = mock(CliTerminal.class); CliTerminal testTerminal = mock(CliTerminal.class);
ConnectionBuilder connectionBuilder = new ConnectionBuilder(testTerminal); ConnectionBuilder connectionBuilder = new ConnectionBuilder(testTerminal);
ConnectionConfiguration con = connectionBuilder.buildConnection(null); ConnectionConfiguration con = connectionBuilder.buildConnection(null, null);
assertNull(con.authUser()); assertNull(con.authUser());
assertNull(con.authPass()); assertNull(con.authPass());
assertEquals("http://localhost:9200/", con.connectionString()); assertEquals("http://localhost:9200/", con.connectionString());
@ -38,7 +42,7 @@ public class ConnectionBuilderTests extends ESTestCase {
public void testBasicConnection() throws Exception { public void testBasicConnection() throws Exception {
CliTerminal testTerminal = mock(CliTerminal.class); CliTerminal testTerminal = mock(CliTerminal.class);
ConnectionBuilder connectionBuilder = new ConnectionBuilder(testTerminal); 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.authUser());
assertNull(con.authPass()); assertNull(con.authPass());
assertEquals("http://foobar:9242/", con.connectionString()); assertEquals("http://foobar:9242/", con.connectionString());
@ -49,7 +53,7 @@ public class ConnectionBuilderTests extends ESTestCase {
public void testUserAndPasswordConnection() throws Exception { public void testUserAndPasswordConnection() throws Exception {
CliTerminal testTerminal = mock(CliTerminal.class); CliTerminal testTerminal = mock(CliTerminal.class);
ConnectionBuilder connectionBuilder = new ConnectionBuilder(testTerminal); 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("user", con.authUser());
assertEquals("pass", con.authPass()); assertEquals("pass", con.authPass());
assertEquals("http://user:pass@foobar:9242/", con.connectionString()); assertEquals("http://user:pass@foobar:9242/", con.connectionString());
@ -61,7 +65,7 @@ public class ConnectionBuilderTests extends ESTestCase {
CliTerminal testTerminal = mock(CliTerminal.class); CliTerminal testTerminal = mock(CliTerminal.class);
when(testTerminal.readPassword("password: ")).thenReturn("password"); when(testTerminal.readPassword("password: ")).thenReturn("password");
ConnectionBuilder connectionBuilder = new ConnectionBuilder(testTerminal); 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("user", con.authUser());
assertEquals("password", con.authPass()); assertEquals("password", con.authPass());
assertEquals("http://user@foobar:9242/", con.connectionString()); assertEquals("http://user@foobar:9242/", con.connectionString());
@ -69,4 +73,37 @@ public class ConnectionBuilderTests extends ESTestCase {
verify(testTerminal, times(1)).readPassword(any()); verify(testTerminal, times(1)).readPassword(any());
verifyNoMoreInteractions(testTerminal); 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);
}
} }

View File

@ -94,7 +94,7 @@ public class JdbcConfiguration extends ConnectionConfiguration {
private static URI parseUrl(String u) throws JdbcSQLException { private static URI parseUrl(String u) throws JdbcSQLException {
String url = u; 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)) { if (!canAccept(u)) {
throw new JdbcSQLException("Expected [" + URL_PREFIX + "] url, received [" + 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 { private static String removeJdbcPrefix(String connectionString) throws JdbcSQLException {
if (connectionString.startsWith(URL_PREFIX)) { if (connectionString.startsWith(URL_PREFIX)) {
return "http://" + connectionString.substring(URL_PREFIX.length()); return connectionString.substring(URL_PREFIX.length());
} else { } else {
throw new JdbcSQLException("Expected [" + URL_PREFIX + "] url, received [" + connectionString + "]"); throw new JdbcSQLException("Expected [" + URL_PREFIX + "] url, received [" + connectionString + "]");
} }

View File

@ -26,30 +26,30 @@ import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory; 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_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_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_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_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_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_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_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"; private static final String SSL_TRUSTSTORE_TYPE_DEFAULT = "JKS";
static final Set<String> OPTION_NAMES = new LinkedHashSet<>(Arrays.asList(SSL, SSL_PROTOCOL, static final Set<String> OPTION_NAMES = new LinkedHashSet<>(Arrays.asList(SSL, SSL_PROTOCOL,
@ -63,7 +63,6 @@ class SslConfig {
private final SSLContext sslContext; private final SSLContext sslContext;
SslConfig(Properties settings) { SslConfig(Properties settings) {
// ssl
enabled = StringUtils.parseBoolean(settings.getProperty(SSL, SSL_DEFAULT)); enabled = StringUtils.parseBoolean(settings.getProperty(SSL, SSL_DEFAULT));
protocol = settings.getProperty(SSL_PROTOCOL, SSL_PROTOCOL_DEFAULT); protocol = settings.getProperty(SSL_PROTOCOL, SSL_PROTOCOL_DEFAULT);
keystoreLocation = settings.getProperty(SSL_KEYSTORE_LOCATION, SSL_KEYSTORE_LOCATION_DEFAULT); keystoreLocation = settings.getProperty(SSL_KEYSTORE_LOCATION, SSL_KEYSTORE_LOCATION_DEFAULT);

View File

@ -33,12 +33,14 @@ public final class UriUtils {
// check if URI can be parsed correctly without adding scheme // 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 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 // if the connection string contains IPv6 localhost [::1] the parsing will fail
URISyntaxException firstException = null;
try { try {
uri = new URI(connectionString); uri = new URI(connectionString);
if (uri.getHost() == null || uri.getScheme() == null) { if (uri.getHost() == null || uri.getScheme() == null) {
uri = null; uri = null;
} }
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
firstException = e;
uri = null; uri = null;
} }
@ -47,7 +49,12 @@ public final class UriUtils {
try { try {
return new URI("http://" + connectionString); return new URI("http://" + connectionString);
} catch (URISyntaxException e) { } 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 { } else {
// We managed to parse URI and all necessary pieces are present, let's make sure the scheme is correct // 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 {
} }
} }
} }

View File

@ -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";
};

View File

@ -83,8 +83,8 @@ public class CliFixture {
try { try {
println("accepting on localhost:" + server.getLocalPort()); println("accepting on localhost:" + server.getLocalPort());
Socket s = server.accept(); Socket s = server.accept();
String url = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.UTF_8)).readLine(); String line = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.UTF_8)).readLine();
if (url == null || url.isEmpty()) { if (line == null || line.isEmpty()) {
continue; continue;
} }
List<String> command = new ArrayList<>(); List<String> command = new ArrayList<>();
@ -100,7 +100,7 @@ public class CliFixture {
command.add("-Dorg.jline.terminal.dumb=true"); command.add("-Dorg.jline.terminal.dumb=true");
command.add("-jar"); command.add("-jar");
command.add(cliJar.toString()); command.add(cliJar.toString());
command.addAll(Arrays.asList(url.split(" "))); command.addAll(Arrays.asList(line.split(" ")));
ProcessBuilder cliBuilder = new ProcessBuilder(command); ProcessBuilder cliBuilder = new ProcessBuilder(command);
cliBuilder.redirectErrorStream(true); cliBuilder.redirectErrorStream(true);
Process process = cliBuilder.start(); Process process = cliBuilder.start();