SQL: enforce JDBC driver - ES server version parity (#38972)

(cherry picked from commit 822a21f29491f295b22dacd04b747781a69ffa61)
This commit is contained in:
Andrei Stefan 2019-02-20 11:25:25 +02:00 committed by Andrei Stefan
parent 92206c8567
commit c1018db404
9 changed files with 201 additions and 40 deletions

View File

@ -24,6 +24,7 @@ dependencies {
compile project(':libs:core')
runtime "com.fasterxml.jackson.core:jackson-core:${versions.jackson}"
testCompile "org.elasticsearch.test:framework:${version}"
testCompile project(path: xpackModule('core'), configuration: 'testArtifacts')
}
dependencyLicenses {

View File

@ -12,10 +12,21 @@ class InfoResponse {
final String cluster;
final int majorVersion;
final int minorVersion;
final int revisionVersion;
InfoResponse(String clusterName, byte versionMajor, byte versionMinor) {
InfoResponse(String clusterName, byte versionMajor, byte versionMinor, byte revisionVersion) {
this.cluster = clusterName;
this.majorVersion = versionMajor;
this.minorVersion = versionMinor;
this.revisionVersion = revisionVersion;
}
@Override
public String toString() {
return cluster + "[" + versionString() + "]";
}
public String versionString() {
return majorVersion + "." + minorVersion + "." + revisionVersion;
}
}

View File

@ -31,7 +31,7 @@ import static org.elasticsearch.xpack.sql.client.StringUtils.EMPTY;
class JdbcHttpClient {
private final HttpClient httpClient;
private final JdbcConfiguration conCfg;
private InfoResponse serverInfo;
private final InfoResponse serverInfo;
/**
* The SQLException is the only type of Exception the JDBC API can throw (and that the user expects).
@ -40,6 +40,8 @@ class JdbcHttpClient {
JdbcHttpClient(JdbcConfiguration conCfg) throws SQLException {
httpClient = new HttpClient(conCfg);
this.conCfg = conCfg;
this.serverInfo = fetchServerInfo();
checkServerVersion();
}
boolean ping(long timeoutInMs) throws SQLException {
@ -72,16 +74,22 @@ class JdbcHttpClient {
}
InfoResponse serverInfo() throws SQLException {
if (serverInfo == null) {
serverInfo = fetchServerInfo();
}
return serverInfo;
}
private InfoResponse fetchServerInfo() throws SQLException {
MainResponse mainResponse = httpClient.serverInfo();
Version version = Version.fromString(mainResponse.getVersion());
return new InfoResponse(mainResponse.getClusterName(), version.major, version.minor);
return new InfoResponse(mainResponse.getClusterName(), version.major, version.minor, version.revision);
}
private void checkServerVersion() throws SQLException {
if (serverInfo.majorVersion != Version.CURRENT.major
|| serverInfo.minorVersion != Version.CURRENT.minor
|| serverInfo.revisionVersion != Version.CURRENT.revision) {
throw new SQLException("This version of the JDBC driver is only compatible with Elasticsearch version " +
Version.CURRENT.toString() + ", attempting to connect to a server version " + serverInfo.versionString());
}
}
/**

View File

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql.jdbc;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.test.http.MockResponse;
import java.io.IOException;
import java.net.URISyntaxException;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Collectors;
public class JdbcConfigurationDataSourceTests extends WebServerTestCase {
public void testDataSourceConfigurationWithSSLInURL() throws SQLException, URISyntaxException, IOException {
webServer().enqueue(new MockResponse().setResponseCode(200).addHeader("Content-Type", "application/json").setBody(
XContentHelper.toXContent(createCurrentVersionMainResponse(), XContentType.JSON, false).utf8ToString()));
Map<String, String> urlPropMap = JdbcConfigurationTests.sslProperties();
Properties allProps = new Properties();
allProps.putAll(urlPropMap);
String sslUrlProps = urlPropMap.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&"));
EsDataSource dataSource = new EsDataSource();
String address = "jdbc:es://" + webServerAddress() + "/?" + sslUrlProps;
dataSource.setUrl(address);
JdbcConnection connection = null;
try {
connection = (JdbcConnection) dataSource.getConnection();
} catch (SQLException sqle) {
fail("Connection creation should have been successful. Error: " + sqle);
}
assertEquals(address, connection.getURL());
JdbcConfigurationTests.assertSslConfig(allProps, connection.cfg.sslConfig());
}
}

View File

@ -266,28 +266,6 @@ public class JdbcConfigurationTests extends ESTestCase {
}
}
public void testDataSourceConfigurationWithSSLInURL() throws SQLException, URISyntaxException {
Map<String, String> urlPropMap = sslProperties();
Properties allProps = new Properties();
allProps.putAll(urlPropMap);
String sslUrlProps = urlPropMap.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&"));
EsDataSource dataSource = new EsDataSource();
String address = "jdbc:es://test?" + sslUrlProps;
dataSource.setUrl(address);
JdbcConnection connection = null;
try {
connection = (JdbcConnection) dataSource.getConnection();
} catch (SQLException sqle) {
fail("Connection creation should have been successful. Error: " + sqle);
}
assertEquals(address, connection.getURL());
assertSslConfig(allProps, connection.cfg.sslConfig());
}
public void testTyposInSslConfigInUrl(){
assertJdbcSqlExceptionFromUrl("ssl.protocl", "ssl.protocol");
assertJdbcSqlExceptionFromUrl("sssl", "ssl");
@ -310,7 +288,7 @@ public class JdbcConfigurationTests extends ESTestCase {
assertJdbcSqlExceptionFromProperties("ssl.ruststore.type", "ssl.truststore.type");
}
private Map<String, String> sslProperties() {
static Map<String, String> sslProperties() {
Map<String, String> sslPropertiesMap = new HashMap<>(8);
// always using "false" so that the SSLContext doesn't actually start verifying the keystore and trustore
// locations, as we don't have file permissions to access them.
@ -326,7 +304,7 @@ public class JdbcConfigurationTests extends ESTestCase {
return sslPropertiesMap;
}
private void assertSslConfig(Properties allProperties, SslConfig sslConfig) throws URISyntaxException {
static void assertSslConfig(Properties allProperties, SslConfig sslConfig) throws URISyntaxException {
// because SslConfig doesn't expose its internal properties (and it shouldn't),
// we compare a newly created SslConfig with the one from the JdbcConfiguration with the equals() method
SslConfig mockSslConfig = new SslConfig(allProperties, new URI("http://test:9200/"));

View File

@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql.jdbc;
import org.elasticsearch.Version;
import org.elasticsearch.action.main.MainResponse;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.test.VersionUtils;
import org.elasticsearch.test.http.MockResponse;
import java.io.IOException;
import java.sql.SQLException;
/**
* Test class for JDBC-ES server versions checks.
*
* It's using a {@code MockWebServer} to be able to create a response just like the one an ES instance
* would create for a request to "/", where the ES version used is configurable.
*/
public class VersionParityTests extends WebServerTestCase {
public void testExceptionThrownOnIncompatibleVersions() throws IOException, SQLException {
Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, VersionUtils.getPreviousVersion(Version.CURRENT));
prepareRequest(version);
String url = JdbcConfiguration.URL_PREFIX + webServer().getHostName() + ":" + webServer().getPort();
SQLException ex = expectThrows(SQLException.class, () -> new JdbcHttpClient(JdbcConfiguration.create(url, null, 0)));
assertEquals("This version of the JDBC driver is only compatible with Elasticsearch version "
+ org.elasticsearch.xpack.sql.client.Version.CURRENT.toString()
+ ", attempting to connect to a server version " + version.toString(), ex.getMessage());
}
public void testNoExceptionThrownForCompatibleVersions() throws IOException {
prepareRequest(null);
String url = JdbcConfiguration.URL_PREFIX + webServerAddress();
try {
new JdbcHttpClient(JdbcConfiguration.create(url, null, 0));
} catch (SQLException sqle) {
fail("JDBC driver version and Elasticsearch server version should be compatible. Error: " + sqle);
}
}
void prepareRequest(Version version) throws IOException {
MainResponse response = version == null ? createCurrentVersionMainResponse() : createMainResponse(version);
webServer().enqueue(new MockResponse().setResponseCode(200).addHeader("Content-Type", "application/json").setBody(
XContentHelper.toXContent(response, XContentType.JSON, false).utf8ToString()));
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql.jdbc;
import org.elasticsearch.Build;
import org.elasticsearch.Version;
import org.elasticsearch.action.main.MainResponse;
import org.elasticsearch.cluster.ClusterName;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.http.MockWebServer;
import org.junit.After;
import org.junit.Before;
import java.util.Date;
/**
* Base class for unit tests that need a web server for basic tests.
*/
public abstract class WebServerTestCase extends ESTestCase {
private MockWebServer webServer = new MockWebServer();
@Before
public void init() throws Exception {
webServer.start();
}
@After
public void cleanup() {
webServer.close();
}
public MockWebServer webServer() {
return webServer;
}
MainResponse createCurrentVersionMainResponse() {
return createMainResponse(Version.CURRENT);
}
MainResponse createMainResponse(Version version) {
String clusterUuid = randomAlphaOfLength(10);
ClusterName clusterName = new ClusterName(randomAlphaOfLength(10));
String nodeName = randomAlphaOfLength(10);
final String date = new Date(randomNonNegativeLong()).toString();
Build build = new Build(
Build.Flavor.UNKNOWN, Build.Type.UNKNOWN, randomAlphaOfLength(8), date, randomBoolean(),
version.toString()
);
return new MainResponse(nodeName, version, clusterName, clusterUuid , build);
}
String webServerAddress() {
return webServer.getHostName() + ":" + webServer.getPort();
}
}

View File

@ -18,6 +18,10 @@ cli_or_drivers_minimal:
privileges: [read, "indices:admin/get"]
# end::cli_drivers
read_nothing:
cluster:
- "cluster:monitor/main"
read_something_else:
cluster:
- "cluster:monitor/main"

View File

@ -230,16 +230,16 @@ public class JdbcSecurityIT extends SqlSecurityTestCase {
@Override
public void checkNoMonitorMain(String user) throws Exception {
// Most SQL actually works fine without monitor/main
expectMatchesAdmin("SELECT * FROM test", user, "SELECT * FROM test");
expectMatchesAdmin("SHOW TABLES LIKE 'test'", user, "SHOW TABLES LIKE 'test'");
expectMatchesAdmin("DESCRIBE test", user, "DESCRIBE test");
// But there are a few things that don't work
try (Connection es = es(userProperties(user))) {
expectUnauthorized("cluster:monitor/main", user, () -> es.getMetaData().getDatabaseMajorVersion());
expectUnauthorized("cluster:monitor/main", user, () -> es.getMetaData().getDatabaseMinorVersion());
}
// Without monitor/main the JDBC driver - ES server version comparison doesn't take place, which fails everything else
expectUnauthorized("cluster:monitor/main", user, () -> es(userProperties(user)));
expectUnauthorized("cluster:monitor/main", user, () -> es(userProperties(user)).getMetaData().getDatabaseMajorVersion());
expectUnauthorized("cluster:monitor/main", user, () -> es(userProperties(user)).getMetaData().getDatabaseMinorVersion());
expectUnauthorized("cluster:monitor/main", user,
() -> es(userProperties(user)).createStatement().executeQuery("SELECT * FROM test"));
expectUnauthorized("cluster:monitor/main", user,
() -> es(userProperties(user)).createStatement().executeQuery("SHOW TABLES LIKE 'test'"));
expectUnauthorized("cluster:monitor/main", user,
() -> es(userProperties(user)).createStatement().executeQuery("DESCRIBE test"));
}
private void expectUnauthorized(String action, String user, ThrowingRunnable r) {