This is an automated email from the ASF dual-hosted git repository.

JNSimba pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new c8a75b273e9 [fix](fe) Require ADMIN for remote CDC driver URLs (#69617)
c8a75b273e9 is described below

commit c8a75b273e999370e354ed39ae65db2c483c0ad0
Author: wudi <[email protected]>
AuthorDate: Tue Nov 3 16:10:48 2026 +0800

    [fix](fe) Require ADMIN for remote CDC driver URLs (#69617)
    
    ### What problem does this PR solve?
    
    Issue Number: N/A
    
    Problem Summary:
    
    Users with permission to create CDC jobs can supply HTTP/HTTPS JDBC
    driver URLs that load code into the FE JVM. Require global ADMIN
    privilege when these URLs are supplied during CDC job creation,
    alteration, or `cdc_stream` construction, before source initialization
    or driver loading.
    
    Share the permission check across the FROM-TO and TVF paths. Preserve
    existing local driver permissions and S3 streaming behavior.
---
 .../insert/streaming/StreamingInsertJob.java       | 22 +++++++--
 .../apache/doris/job/util/StreamingJobUtils.java   | 12 +++++
 .../trees/plans/commands/AlterJobCommand.java      |  6 ++-
 .../CdcStreamTableValuedFunction.java              |  3 ++
 .../StreamingInsertJobOffsetPersistenceTest.java   | 51 +++++++++++++++++----
 .../doris/job/util/StreamingJobUtilsTest.java      | 30 +++++++++++++
 .../cdc/test_streaming_mysql_job_priv.groovy       | 29 +++++++++++-
 .../cdc/tvf/test_cdc_stream_tvf_mysql.groovy       | 21 +++++++++
 .../tvf/test_streaming_job_cdc_stream_mysql.groovy | 52 ++++++++++++++++++++--
 9 files changed, 208 insertions(+), 18 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJob.java
 
b/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJob.java
index add7761ddc2..a1ee8ccc85a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJob.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJob.java
@@ -79,6 +79,7 @@ import org.apache.doris.qe.ShowResultSetMetaData;
 import org.apache.doris.rpc.RpcException;
 import org.apache.doris.service.FrontendOptions;
 import org.apache.doris.system.Backend;
+import org.apache.doris.tablefunction.CdcStreamTableValuedFunction;
 import org.apache.doris.tablefunction.S3TableValuedFunction;
 import org.apache.doris.thrift.TCell;
 import org.apache.doris.thrift.TRow;
@@ -267,6 +268,8 @@ public class StreamingInsertJob extends 
AbstractJob<StreamingJobSchedulerTask, M
         try {
             init();
             checkRequiredSourceProperties();
+            StreamingJobUtils.checkCdcDriverPrivilege(ConnectContext.get(),
+                    sourceProperties.get(DataSourceConfigKeys.DRIVER_URL));
             DataSourceConfigValidator.validateSourceBeforeTableCreation(
                     dataSourceType, sourceProperties);
             List<String> createTbls = createTableIfNotExists();
@@ -429,9 +432,13 @@ public class StreamingInsertJob extends 
AbstractJob<StreamingJobSchedulerTask, M
     private void initInsertJob() {
         try {
             init();
+            ConnectContext ctx = ConnectContext.get();
             UnboundTVFRelation currentTvf = getCurrentTvf();
             this.tvfType = currentTvf.getFunctionName();
             this.originTvfProps = currentTvf.getProperties().getMap();
+            if (CdcStreamTableValuedFunction.NAME.equalsIgnoreCase(tvfType)) {
+                StreamingJobUtils.checkCdcDriverPrivilege(ctx, 
originTvfProps.get(DataSourceConfigKeys.DRIVER_URL));
+            }
             this.offsetProvider = createOffsetProvider(sourceProperties);
             this.offsetProvider.ensureInitialized(getJobId(), originTvfProps);
             // cdc_stream TVF has TABLE; S3 TVF doesn't, so syncTables stays 
null there.
@@ -489,9 +496,18 @@ public class StreamingInsertJob extends 
AbstractJob<StreamingJobSchedulerTask, M
     public void initLogicalPlan(boolean regen) {
         if (regen || baseCommand == null) {
             this.baseCommand = null;
-            makeConnectContext();
-            LogicalPlan logicalPlan = new 
NereidsParser().parseSingle(getExecuteSql());
-            this.baseCommand = (InsertIntoTableCommand) logicalPlan;
+            ConnectContext previousContext = ConnectContext.get();
+            try {
+                makeConnectContext();
+                LogicalPlan logicalPlan = new 
NereidsParser().parseSingle(getExecuteSql());
+                this.baseCommand = (InsertIntoTableCommand) logicalPlan;
+            } finally {
+                // Subsequent privilege checks must retain the caller's 
identity and session roles.
+                ConnectContext.remove();
+                if (previousContext != null) {
+                    previousContext.setThreadLocalInfo();
+                }
+            }
         }
     }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/job/util/StreamingJobUtils.java 
b/fe/fe-core/src/main/java/org/apache/doris/job/util/StreamingJobUtils.java
index 5c5b4064338..88a98854e49 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/job/util/StreamingJobUtils.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/job/util/StreamingJobUtils.java
@@ -31,6 +31,7 @@ import org.apache.doris.catalog.info.TableNameInfo;
 import org.apache.doris.cloud.system.CloudSystemInfoService;
 import org.apache.doris.common.Config;
 import org.apache.doris.common.DdlException;
+import org.apache.doris.common.ErrorCode;
 import org.apache.doris.common.FeConstants;
 import org.apache.doris.common.util.SmallFileMgr;
 import org.apache.doris.common.util.SmallFileMgr.SmallFile;
@@ -43,6 +44,8 @@ import org.apache.doris.job.common.DataSourceType;
 import org.apache.doris.job.exception.JobException;
 import 
org.apache.doris.job.extensions.insert.streaming.PostgresResourceValidator;
 import org.apache.doris.job.extensions.insert.streaming.StreamingInsertJob;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.trees.plans.commands.CreateTableCommand;
 import org.apache.doris.nereids.trees.plans.commands.info.ColumnDefinition;
 import org.apache.doris.nereids.trees.plans.commands.info.CreateTableInfo;
@@ -265,6 +268,15 @@ public class StreamingJobUtils {
         return ctx;
     }
 
+    public static void checkCdcDriverPrivilege(ConnectContext ctx, String 
driverUrl) {
+        if ((StringUtils.startsWithIgnoreCase(driverUrl, "http://";)
+                || StringUtils.startsWithIgnoreCase(driverUrl, "https://";))
+                && 
!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ctx, 
PrivPredicate.ADMIN)) {
+            throw new 
AnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR.formatErrorMsg("ADMIN")
+                    + " HTTP/HTTPS CDC driver URLs require ADMIN privilege. 
Use a preinstalled JDBC driver instead.");
+        }
+    }
+
     public static JdbcClient getJdbcClient(DataSourceType sourceType, 
Map<String, String> properties) {
         JdbcClientConfig config = new JdbcClientConfig();
         config.setCatalog(sourceType.name());
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterJobCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterJobCommand.java
index efa1757a6d5..965989002f6 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterJobCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AlterJobCommand.java
@@ -28,6 +28,7 @@ import org.apache.doris.job.common.JobStatus;
 import 
org.apache.doris.job.extensions.insert.streaming.DataSourceConfigValidator;
 import org.apache.doris.job.extensions.insert.streaming.StreamingInsertJob;
 import org.apache.doris.job.extensions.insert.streaming.StreamingJobProperties;
+import org.apache.doris.job.util.StreamingJobUtils;
 import org.apache.doris.nereids.analyzer.UnboundTVFRelation;
 import org.apache.doris.nereids.parser.NereidsParser;
 import org.apache.doris.nereids.trees.plans.PlanType;
@@ -168,6 +169,8 @@ public class AlterJobCommand extends AlterCommand 
implements ForwardWithSync, Ne
                 boolean sourcePropModified =
                         
isPropertiesModified(streamingJob.getSourceProperties(), 
this.getSourceProperties());
                 if (sourcePropModified) {
+                    
StreamingJobUtils.checkCdcDriverPrivilege(ConnectContext.get(),
+                            
sourceProperties.get(DataSourceConfigKeys.DRIVER_URL));
                     
DataSourceConfigValidator.validateSource(this.getSourceProperties(),
                             streamingJob.getDataSourceType().name());
                     
checkUnmodifiableSourceProperties(streamingJob.getSourceProperties());
@@ -325,7 +328,8 @@ public class AlterJobCommand extends AlterCommand 
implements ForwardWithSync, Ne
                                     
inputTvf.getProperties().getMap().get(unmodifiable)),
                             "The '%s' property cannot be modified in ALTER JOB 
for cdc_stream", unmodifiable);
                 }
-                
CdcStreamTableValuedFunction.validateProperties(inputTvf.getProperties().getMap());
+                // Validate merged resource properties as the ALTER caller, 
before schema discovery.
+                new 
CdcStreamTableValuedFunction(inputTvf.getProperties().getMap()).checkAuth(ConnectContext.get());
                 break;
             default:
                 throw new IllegalArgumentException("Unsupported tvf type:" + 
inputTvf.getFunctionName());
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/CdcStreamTableValuedFunction.java
 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/CdcStreamTableValuedFunction.java
index 0a53ec38479..14f02c80a62 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/CdcStreamTableValuedFunction.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/CdcStreamTableValuedFunction.java
@@ -29,6 +29,7 @@ import org.apache.doris.job.common.DataSourceType;
 import 
org.apache.doris.job.extensions.insert.streaming.DataSourceConfigValidator;
 import 
org.apache.doris.job.extensions.insert.streaming.StreamingJdbcUrlNormalizer;
 import org.apache.doris.job.util.StreamingJobUtils;
+import org.apache.doris.qe.ConnectContext;
 import org.apache.doris.thrift.TBrokerFileStatus;
 import org.apache.doris.thrift.TFileType;
 
@@ -83,6 +84,8 @@ public class CdcStreamTableValuedFunction extends 
ExternalFileTableValuedFunctio
         StreamingJobUtils.populateDefaultSourceProperties(sourceType, 
copyProps, jobId);
 
         super.parseCommonProperties(copyProps);
+        StreamingJobUtils.checkCdcDriverPrivilege(ConnectContext.get(),
+                processedParams.get(DataSourceConfigKeys.DRIVER_URL));
         this.processedParams.put(ENABLE_CDC_CLIENT_KEY, "true");
         this.processedParams.put(URI_KEY, URI);
         this.processedParams.put(HTTP_ENABLE_RANGE_REQUEST_KEY, "false");
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJobOffsetPersistenceTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJobOffsetPersistenceTest.java
index 63bce5df853..c07703f4b2c 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJobOffsetPersistenceTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJobOffsetPersistenceTest.java
@@ -17,10 +17,13 @@
 
 package org.apache.doris.job.extensions.insert.streaming;
 
+import org.apache.doris.analysis.UserIdentity;
 import org.apache.doris.catalog.Env;
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.Config;
 import org.apache.doris.common.jmockit.Deencapsulation;
+import org.apache.doris.datasource.CatalogMgr;
+import org.apache.doris.datasource.InternalCatalog;
 import org.apache.doris.job.cdc.DataSourceConfigKeys;
 import org.apache.doris.job.cdc.request.CommitOffsetRequest;
 import org.apache.doris.job.cdc.split.SnapshotSplit;
@@ -34,8 +37,10 @@ import 
org.apache.doris.job.offset.jdbc.JdbcSourceOffsetProvider;
 import org.apache.doris.job.offset.jdbc.JdbcTvfSourceOffsetProvider;
 import org.apache.doris.job.offset.s3.S3Offset;
 import org.apache.doris.job.offset.s3.S3SourceOffsetProvider;
+import org.apache.doris.nereids.exceptions.ParseException;
 import org.apache.doris.nereids.trees.plans.commands.AlterJobCommand;
 import org.apache.doris.persist.gson.GsonUtils;
+import org.apache.doris.qe.ConnectContext;
 import org.apache.doris.tablefunction.CdcStreamTableValuedFunction;
 import org.apache.doris.tablefunction.S3TableValuedFunction;
 import org.apache.doris.transaction.GlobalTransactionMgrIface;
@@ -121,21 +126,49 @@ public class StreamingInsertJobOffsetPersistenceTest {
         Deencapsulation.setField(job, "jobId", 9001L);
         job.setJobStatus(JobStatus.PAUSED);
         Deencapsulation.setField(job, "tvfType", 
CdcStreamTableValuedFunction.NAME);
-        HashMap<String, String> tvfProperties = new HashMap<>();
-        tvfProperties.put(DataSourceConfigKeys.TYPE, "postgres");
-        tvfProperties.put(DataSourceConfigKeys.JDBC_URL, 
"jdbc:postgresql://127.0.0.1:5432/source_db");
-        tvfProperties.put(DataSourceConfigKeys.TABLE, "source_table");
-        Deencapsulation.setField(job, "originTvfProps", tvfProperties);
+        Deencapsulation.setField(job, "createUser", UserIdentity.ROOT);
+        job.setExecuteSql("INSERT INTO internal.test_db.target_table SELECT * 
FROM cdc_stream("
+                + "\"type\"=\"postgres\", 
\"jdbc_url\"=\"jdbc:postgresql://127.0.0.1:5432/source_db\", "
+                + "\"table\"=\"source_table\")");
 
         job.gsonPostProcess();
         JdbcTvfSourceOffsetProvider provider = Deencapsulation.getField(job, 
"offsetProvider");
         Assertions.assertNull(Deencapsulation.getField(provider, 
"sourceType"));
 
-        job.validateAlterOffset("{\"lsn\":\"300\"}");
+        ConnectContext previousContext = ConnectContext.get();
+        try (MockedStatic<Env> envMockedStatic = 
Mockito.mockStatic(Env.class)) {
+            Env env = Mockito.mock(Env.class);
+            InternalCatalog catalog = Mockito.mock(InternalCatalog.class);
+            CatalogMgr catalogMgr = Mockito.mock(CatalogMgr.class);
+            
Mockito.when(catalog.getName()).thenReturn(InternalCatalog.INTERNAL_CATALOG_NAME);
+            Mockito.when(env.getInternalCatalog()).thenReturn(catalog);
+            Mockito.when(env.getCatalogMgr()).thenReturn(catalogMgr);
+            
Mockito.when(catalogMgr.getCatalog(InternalCatalog.INTERNAL_CATALOG_NAME)).thenReturn(catalog);
+            envMockedStatic.when(Env::getCurrentEnv).thenReturn(env);
+            ConnectContext caller = new ConnectContext();
+            
caller.setCurrentUserIdentity(UserIdentity.createAnalyzedUserIdentWithIp("alter_user",
 "%"));
+            
caller.setAuthenticatedRoles(Collections.singleton("session_role"));
+            caller.setThreadLocalInfo();
+
+            // Restored jobs have no cached plan/TVF properties. Parsing must 
not replace the caller.
+            job.validateAlterOffset("{\"lsn\":\"300\"}");
 
-        Assertions.assertEquals(JobStatus.PAUSED, job.getJobStatus());
-        Assertions.assertThrows(AnalysisException.class,
-                () -> 
job.validateAlterOffset("{\"file\":\"mysql-bin.000001\",\"pos\":\"154\"}"));
+            Assertions.assertSame(caller, ConnectContext.get());
+            Assertions.assertEquals(Collections.singleton("session_role"),
+                    ConnectContext.get().getAuthenticatedRoles());
+            Assertions.assertEquals(JobStatus.PAUSED, job.getJobStatus());
+            Assertions.assertThrows(AnalysisException.class,
+                    () -> 
job.validateAlterOffset("{\"file\":\"mysql-bin.000001\",\"pos\":\"154\"}"));
+
+            job.setExecuteSql("INSERT INTO");
+            Assertions.assertThrows(ParseException.class, () -> 
job.initLogicalPlan(true));
+            Assertions.assertSame(caller, ConnectContext.get());
+        } finally {
+            ConnectContext.remove();
+            if (previousContext != null) {
+                previousContext.setThreadLocalInfo();
+            }
+        }
     }
 
     @Test
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/job/util/StreamingJobUtilsTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/job/util/StreamingJobUtilsTest.java
index b55fa2c817c..ee72dd9c043 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/job/util/StreamingJobUtilsTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/job/util/StreamingJobUtilsTest.java
@@ -30,6 +30,10 @@ import org.apache.doris.datasource.jdbc.client.JdbcClient;
 import org.apache.doris.job.cdc.DataSourceConfigKeys;
 import org.apache.doris.job.common.DataSourceType;
 import org.apache.doris.job.exception.JobException;
+import org.apache.doris.mysql.privilege.AccessControllerManager;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.qe.ConnectContext;
 import org.apache.doris.qe.GlobalVariable;
 
 import org.junit.jupiter.api.Assertions;
@@ -57,6 +61,32 @@ public class StreamingJobUtilsTest {
         MockitoAnnotations.initMocks(this);
     }
 
+    @Test
+    public void testCdcDriverPrivilege() {
+        Env env = Mockito.mock(Env.class);
+        ConnectContext ctx = Mockito.mock(ConnectContext.class);
+        AccessControllerManager accessManager = 
Mockito.mock(AccessControllerManager.class);
+        try (MockedStatic<Env> mockedEnv = Mockito.mockStatic(Env.class)) {
+            mockedEnv.when(Env::getCurrentEnv).thenReturn(env);
+            Mockito.when(env.getAccessManager()).thenReturn(accessManager);
+            List<String> remoteDrivers = 
List.of("http://example.invalid/driver.jar";,
+                    "https://example.invalid/driver.jar";);
+            for (String driver : remoteDrivers) {
+                AnalysisException error = 
Assertions.assertThrows(AnalysisException.class,
+                        () -> StreamingJobUtils.checkCdcDriverPrivilege(ctx, 
driver));
+                Assertions.assertTrue(error.getMessage().contains("ADMIN"));
+            }
+            Mockito.when(accessManager.checkGlobalPriv(ctx, 
PrivPredicate.ADMIN)).thenReturn(true);
+            for (String driver : remoteDrivers) {
+                Assertions.assertDoesNotThrow(() -> 
StreamingJobUtils.checkCdcDriverPrivilege(ctx, driver));
+            }
+            Mockito.when(accessManager.checkGlobalPriv(ctx, 
PrivPredicate.ADMIN)).thenReturn(false);
+            for (String driver : Arrays.asList(null, "driver.jar", 
"file:///opt/doris/driver.jar")) {
+                Assertions.assertDoesNotThrow(() -> 
StreamingJobUtils.checkCdcDriverPrivilege(ctx, driver));
+            }
+        }
+    }
+
     @Test
     public void testBinarySourceColumnsUseSupportedOlapTypes() {
         List<Column> columns = new ArrayList<>();
diff --git 
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_priv.groovy
 
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_priv.groovy
index 6100c7eee9e..1507d3c3b5f 100644
--- 
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_priv.groovy
+++ 
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_priv.groovy
@@ -100,14 +100,34 @@ suite("test_streaming_mysql_job_priv", 
"p0,external,mysql,external_docker,extern
             sql """INSERT INTO ${mysqlDb}.${tableName} (name, age) VALUES 
('B1', 2);"""
         }
 
+        // Prepare a local driver without granting ADMIN to the job creator.
+        File driverFile = new 
File("${context.config.dataPath}/jdbc_driver/mysql_priv_driver.jar").absoluteFile
+        driverFile.parentFile.mkdirs()
+        def download = new ProcessBuilder("curl", "--fail", "--location", 
"--max-time", "120",
+                driver_url, "--output", 
driverFile.path).redirectErrorStream(true).start()
+        String downloadOutput = download.text
+        assertEquals(0, download.waitFor(), downloadOutput)
+        scp_udf_file_to_all_fe(driverFile.path)
+        String localDriverUrl = "file://${driverFile.path}"
+
         // Create the job without ALTER privilege to verify schema changes use 
the job creator's identity.
         sql """grant load_priv,create_priv on ${dbName}.* to ${user}"""
         connect(user, "${pwd}", url) {
+            test {
+                sql """CREATE JOB ${jobName} ON STREAMING
+                        FROM MYSQL (
+                            "jdbc_url" = 
"jdbc:mysql://${externalEnvIp}:${mysql_port}",
+                            "driver_url" = "${driver_url}", "driver_class" = 
"com.mysql.cj.jdbc.Driver",
+                            "user" = "root", "password" = "123456", "database" 
= "${mysqlDb}",
+                            "include_tables" = "${tableName}", "offset" = 
"initial")
+                        TO DATABASE ${currentDb} 
("table.create.properties.replication_num" = "1")"""
+                exception "HTTP/HTTPS CDC driver URLs require ADMIN privilege"
+            }
             sql """CREATE JOB ${jobName}
                     ON STREAMING
                     FROM MYSQL (
                         "jdbc_url" = 
"jdbc:mysql://${externalEnvIp}:${mysql_port}?allowPublicKeyRetrieval=true&useSSL=false",
-                        "driver_url" = "${driver_url}",
+                        "driver_url" = "${localDriverUrl}",
                         "driver_class" = "com.mysql.cj.jdbc.Driver",
                         "user" = "root",
                         "password" = "123456",
@@ -191,6 +211,13 @@ suite("test_streaming_mysql_job_priv", 
"p0,external,mysql,external_docker,extern
         }
 
         sql "PAUSE JOB where jobname =  '${jobName}'";
+        connect(user, "${pwd}", url) {
+            test {
+                sql """ALTER JOB ${jobName} FROM MYSQL ("driver_url" = 
"${driver_url}")
+                        TO DATABASE ${currentDb}"""
+                exception "HTTP/HTTPS CDC driver URLs require ADMIN privilege"
+            }
+        }
 
         sql """ALTER JOB ${jobName}
                 FROM MYSQL (
diff --git 
a/regression-test/suites/job_p0/streaming_job/cdc/tvf/test_cdc_stream_tvf_mysql.groovy
 
b/regression-test/suites/job_p0/streaming_job/cdc/tvf/test_cdc_stream_tvf_mysql.groovy
index 744ab7539ac..c9b6b766e82 100644
--- 
a/regression-test/suites/job_p0/streaming_job/cdc/tvf/test_cdc_stream_tvf_mysql.groovy
+++ 
b/regression-test/suites/job_p0/streaming_job/cdc/tvf/test_cdc_stream_tvf_mysql.groovy
@@ -138,6 +138,27 @@ suite("test_cdc_stream_tvf_mysql", 
"p0,external,mysql,external_docker,external_d
             exception "ssl_mode 'verify-ca' requires ssl_rootcert to be set"
         }
 
+        // Remote driver URLs require ADMIN even for standalone TVF queries.
+        String driverUser = "test_cdc_stream_tvf_mysql_driver_user"
+        def jdbcTokens = context.getJdbcUrl().split('/')
+        String driverUserUrl = jdbcTokens[0] + "//" + jdbcTokens[2] + "/" + 
currentDb + "?"
+        sql """DROP USER IF EXISTS '${driverUser}'"""
+        sql """CREATE USER '${driverUser}' IDENTIFIED BY '123456'"""
+        sql """GRANT SELECT_PRIV ON ${currentDb}.* TO '${driverUser}'"""
+        try {
+            connectToDoris(driverUser, "123456", driverUserUrl) {
+                test {
+                    sql """SELECT * FROM cdc_stream(
+                            "type" = "mysql", "jdbc_url" = 
"jdbc:mysql://${externalEnvIp}:${mysql_port}",
+                            "database" = "${mysqlDb}", "table" = "${table1}", 
"offset" = "latest",
+                            "driver_url" = "${driver_url}")"""
+                    exception "HTTP/HTTPS CDC driver URLs require ADMIN 
privilege"
+                }
+            }
+        } finally {
+            sql """DROP USER '${driverUser}'"""
+        }
+
         // --- Data setup ---
 
         connect("root", "123456", 
"jdbc:mysql://${externalEnvIp}:${mysql_port}") {
diff --git 
a/regression-test/suites/job_p0/streaming_job/cdc/tvf/test_streaming_job_cdc_stream_mysql.groovy
 
b/regression-test/suites/job_p0/streaming_job/cdc/tvf/test_streaming_job_cdc_stream_mysql.groovy
index d0848257a8b..78d8b76a897 100644
--- 
a/regression-test/suites/job_p0/streaming_job/cdc/tvf/test_streaming_job_cdc_stream_mysql.groovy
+++ 
b/regression-test/suites/job_p0/streaming_job/cdc/tvf/test_streaming_job_cdc_stream_mysql.groovy
@@ -67,10 +67,8 @@ suite("test_streaming_job_cdc_stream_mysql", 
"p0,external,mysql,external_docker,
             sql """INSERT INTO ${mysqlDb}.${mysqlTable} (name, age) VALUES 
('B1', 2)"""
         }
 
-        // create streaming job via cdc_stream TVF (offset=initial → snapshot 
then binlog)
-        sql """
-            CREATE JOB ${jobName}
-            ON STREAMING DO INSERT INTO ${currentDb}.${dorisTable} (name, age)
+        String insertSql = """
+            INSERT INTO ${currentDb}.${dorisTable} (name, age)
             SELECT name, age FROM cdc_stream(
                 "type"         = "mysql",
                 "jdbc_url"     = "jdbc:mysql://${externalEnvIp}:${mysql_port}",
@@ -85,6 +83,25 @@ suite("test_streaming_job_cdc_stream_mysql", 
"p0,external,mysql,external_docker,
             )
         """
 
+        String driverUser = "test_streaming_cdc_mysql_driver_user"
+        def jdbcTokens = context.getJdbcUrl().split('/')
+        String driverUserUrl = jdbcTokens[0] + "//" + jdbcTokens[2] + "/" + 
currentDb + "?"
+        sql """DROP USER IF EXISTS '${driverUser}'"""
+        sql """CREATE USER '${driverUser}' IDENTIFIED BY '123456'"""
+        sql """GRANT LOAD_PRIV ON ${currentDb}.* TO '${driverUser}'"""
+        if (isCloudMode()) {
+            def cluster = sql_return_maparray("SHOW CLUSTERS").find { 
it.is_current.equalsIgnoreCase("TRUE") }.cluster
+            sql """GRANT USAGE_PRIV ON CLUSTER `${cluster}` TO 
'${driverUser}'"""
+        }
+        connectToDoris(driverUser, "123456", driverUserUrl) {
+            test {
+                sql """CREATE JOB ${jobName} ON STREAMING DO ${insertSql}"""
+                exception "HTTP/HTTPS CDC driver URLs require ADMIN privilege"
+            }
+        }
+        // The administrator can create the same job with the remote driver.
+        sql """CREATE JOB ${jobName} ON STREAMING DO ${insertSql}"""
+
         // wait for at least one snapshot task to succeed
         try {
             Awaitility.await().atMost(300, SECONDS).pollInterval(2, 
SECONDS).until({
@@ -130,6 +147,33 @@ suite("test_streaming_job_cdc_stream_mysql", 
"p0,external,mysql,external_docker,
             s.size() == 1 && s.get(0).get(0) == "PAUSED"
         })
 
+        String driverResource = "test_streaming_cdc_mysql_driver_resource"
+        try_sql """DROP RESOURCE '${driverResource}'"""
+        try {
+            sql """CREATE RESOURCE '${driverResource}' PROPERTIES (
+                "type" = "jdbc",
+                "jdbc_url" = "jdbc:mysql://${externalEnvIp}:${mysql_port}",
+                "user" = "root", "password" = "123456",
+                "driver_url" = "${driver_url}", "driver_class" = 
"com.mysql.cj.jdbc.Driver"
+            )"""
+            // ALTER does not read the local jar; the conflicting SQL must be 
rejected first.
+            sql """ALTER RESOURCE '${driverResource}' PROPERTIES ("driver_url" 
= "file:///trusted/mysql.jar")"""
+            sql """GRANT USAGE_PRIV ON RESOURCE '${driverResource}' TO 
'${driverUser}'"""
+            String inheritedDriverSql = insertSql.replace('"driver_url"',
+                    '"resource" = "' + driverResource + '", "Driver_url"')
+                    .replace(driver_url, 'https://example.invalid/driver.jar')
+                    .replace('com.mysql.cj.jdbc.Driver', 
'example.InvalidDriver')
+            connectToDoris(driverUser, "123456", driverUserUrl) {
+                test {
+                    sql """ALTER JOB ${jobName} ${inheritedDriverSql}"""
+                    exception "HTTP/HTTPS CDC driver URLs require ADMIN 
privilege"
+                }
+            }
+        } finally {
+            sql """DROP RESOURCE '${driverResource}'"""
+        }
+        sql """DROP USER '${driverUser}'"""
+
         test {
             sql """
                 ALTER JOB ${jobName}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to