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 4af32c5b2bfd40ffe365940913eaaba4f2a08734
Author: Li Yang <[email protected]>
AuthorDate: Sat Nov 7 14:44:43 2026 +0800

    KYLIN-6124 validate query pushdown JDBC config
---
 .../apache/kylin/rest/service/ProjectService.java  | 92 ++++++++++++++++++----
 .../kylin/rest/service/ProjectServiceTest.java     | 62 +++++++++++++++
 2 files changed, 140 insertions(+), 14 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 d767b7b396..2d02209294 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
@@ -173,6 +173,16 @@ public class ProjectService extends BasicService {
     private static final String SNAPSHOT_AUTO_REFRESH_TIME_MODE_MINUTE = 
"MINUTE";
     private static final String SNAPSHOT_AUTO_REFRESH_TIME_MODES = "DAY, 
HOURS, MINUTE";
     private static final String KYLIN_QUERY_PUSHDOWN_RUNNER_CLASS_NAME = 
"kylin.query.pushdown.runner-class-name";
+    private static final String 
KYLIN_QUERY_PUSHDOWN_PARTITION_CHECK_RUNNER_CLASS_NAME =
+            "kylin.query.pushdown.partition-check.runner-class-name";
+    private static final String KYLIN_QUERY_PUSHDOWN_CONVERTER_CLASS_NAMES =
+            "kylin.query.pushdown.converter-class-names";
+    private static final String KYLIN_QUERY_PUSHDOWN_JDBC_URL = 
"kylin.query.pushdown.jdbc.url";
+    private static final String KYLIN_QUERY_PUSHDOWN_JDBC_DRIVER = 
"kylin.query.pushdown.jdbc.driver";
+    private static final Set<String> DEFAULT_PUSHDOWN_RUNNER_CLASS_NAMES = 
Sets.newHashSet(
+            "org.apache.kylin.query.pushdown.PushDownRunnerJdbcImpl",
+            "org.apache.kylin.query.pushdown.PushDownRunnerSparkImpl",
+            "org.apache.kylin.sdk.datasource.PushDownRunnerSDKImpl");
     private static final String DEFAULT_VAL = "default";
     private static final Pattern INVALID_FILENAME_CHARS = 
Pattern.compile("[\\\\/:*?\"'<>|\\u0000-\\u001F\\s]|^[.]+$|[.]$");
 
@@ -444,26 +454,81 @@ public class ProjectService extends BasicService {
         if (projectInstance == null) {
             throw new KylinException(PROJECT_NOT_EXIST, project);
         }
-        if (overrideKylinProps.containsKey(KYLIN_JOB_MAX_CONCURRENT_JOBS)) {
-            val maxConcurrentJobs = 
Ints.tryParse(overrideKylinProps.get(KYLIN_JOB_MAX_CONCURRENT_JOBS));
+        Map<String, String> normalizedOverrides = 
KylinConfig.trimKVFromMap(overrideKylinProps);
+        if (normalizedOverrides.containsKey(KYLIN_JOB_MAX_CONCURRENT_JOBS)) {
+            val maxConcurrentJobs = 
Ints.tryParse(normalizedOverrides.get(KYLIN_JOB_MAX_CONCURRENT_JOBS));
             if (Objects.isNull(maxConcurrentJobs) || maxConcurrentJobs < 0)
                 throw new KylinException(INVALID_PARAMETER,
                         
MsgPicker.getMsg().getIllegalNegative(KYLIN_JOB_MAX_CONCURRENT_JOBS));
         }
-        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,
+        if 
(normalizedOverrides.containsKey(KYLIN_SOURCE_JDBC_CONNECTION_URL_KEY)
+                || 
normalizedOverrides.containsKey(KYLIN_SOURCE_JDBC_DRIVER_KEY)) {
+            String url = 
normalizedOverrides.getOrDefault(KYLIN_SOURCE_JDBC_CONNECTION_URL_KEY,
                     projectInstance.getConfig().getJdbcConnectionUrl());
-            String driver = 
overrideKylinProps.getOrDefault(KYLIN_SOURCE_JDBC_DRIVER_KEY,
+            String driver = 
normalizedOverrides.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());
             }
         }
-        encryptJdbcPassInOverrideKylinProps(overrideKylinProps);
+        validateQueryPushdownConfig(projectInstance, normalizedOverrides);
+        encryptJdbcPassInOverrideKylinProps(normalizedOverrides);
         projectManager.updateProject(project, copyForWrite -> 
copyForWrite.getOverrideKylinProps()
-                .putAll(KylinConfig.trimKVFromMap(overrideKylinProps)));
+                .putAll(normalizedOverrides));
+    }
+
+    private void validateQueryPushdownConfig(ProjectInstance projectInstance, 
Map<String, String> overrides) {
+        validateQueryPushdownJdbcConfig(projectInstance, overrides);
+        validateQueryPushdownRunnerClasses(overrides);
+        validateQueryPushdownConverterClasses(overrides);
+    }
+
+    private void validateQueryPushdownJdbcConfig(ProjectInstance 
projectInstance, Map<String, String> overrides) {
+        if (!overrides.containsKey(KYLIN_QUERY_PUSHDOWN_JDBC_URL)
+                && !overrides.containsKey(KYLIN_QUERY_PUSHDOWN_JDBC_DRIVER)) {
+            return;
+        }
+        String url = overrides.getOrDefault(KYLIN_QUERY_PUSHDOWN_JDBC_URL, 
projectInstance.getConfig().getJdbcUrl());
+        String driver = 
overrides.getOrDefault(KYLIN_QUERY_PUSHDOWN_JDBC_DRIVER,
+                projectInstance.getConfig().getJdbcDriverClass());
+        if (StringUtils.isNotBlank(url) && 
KylinConfig.getInstanceFromEnv().isSourceJdbcWhiteListEnabled()
+                && !JdbcUtils.validateUrlByWhiteList(url, driver)) {
+            throw new KylinException(INVALID_JDBC_SOURCE_CONFIG, 
MsgPicker.getMsg().getJdbcConnectionInfoWrong());
+        }
+    }
+
+    private void validateQueryPushdownRunnerClasses(Map<String, String> 
overrides) {
+        KylinConfig config = KylinConfig.getInstanceFromEnv();
+        Set<String> allowedRunners = 
Sets.newHashSet(DEFAULT_PUSHDOWN_RUNNER_CLASS_NAMES);
+        allowedRunners.add(config.getPushDownRunnerClassName());
+        allowedRunners.add(config.getPartitionCheckRunnerClassName());
+        validatePushdownClassOverride(overrides, 
KYLIN_QUERY_PUSHDOWN_RUNNER_CLASS_NAME, allowedRunners);
+        validatePushdownClassOverride(overrides, 
KYLIN_QUERY_PUSHDOWN_PARTITION_CHECK_RUNNER_CLASS_NAME,
+                allowedRunners);
+    }
+
+    private void validatePushdownClassOverride(Map<String, String> overrides, 
String key, Set<String> allowedClasses) {
+        String className = overrides.get(key);
+        if (StringUtils.isNotBlank(className) && 
!allowedClasses.contains(className)) {
+            throw new KylinException(INVALID_PARAMETER, 
String.format(Locale.ROOT,
+                    "Unsupported query pushdown class for '%s'", key));
+        }
+    }
+
+    private void validateQueryPushdownConverterClasses(Map<String, String> 
overrides) {
+        String converterClassNames = 
overrides.get(KYLIN_QUERY_PUSHDOWN_CONVERTER_CLASS_NAMES);
+        if (StringUtils.isBlank(converterClassNames)) {
+            return;
+        }
+        Set<String> allowedConverters = Sets
+                
.newHashSet(KylinConfig.getInstanceFromEnv().getPushDownConverterClassNames());
+        for (String converterClassName : 
StringUtils.split(converterClassNames, ',')) {
+            if (!allowedConverters.contains(converterClassName.trim())) {
+                throw new KylinException(INVALID_PARAMETER,
+                        "Unsupported query pushdown converter class");
+            }
+        }
     }
 
     private void encryptJdbcPassInOverrideKylinProps(Map<String, String> 
overrideKylinProps) {
@@ -895,12 +960,11 @@ public class ProjectService extends BasicService {
     @PreAuthorize(Constant.ACCESS_HAS_ROLE_ADMIN + " or 
hasPermission(#project, 'ADMINISTRATION')")
     @Transaction(project = 0)
     public void updatePushDownProjectConfig(String project, 
PushDownProjectConfigRequest pushDownProjectConfigRequest) {
-        getManager(NProjectManager.class).updateProject(project, copyForWrite 
-> {
-            
copyForWrite.putOverrideKylinProps(KYLIN_QUERY_PUSHDOWN_RUNNER_CLASS_NAME,
-                    pushDownProjectConfigRequest.getRunnerClassName());
-            
copyForWrite.putOverrideKylinProps("kylin.query.pushdown.converter-class-names",
-                    pushDownProjectConfigRequest.getConverterClassNames());
-        });
+        Map<String, String> overrides = Maps.newHashMap();
+        overrides.put(KYLIN_QUERY_PUSHDOWN_RUNNER_CLASS_NAME, 
pushDownProjectConfigRequest.getRunnerClassName());
+        overrides.put(KYLIN_QUERY_PUSHDOWN_CONVERTER_CLASS_NAMES,
+                pushDownProjectConfigRequest.getConverterClassNames());
+        updateProjectOverrideKylinProps(project, overrides);
     }
 
     @PreAuthorize(Constant.ACCESS_HAS_ROLE_ADMIN + " or 
hasPermission(#project, 'ADMINISTRATION')")
diff --git 
a/src/modeling-service/src/test/java/org/apache/kylin/rest/service/ProjectServiceTest.java
 
b/src/modeling-service/src/test/java/org/apache/kylin/rest/service/ProjectServiceTest.java
index 26a87e7731..3b905e6652 100644
--- 
a/src/modeling-service/src/test/java/org/apache/kylin/rest/service/ProjectServiceTest.java
+++ 
b/src/modeling-service/src/test/java/org/apache/kylin/rest/service/ProjectServiceTest.java
@@ -531,6 +531,68 @@ public class ProjectServiceTest extends 
NLocalFileMetadataTestCase {
         Assert.assertThrows(KylinException.class, () -> 
projectService.updateDefaultDatabase(project, "not_exits"));
     }
 
+    @Test
+    public void testUpdateProjectConfigAcceptsSafePushdownJdbcConfig() {
+        Map<String, String> overrides = Maps.newHashMap();
+        overrides.put("kylin.query.pushdown.runner-class-name",
+                "org.apache.kylin.query.pushdown.PushDownRunnerJdbcImpl");
+        overrides.put("kylin.query.pushdown.jdbc.driver", "org.h2.Driver");
+        overrides.put("kylin.query.pushdown.jdbc.url", 
"jdbc:h2:mem:pushdown;MODE=MYSQL");
+
+        projectService.updateProjectConfig(PROJECT, overrides);
+
+        Map<String, String> projectOverrides = 
projectManager.getProject(PROJECT).getOverrideKylinProps();
+        
Assert.assertEquals("org.apache.kylin.query.pushdown.PushDownRunnerJdbcImpl",
+                
projectOverrides.get("kylin.query.pushdown.runner-class-name"));
+        Assert.assertEquals("org.h2.Driver", 
projectOverrides.get("kylin.query.pushdown.jdbc.driver"));
+        Assert.assertEquals("jdbc:h2:mem:pushdown;MODE=MYSQL",
+                projectOverrides.get("kylin.query.pushdown.jdbc.url"));
+    }
+
+    @Test
+    public void testUpdateProjectConfigRejectsUnsafePushdownJdbcUrl() {
+        Map<String, String> overrides = Maps.newHashMap();
+        overrides.put("kylin.query.pushdown.runner-class-name",
+                "org.apache.kylin.query.pushdown.PushDownRunnerJdbcImpl");
+        overrides.put("kylin.query.pushdown.jdbc.driver", "org.h2.Driver");
+        overrides.put(" kylin.query.pushdown.jdbc.url ",
+                "jdbc:h2:mem:pushdown;INIT=RUNSCRIPT FROM 
'http://127.0.0.1/init.sql'");
+
+        Assert.assertThrows(KylinException.class, () -> 
projectService.updateProjectConfig(PROJECT, overrides));
+        
Assert.assertNull(projectManager.getProject(PROJECT).getOverrideKylinProps()
+                .get("kylin.query.pushdown.jdbc.url"));
+    }
+
+    @Test
+    public void testUpdateProjectConfigRejectsMismatchedPushdownDriver() {
+        Map<String, String> overrides = Maps.newHashMap();
+        overrides.put("kylin.query.pushdown.jdbc.driver", 
"com.mysql.cj.jdbc.Driver");
+        overrides.put("kylin.query.pushdown.jdbc.url", "jdbc:h2:mem:pushdown");
+
+        Assert.assertThrows(KylinException.class, () -> 
projectService.updateProjectConfig(PROJECT, overrides));
+        
Assert.assertNull(projectManager.getProject(PROJECT).getOverrideKylinProps()
+                .get("kylin.query.pushdown.jdbc.driver"));
+    }
+
+    @Test
+    public void testUpdateProjectConfigRejectsUnknownPushdownRunnerClass() {
+        Map<String, String> overrides = Maps.newHashMap();
+        overrides.put("kylin.query.pushdown.runner-class-name", 
"java.lang.Runtime");
+
+        Assert.assertThrows(KylinException.class, () -> 
projectService.updateProjectConfig(PROJECT, overrides));
+        
Assert.assertNull(projectManager.getProject(PROJECT).getOverrideKylinProps()
+                .get("kylin.query.pushdown.runner-class-name"));
+    }
+
+    @Test
+    public void testUpdatePushdownProjectConfigRejectsUnknownRunnerClass() {
+        PushDownProjectConfigRequest request = new 
PushDownProjectConfigRequest();
+        request.setRunnerClassName("java.lang.Runtime");
+
+        Assert.assertThrows(KylinException.class,
+                () -> projectService.updatePushDownProjectConfig(PROJECT, 
request));
+    }
+
     @Test
     public void testUpdateProjectConfigTrim() {
 

Reply via email to