SQL: enforce JDBC driver - ES server version parity (#38972)
(cherry picked from commit 822a21f29491f295b22dacd04b747781a69ffa61)
This commit is contained in:
parent
92206c8567
commit
c1018db404
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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/"));
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue