This is an automated email from the ASF dual-hosted git repository. liyang-kylin pushed a commit to branch kylin5 in repository https://gitbox.apache.org/repos/asf/kylin.git
commit 771c13b52cc58ff8234fa84a0c66e36577a77593 Author: Li Yang <[email protected]> AuthorDate: Fri Oct 2 22:04:07 2026 +0800 KYLIN-6111 harden JDBC source validation --- .../apache/kylin/rest/service/ProjectService.java | 12 ++- .../CommonJdbcSourceConnectionValidator.java | 67 +--------------- .../CommonJdbcSourceConnectionValidatorTest.java | 16 ++++ .../org/apache/kylin/common/KylinConfigBase.java | 64 ++++++++++++++- .../framework/SourceConnectorFactory.java | 2 +- .../sdk/datasource/framework/utils/JdbcUtils.java | 13 ++- .../DefaultJdbcSourceConnectionValidator.java | 93 ++++++++++++++++++++++ .../security/JdbcSourceValidationSettings.java | 7 +- .../datasource/framework/utils/JdbcUtilsTest.java | 35 +++++++- 9 files changed, 227 insertions(+), 82 deletions(-) diff --git a/src/common-service/src/main/java/org/apache/kylin/rest/service/ProjectService.java b/src/common-service/src/main/java/org/apache/kylin/rest/service/ProjectService.java index 4f96e6ad7d..ec5ad37e15 100644 --- a/src/common-service/src/main/java/org/apache/kylin/rest/service/ProjectService.java +++ b/src/common-service/src/main/java/org/apache/kylin/rest/service/ProjectService.java @@ -450,10 +450,14 @@ public class ProjectService extends BasicService { throw new KylinException(INVALID_PARAMETER, MsgPicker.getMsg().getIllegalNegative(KYLIN_JOB_MAX_CONCURRENT_JOBS)); } - if (overrideKylinProps.containsKey(KYLIN_SOURCE_JDBC_CONNECTION_URL_KEY)) { - String url = overrideKylinProps.get(KYLIN_SOURCE_JDBC_CONNECTION_URL_KEY); - if (KylinConfig.getInstanceFromEnv().isSourceJdbcWhiteListEnabled() - && !JdbcUtils.validateUrlByWhiteList(url)) { + if (overrideKylinProps.containsKey(KYLIN_SOURCE_JDBC_CONNECTION_URL_KEY) + || overrideKylinProps.containsKey(KYLIN_SOURCE_JDBC_DRIVER_KEY)) { + String url = overrideKylinProps.getOrDefault(KYLIN_SOURCE_JDBC_CONNECTION_URL_KEY, + projectInstance.getConfig().getJdbcConnectionUrl()); + String driver = overrideKylinProps.getOrDefault(KYLIN_SOURCE_JDBC_DRIVER_KEY, + projectInstance.getConfig().getJdbcDriver()); + if (StringUtils.isNotBlank(url) && KylinConfig.getInstanceFromEnv().isSourceJdbcWhiteListEnabled() + && !JdbcUtils.validateUrlByWhiteList(url, driver)) { throw new KylinException(INVALID_JDBC_SOURCE_CONFIG, MsgPicker.getMsg().getJdbcConnectionInfoWrong()); } } diff --git a/src/common-service/src/main/java/org/apache/kylin/rest/source/CommonJdbcSourceConnectionValidator.java b/src/common-service/src/main/java/org/apache/kylin/rest/source/CommonJdbcSourceConnectionValidator.java index 72c3cb122b..1b6463bbf0 100644 --- a/src/common-service/src/main/java/org/apache/kylin/rest/source/CommonJdbcSourceConnectionValidator.java +++ b/src/common-service/src/main/java/org/apache/kylin/rest/source/CommonJdbcSourceConnectionValidator.java @@ -18,70 +18,7 @@ package org.apache.kylin.rest.source; -import java.util.List; -import java.util.Map; -import java.util.Set; +import org.apache.kylin.sdk.datasource.security.DefaultJdbcSourceConnectionValidator; -import org.apache.commons.lang3.StringUtils; -import org.apache.kylin.sdk.datasource.security.AbstractJdbcSourceConnectionValidator; -import org.springframework.web.util.UriComponents; -import org.springframework.web.util.UriComponentsBuilder; - -import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@NoArgsConstructor -@Slf4j -public class CommonJdbcSourceConnectionValidator extends AbstractJdbcSourceConnectionValidator { - - private static final String JDBC_COLON = "jdbc:"; - - private boolean parsed = false; - private String scheme; - private String host; - private int port; - private String path; - private Map<String, List<String>> queryParams; - - @Override - public boolean isValid() { - if (!parsed) { - try { - parseUrl(); - } catch (Exception e) { - log.error("Error on parseUrl", e); - return false; - } - } - // Only query param keys need to be validated currently - return validateQueryParamKeys(); - } - - private boolean validateQueryParamKeys() { - Set<String> validUrlParamKeys = settings.getValidUrlParamKeys(); - Set<String> userInputKeys = queryParams.keySet(); - return validUrlParamKeys.containsAll(userInputKeys); - } - - private void parseUrl() { - if (parsed) { - return; - } - if (StringUtils.isBlank(url)) { - throw new IllegalStateException("url cannot be empty"); - } - if (!url.startsWith(JDBC_COLON)) { - throw new IllegalStateException("url must start with " + JDBC_COLON); - } - - String noPrefixUrl = url.substring(JDBC_COLON.length()); - UriComponents uri = UriComponentsBuilder.fromUriString(noPrefixUrl).build(); - scheme = uri.getScheme(); - host = uri.getHost(); - port = uri.getPort(); - path = uri.getPath(); - queryParams = uri.getQueryParams(); - - parsed = true; - } +public class CommonJdbcSourceConnectionValidator extends DefaultJdbcSourceConnectionValidator { } diff --git a/src/common-service/src/test/java/org/apache/kylin/rest/source/CommonJdbcSourceConnectionValidatorTest.java b/src/common-service/src/test/java/org/apache/kylin/rest/source/CommonJdbcSourceConnectionValidatorTest.java index 2cc2942af5..56bab6d79f 100644 --- a/src/common-service/src/test/java/org/apache/kylin/rest/source/CommonJdbcSourceConnectionValidatorTest.java +++ b/src/common-service/src/test/java/org/apache/kylin/rest/source/CommonJdbcSourceConnectionValidatorTest.java @@ -31,6 +31,7 @@ public class CommonJdbcSourceConnectionValidatorTest { public void testValidate() { JdbcSourceValidationSettings settings = JdbcSourceValidationSettings.builder() .validUrlParamKeys(Sets.newHashSet("q1", "q2", "q3")) + .validSemicolonParamKeys(Sets.newHashSet("DB_CLOSE_DELAY", "MODE")) .build(); { @@ -43,6 +44,21 @@ public class CommonJdbcSourceConnectionValidatorTest { validator.settings(settings).url("jdbc:mysql://localhost:123/data_db?q1=v1&q2=v2&q4=v4"); assertFalse(validator.isValid()); } + { + CommonJdbcSourceConnectionValidator validator = new CommonJdbcSourceConnectionValidator(); + validator.settings(settings).url("jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;MODE=MYSQL"); + assertTrue(validator.isValid()); + } + { + CommonJdbcSourceConnectionValidator validator = new CommonJdbcSourceConnectionValidator(); + validator.settings(settings).url("jdbc:h2:mem:db;INIT=RUNSCRIPT FROM 'evil.sql'"); + assertFalse(validator.isValid()); + } + { + CommonJdbcSourceConnectionValidator validator = new CommonJdbcSourceConnectionValidator(); + validator.settings(settings).url("jdbc:mysql://localhost:123/data_db;allowLoadLocalInfile=true"); + assertFalse(validator.isValid()); + } } @Test diff --git a/src/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java b/src/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java index 060aa42c5f..8097e6fc27 100644 --- a/src/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java +++ b/src/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java @@ -4530,15 +4530,16 @@ public abstract class KylinConfigBase implements Serializable { } public boolean isSourceJdbcWhiteListEnabled() { - return Boolean.parseBoolean(getOptional("kylin.source.jdbc.white-list.enabled", FALSE)); + return Boolean.parseBoolean(getOptional("kylin.source.jdbc.white-list.enabled", TRUE)); } public Set<String> getSourceJdbcWhiteListSchemes() { - String config = StringUtils.deleteWhitespace(getOptional("kylin.source.jdbc.white-list.schemes", "")); + String config = StringUtils.deleteWhitespace(getOptional("kylin.source.jdbc.white-list.schemes", + "h2,mysql,postgresql,sqlserver,snowflake")); if (StringUtils.isBlank(config)) { return Collections.emptySet(); } - return Sets.newHashSet(config.split(",")); + return Arrays.stream(config.split(",")).map(value -> value.toLowerCase(Locale.ROOT)).collect(Collectors.toSet()); } public String getSourceJdbcWhiteListValidatorClassByScheme(String scheme) { @@ -4547,7 +4548,7 @@ public abstract class KylinConfigBase implements Serializable { return null; } return getOptional(String.format(Locale.ROOT, "kylin.source.jdbc.white-list.%s.validator-class", scheme), - "org.apache.kylin.rest.source.CommonJdbcSourceConnectionValidator"); + "org.apache.kylin.sdk.datasource.security.DefaultJdbcSourceConnectionValidator"); } public Set<String> getSourceJdbcWhiteListUrlParamKeysByScheme(String scheme) { @@ -4563,6 +4564,61 @@ public abstract class KylinConfigBase implements Serializable { return Sets.newHashSet(config.split(",")); } + public Set<String> getSourceJdbcWhiteListDriversByScheme(String scheme) { + Set<String> whiteListSchemes = getSourceJdbcWhiteListSchemes(); + if (!whiteListSchemes.contains(scheme)) { + return Collections.emptySet(); + } + String config = StringUtils.deleteWhitespace(getOptional(String.format(Locale.ROOT, + "kylin.source.jdbc.white-list.%s.drivers", scheme), getDefaultSourceJdbcDrivers(scheme))); + if (StringUtils.isBlank(config)) { + return Collections.emptySet(); + } + return Sets.newHashSet(config.split(",")); + } + + public Set<String> getSourceJdbcWhiteListSemicolonParamKeysByScheme(String scheme) { + Set<String> whiteListSchemes = getSourceJdbcWhiteListSchemes(); + if (!whiteListSchemes.contains(scheme)) { + return Collections.emptySet(); + } + String defaultValue; + switch (scheme) { + case "h2": + defaultValue = "DB_CLOSE_DELAY,MODE,DATABASE_TO_UPPER,DEFAULT_LOCK_TIMEOUT,SCHEMA"; + break; + case "sqlserver": + defaultValue = "databaseName,encrypt,trustServerCertificate,applicationName,loginTimeout,socketTimeout"; + break; + default: + defaultValue = ""; + break; + } + String config = StringUtils.deleteWhitespace(getOptional(String.format(Locale.ROOT, + "kylin.source.jdbc.white-list.%s.semicolon-param-keys", scheme), defaultValue)); + if (StringUtils.isBlank(config)) { + return Collections.emptySet(); + } + return Sets.newHashSet(config.split(",")); + } + + private String getDefaultSourceJdbcDrivers(String scheme) { + switch (scheme) { + case "h2": + return "org.h2.Driver"; + case "mysql": + return "com.mysql.jdbc.Driver,com.mysql.cj.jdbc.Driver"; + case "postgresql": + return "org.postgresql.Driver"; + case "sqlserver": + return "com.microsoft.sqlserver.jdbc.SQLServerDriver"; + case "snowflake": + return "net.snowflake.client.jdbc.SnowflakeDriver"; + default: + return ""; + } + } + public boolean isForcedToPushDown() { return Boolean.parseBoolean(getOptional("kylin.query.pushdown-force", FALSE)); } diff --git a/src/datasource-sdk/src/main/java/org/apache/kylin/sdk/datasource/framework/SourceConnectorFactory.java b/src/datasource-sdk/src/main/java/org/apache/kylin/sdk/datasource/framework/SourceConnectorFactory.java index 9be07b6913..48c08fd330 100644 --- a/src/datasource-sdk/src/main/java/org/apache/kylin/sdk/datasource/framework/SourceConnectorFactory.java +++ b/src/datasource-sdk/src/main/java/org/apache/kylin/sdk/datasource/framework/SourceConnectorFactory.java @@ -43,7 +43,7 @@ public class SourceConnectorFactory { String adaptorClazz = config.getJdbcAdaptorClass(); if (KylinConfig.getInstanceFromEnv().isSourceJdbcWhiteListEnabled() - && !JdbcUtils.validateUrlByWhiteList(jdbcUrl)) { + && !JdbcUtils.validateUrlByWhiteList(jdbcUrl, jdbcDriver)) { throw new KylinException(INVALID_JDBC_SOURCE_CONFIG, MsgPicker.getMsg().getJdbcConnectionInfoWrong()); } diff --git a/src/datasource-sdk/src/main/java/org/apache/kylin/sdk/datasource/framework/utils/JdbcUtils.java b/src/datasource-sdk/src/main/java/org/apache/kylin/sdk/datasource/framework/utils/JdbcUtils.java index 28f616a2b1..68e8a8eb7a 100644 --- a/src/datasource-sdk/src/main/java/org/apache/kylin/sdk/datasource/framework/utils/JdbcUtils.java +++ b/src/datasource-sdk/src/main/java/org/apache/kylin/sdk/datasource/framework/utils/JdbcUtils.java @@ -19,6 +19,7 @@ package org.apache.kylin.sdk.datasource.framework.utils; import java.sql.Connection; +import java.util.Locale; import java.util.Properties; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -47,7 +48,8 @@ public class JdbcUtils { connProp.put("username", username); connProp.put("password", password); - if (KylinConfig.getInstanceFromEnv().isSourceJdbcWhiteListEnabled() && !validateUrlByWhiteList(url)) { + if (KylinConfig.getInstanceFromEnv().isSourceJdbcWhiteListEnabled() + && !validateUrlByWhiteList(url, driver)) { log.warn("jdbc url white list check failed"); return false; } @@ -61,7 +63,7 @@ public class JdbcUtils { } } - public static boolean validateUrlByWhiteList(String url) { + public static boolean validateUrlByWhiteList(String url, String driver) { try { KylinConfig config = KylinConfig.getInstanceFromEnv(); String scheme = null; @@ -70,12 +72,17 @@ public class JdbcUtils { if (m.find()) { scheme = m.group(); } + scheme = scheme == null ? null : scheme.toLowerCase(Locale.ROOT); if (StringUtils.isBlank(scheme) || !config.getSourceJdbcWhiteListSchemes().contains(scheme)) { return false; } + if (!config.getSourceJdbcWhiteListDriversByScheme(scheme).contains(driver)) { + return false; + } JdbcSourceValidationSettings settings = JdbcSourceValidationSettings.builder() - .validUrlParamKeys(config.getSourceJdbcWhiteListUrlParamKeysByScheme(scheme)).build(); + .validUrlParamKeys(config.getSourceJdbcWhiteListUrlParamKeysByScheme(scheme)) + .validSemicolonParamKeys(config.getSourceJdbcWhiteListSemicolonParamKeysByScheme(scheme)).build(); JdbcSourceConnectionValidator validator = (JdbcSourceConnectionValidator) ClassUtil .newInstance(config.getSourceJdbcWhiteListValidatorClassByScheme(scheme)); diff --git a/src/datasource-sdk/src/main/java/org/apache/kylin/sdk/datasource/security/DefaultJdbcSourceConnectionValidator.java b/src/datasource-sdk/src/main/java/org/apache/kylin/sdk/datasource/security/DefaultJdbcSourceConnectionValidator.java new file mode 100644 index 0000000000..ca0f1819db --- /dev/null +++ b/src/datasource-sdk/src/main/java/org/apache/kylin/sdk/datasource/security/DefaultJdbcSourceConnectionValidator.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.kylin.sdk.datasource.security; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; + +public class DefaultJdbcSourceConnectionValidator extends AbstractJdbcSourceConnectionValidator { + + private static final String JDBC_PREFIX = "jdbc:"; + + @Override + public boolean isValid() { + if (StringUtils.isBlank(url) || !url.startsWith(JDBC_PREFIX)) { + return false; + } + + try { + String schemeSpecificUrl = url.substring(JDBC_PREFIX.length()); + int schemeEnd = schemeSpecificUrl.indexOf(':'); + if (schemeEnd <= 0 || StringUtils.isBlank(schemeSpecificUrl.substring(0, schemeEnd))) { + return false; + } + + int queryIndex = url.indexOf('?'); + String urlWithoutQuery = queryIndex < 0 ? url : url.substring(0, queryIndex); + String query = queryIndex < 0 ? "" : url.substring(queryIndex + 1); + if (queryIndex >= 0 && StringUtils.isBlank(query)) { + return false; + } + if (query.contains(";") || url.indexOf('#') >= 0) { + return false; + } + + Set<String> queryKeys = parseKeys(query, '&'); + Set<String> semicolonKeys = parseSemicolonKeys(urlWithoutQuery); + return settings.getValidUrlParamKeys().containsAll(queryKeys) + && settings.getValidSemicolonParamKeys().containsAll(semicolonKeys); + } catch (IllegalArgumentException e) { + return false; + } + } + + private Set<String> parseSemicolonKeys(String urlWithoutQuery) { + int semicolonIndex = urlWithoutQuery.indexOf(';'); + if (semicolonIndex < 0) { + return Collections.emptySet(); + } + String semicolonContent = urlWithoutQuery.substring(semicolonIndex + 1); + if (StringUtils.isBlank(semicolonContent)) { + throw new IllegalArgumentException("empty semicolon parameters"); + } + return parseKeys(semicolonContent, ';'); + } + + private Set<String> parseKeys(String content, char separator) { + if (StringUtils.isBlank(content)) { + return Collections.emptySet(); + } + + Set<String> keys = new LinkedHashSet<>(); + for (String segment : content.split(String.valueOf(separator), -1)) { + if (StringUtils.isBlank(segment)) { + throw new IllegalArgumentException("empty parameter"); + } + int equalsIndex = segment.indexOf('='); + String key = equalsIndex < 0 ? segment : segment.substring(0, equalsIndex); + key = key.trim(); + if (StringUtils.isBlank(key) || !keys.add(key)) { + throw new IllegalArgumentException("invalid parameter"); + } + } + return keys; + } +} diff --git a/src/datasource-sdk/src/main/java/org/apache/kylin/sdk/datasource/security/JdbcSourceValidationSettings.java b/src/datasource-sdk/src/main/java/org/apache/kylin/sdk/datasource/security/JdbcSourceValidationSettings.java index da2065ae0c..3eaad3a6d5 100644 --- a/src/datasource-sdk/src/main/java/org/apache/kylin/sdk/datasource/security/JdbcSourceValidationSettings.java +++ b/src/datasource-sdk/src/main/java/org/apache/kylin/sdk/datasource/security/JdbcSourceValidationSettings.java @@ -18,6 +18,7 @@ package org.apache.kylin.sdk.datasource.security; +import java.util.Collections; import java.util.Set; import lombok.AllArgsConstructor; @@ -29,5 +30,9 @@ import lombok.Getter; @AllArgsConstructor public final class JdbcSourceValidationSettings { - private Set<String> validUrlParamKeys; + @Builder.Default + private Set<String> validUrlParamKeys = Collections.emptySet(); + + @Builder.Default + private Set<String> validSemicolonParamKeys = Collections.emptySet(); } diff --git a/src/datasource-sdk/src/test/java/org/apache/kylin/sdk/datasource/framework/utils/JdbcUtilsTest.java b/src/datasource-sdk/src/test/java/org/apache/kylin/sdk/datasource/framework/utils/JdbcUtilsTest.java index a53d5297c1..989d775a10 100644 --- a/src/datasource-sdk/src/test/java/org/apache/kylin/sdk/datasource/framework/utils/JdbcUtilsTest.java +++ b/src/datasource-sdk/src/test/java/org/apache/kylin/sdk/datasource/framework/utils/JdbcUtilsTest.java @@ -21,6 +21,7 @@ package org.apache.kylin.sdk.datasource.framework.utils; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import org.apache.kylin.common.KylinConfig; import org.apache.kylin.common.util.NLocalFileMetadataTestCase; import org.apache.kylin.sdk.datasource.security.AbstractJdbcSourceConnectionValidator; import org.junit.After; @@ -48,12 +49,38 @@ public class JdbcUtilsTest extends NLocalFileMetadataTestCase { "org.apache.kylin.sdk.datasource.framework.utils.JdbcUtilsTest$MockJdbcSourceConnectionValidator"); // valid cases - assertTrue(JdbcUtils.validateUrlByWhiteList("jdbc:mysql://localhost:3306/db")); - assertTrue(JdbcUtils.validateUrlByWhiteList("jdbc:postgresql://localhost:5433/db")); + assertTrue(JdbcUtils.validateUrlByWhiteList("jdbc:mysql://localhost:3306/db", "com.mysql.cj.jdbc.Driver")); + assertTrue(JdbcUtils.validateUrlByWhiteList("jdbc:postgresql://localhost:5433/db", "org.postgresql.Driver")); // invalid cases - assertFalse(JdbcUtils.validateUrlByWhiteList("xxx://localhost:3306/db")); - assertFalse(JdbcUtils.validateUrlByWhiteList("jdbc:mongodb://localhost:1234/db")); + assertFalse(JdbcUtils.validateUrlByWhiteList("xxx://localhost:3306/db", "com.mysql.cj.jdbc.Driver")); + assertFalse(JdbcUtils.validateUrlByWhiteList("jdbc:mongodb://localhost:1234/db", "org.mongodb.Driver")); + } + + @Test + public void testValidateUrlByWhiteList_driverCheck() { + overwriteSystemProp("kylin.source.jdbc.white-list.mysql.validator-class", + "org.apache.kylin.sdk.datasource.framework.utils.JdbcUtilsTest$MockJdbcSourceConnectionValidator"); + assertTrue(JdbcUtils.validateUrlByWhiteList("jdbc:mysql://localhost:3306/db", "com.mysql.jdbc.Driver")); + assertFalse(JdbcUtils.validateUrlByWhiteList("jdbc:mysql://localhost:3306/db", "org.h2.Driver")); + assertFalse(JdbcUtils.validateUrlByWhiteList("jdbc:mysql://localhost:3306/db", "com.mysql.jdbc.driver")); + } + + @Test + public void testValidateUrlByWhiteList_enabledByDefault() { + assertTrue(KylinConfig.getInstanceFromEnv().isSourceJdbcWhiteListEnabled()); + } + + @Test + public void testValidateUrlByWhiteList_semicolonSettings() { + assertTrue(JdbcUtils.validateUrlByWhiteList("jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;MODE=MYSQL", "org.h2.Driver")); + assertTrue(JdbcUtils.validateUrlByWhiteList("jdbc:sqlserver://localhost;databaseName=kylin", + "com.microsoft.sqlserver.jdbc.SQLServerDriver")); + assertFalse(JdbcUtils.validateUrlByWhiteList("jdbc:h2:mem:db;INIT=RUNSCRIPT FROM 'evil.sql'", + "org.h2.Driver")); + assertFalse(JdbcUtils.validateUrlByWhiteList("jdbc:h2:mem:db;", "org.h2.Driver")); + assertFalse(JdbcUtils.validateUrlByWhiteList( + "jdbc:mysql://localhost:3306/db;allowLoadLocalInfile=true", "com.mysql.cj.jdbc.Driver")); } public static class MockJdbcSourceConnectionValidator extends AbstractJdbcSourceConnectionValidator {
