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

liulijia 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 8961fff814f [fix](auth) delete command improperly detects the LOAD 
permission of non target tables (#60837)
8961fff814f is described below

commit 8961fff814fd724ee58b97ae50ca4f97d3056873
Author: Lijia Liu <[email protected]>
AuthorDate: Fri Feb 27 11:01:53 2026 +0800

    [fix](auth) delete command improperly detects the LOAD permission of non 
target tables (#60837)
    
    ### What problem does this PR solve?
    If we have tow tables: `delete_target` and `delete_from`.
    we delete `delete_target` by `delete_from` as follows:
    ```
    create table delete_target (id int)  unique key (id) DISTRIBUTED BY HASH 
(id)  PROPERTIES ("replication_allocation" = "tag.location.default: 1");
    
    insert into delete_target values (1);
    
     create table delete_from (id int, t datetime) PROPERTIES 
("replication_allocation" = "tag.location.default: 1");
    
     insert into delete_from values (1, '2024-01-01 00:00:00');
     insert into delete_from select * from  delete_from; -- 2
     insert into delete_from select * from  delete_from; -- 4
     insert into delete_from select * from  delete_from; -- 8
     insert into delete_from select * from  delete_from; -- 16
     insert into delete_from select * from  delete_from; -- 32
     insert into delete_from select * from  delete_from; -- 64
    analyze table delete_from;
    -- wait some time for tablet report. fe will detect delete_from has more 
rows than delete_target. fe will choose delete_from ritht anti join 
delete_target
    create user delete_test;
    GRANT SELECT_PRIV ON test.delete_from to delete_test;
    
    GRANT LOAD_PRIV ON test.delete_target to delete_test;
    
    SET PASSWORD FOR delete_test = PASSWORD('123');
    
    mysql -h 127.0.0.1 -u delete_test -p123 -P 9030 -A -c  test
    
    delete from delete_target a where not EXISTS ( SELECT 1 FROM delete_from  
WHERE id = a.id and t >'2023-01-01 00:00' );
    
    ```
    The error is:
    ```
    LOAD command denied to user 'delete_test'@'172.19.0.1' for table 'test: 
delete_from'
    ```
---
 .../trees/plans/commands/DeleteFromCommand.java    | 53 +++++++-------
 .../auth_call/test_dml_delete_table_auth.groovy    | 81 +++++++++++++++++++++-
 2 files changed, 108 insertions(+), 26 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DeleteFromCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DeleteFromCommand.java
index bc2bba1b325..04e155b29fc 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DeleteFromCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DeleteFromCommand.java
@@ -36,6 +36,7 @@ import org.apache.doris.catalog.PartitionType;
 import org.apache.doris.catalog.TableIf;
 import org.apache.doris.common.Config;
 import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
 import org.apache.doris.common.util.Util;
 import org.apache.doris.mysql.privilege.PrivPredicate;
 import org.apache.doris.nereids.CascadesContext;
@@ -149,36 +150,27 @@ public class DeleteFromCommand extends Command implements 
ForwardWithSync, Expla
                     .getDeleteHandler().processEmptyRelation(ctx.getState());
             return;
         }
+        OlapTable olapTable = getTargetTable(ctx);
+
+        // check auth
+        if (!Env.getCurrentEnv().getAccessManager()
+                .checkTblPriv(ConnectContext.get(), 
olapTable.getDatabase().getCatalog().getName(),
+                        olapTable.getDatabase().getFullName(), 
olapTable.getName(), PrivPredicate.LOAD)) {
+            
ErrorReport.reportAnalysisException(ErrorCode.ERR_TABLEACCESS_DENIED_ERROR, 
"LOAD",
+                    ConnectContext.get().getQualifiedUser(), 
ConnectContext.get().getRemoteIP(),
+                    olapTable.getDatabase().getFullName() + "." + 
Util.getTempTableDisplayName(olapTable.getName()));
+        }
+
         Optional<PhysicalFilter<?>> optFilter = (planner.getPhysicalPlan()
                 
.<PhysicalFilter<?>>collect(PhysicalFilter.class::isInstance)).stream()
                 .findAny();
-        Optional<PhysicalOlapScan> optScan = (planner.getPhysicalPlan()
-                
.<PhysicalOlapScan>collect(PhysicalOlapScan.class::isInstance)).stream()
-                .findAny();
-        Optional<UnboundRelation> optRelation = (logicalQuery
-                
.<UnboundRelation>collect(UnboundRelation.class::isInstance)).stream()
-                .findAny();
         Preconditions.checkArgument(optFilter.isPresent(), "delete command 
must contain filter");
-        Preconditions.checkArgument(optScan.isPresent(), "delete command could 
be only used on olap table");
-        Preconditions.checkArgument(optRelation.isPresent(), "delete command 
could be only used on olap table");
-        PhysicalOlapScan scan = optScan.get();
-        UnboundRelation relation = optRelation.get();
         PhysicalFilter<?> filter = optFilter.get();
 
-        if (!Env.getCurrentEnv().getAccessManager()
-                .checkTblPriv(ConnectContext.get(), 
scan.getDatabase().getCatalog().getName(),
-                        scan.getDatabase().getFullName(),
-                        scan.getTable().getName(), PrivPredicate.LOAD)) {
-            String message = 
ErrorCode.ERR_TABLEACCESS_DENIED_ERROR.formatErrorMsg("LOAD",
-                    ConnectContext.get().getQualifiedUser(), 
ConnectContext.get().getRemoteIP(),
-                    scan.getDatabase().getFullName() + ": " + 
Util.getTempTableDisplayName(scan.getTable().getName()));
-            throw new AnalysisException(message);
-        }
-
         // predicate check
-        OlapTable olapTable = scan.getTable();
         Set<String> columns = 
olapTable.getFullSchema().stream().map(Column::getName).collect(Collectors.toSet());
         try {
+            // treat sql as simple `delete from t where keyC = ...`
             Plan plan = planner.getPhysicalPlan();
             checkSubQuery(plan);
             for (Expression conjunct : filter.getConjuncts()) {
@@ -192,14 +184,16 @@ public class DeleteFromCommand extends Command implements 
ForwardWithSync, Expla
                         logicalQuery, Optional.empty()).run(ctx, executor);
                 return;
             } catch (Exception e2) {
+                LOG.warn("delete from command failed", e2);
                 throw e;
             }
         }
 
+        // if table's enable_mow_light_delete is false, use 
`DeleteFromUsingCommand`
         if (olapTable.getKeysType() == KeysType.UNIQUE_KEYS && 
olapTable.getEnableUniqueKeyMergeOnWrite()
                 && !olapTable.getEnableMowLightDelete()) {
-            new DeleteFromUsingCommand(nameParts, tableAlias, isTempPart, 
partitions,
-                    logicalQuery, Optional.empty()).run(ctx, executor);
+            new DeleteFromUsingCommand(nameParts, tableAlias, isTempPart, 
partitions, logicalQuery,
+                    Optional.empty()).run(ctx, executor);
             return;
         }
 
@@ -225,6 +219,17 @@ public class DeleteFromCommand extends Command implements 
ForwardWithSync, Expla
             throw new AnalysisException("delete all rows is forbidden 
temporary.");
         }
 
+        Optional<UnboundRelation> optRelation = (logicalQuery
+                
.<UnboundRelation>collect(UnboundRelation.class::isInstance)).stream()
+                .findAny();
+        Optional<PhysicalOlapScan> optScan = (planner.getPhysicalPlan()
+                
.<PhysicalOlapScan>collect(PhysicalOlapScan.class::isInstance)).stream()
+                .findAny();
+        Preconditions.checkArgument(optRelation.isPresent(), "delete command 
must apply to one table");
+        Preconditions.checkArgument(optScan.isPresent(), "delete command could 
be only used on olap table");
+        // prune partitions
+        PhysicalOlapScan scan = optScan.get();
+        UnboundRelation relation = optRelation.get();
         ArrayList<String> partitionNames = 
Lists.newArrayList(relation.getPartNames());
         List<Partition> selectedPartitions = getSelectedPartitions(olapTable, 
filter, scan, partitionNames);
 
@@ -459,7 +464,7 @@ public class DeleteFromCommand extends Command implements 
ForwardWithSync, Expla
         List<String> qualifiedTableName = RelationUtil.getQualifierName(ctx, 
nameParts);
         TableIf table = RelationUtil.getTable(qualifiedTableName, 
ctx.getEnv(), Optional.empty());
         if (!(table instanceof OlapTable)) {
-            throw new AnalysisException("table must be olapTable in delete 
command");
+            throw new AnalysisException("delete command could be only used on 
olap table");
         }
         return ((OlapTable) table);
     }
diff --git a/regression-test/suites/auth_call/test_dml_delete_table_auth.groovy 
b/regression-test/suites/auth_call/test_dml_delete_table_auth.groovy
index bde3e14d542..06215b8cacd 100644
--- a/regression-test/suites/auth_call/test_dml_delete_table_auth.groovy
+++ b/regression-test/suites/auth_call/test_dml_delete_table_auth.groovy
@@ -24,6 +24,7 @@ suite("test_dml_delete_table_auth","p0,auth_call") {
     String pwd = 'C123_567p'
     String dbName = 'test_dml_delete_table_auth_db'
     String tableName = 'test_dml_delete_table_auth_tb'
+    String fromTableName = 'test_dml_delete_table_auth_from_tb'
 
     try_sql("DROP USER ${user}")
     try_sql """drop database if exists ${dbName}"""
@@ -42,9 +43,11 @@ suite("test_dml_delete_table_auth","p0,auth_call") {
                 id BIGINT,
                 username VARCHAR(20)
             )
+            UNIQUE KEY(`id`)
             DISTRIBUTED BY HASH(id) BUCKETS 2
             PROPERTIES (
-                "replication_num" = "1"
+                "replication_num" = "1",
+                "enable_mow_light_delete" = "true"
             );"""
     sql """
         insert into ${dbName}.`${tableName}` values 
@@ -53,6 +56,46 @@ suite("test_dml_delete_table_auth","p0,auth_call") {
         (3, "333");
         """
 
+    sql """create table ${dbName}.${fromTableName} (
+                id BIGINT,
+                username VARCHAR(20)
+            )
+            DISTRIBUTED BY HASH(id) BUCKETS 2
+            PROPERTIES (
+                "replication_num" = "1"
+            );"""
+    sql """
+        insert into ${dbName}.`${fromTableName}` values 
+        (1, "111"),
+        (2, "222"),
+        (3, "333");
+        """
+    sql """
+        insert into ${dbName}.`${fromTableName}` select * from 
${dbName}.${fromTableName};
+        """
+    sql """
+        insert into ${dbName}.`${fromTableName}` select * from 
${dbName}.${fromTableName};
+        """
+    sql """
+        insert into ${dbName}.`${fromTableName}` select * from 
${dbName}.${fromTableName};
+        """
+    sql """
+        insert into ${dbName}.`${fromTableName}` select * from 
${dbName}.${fromTableName};
+        """
+    sql """
+        insert into ${dbName}.`${fromTableName}` select * from 
${dbName}.${fromTableName};
+        """
+    sql """
+        insert into ${dbName}.`${fromTableName}` select * from 
${dbName}.${fromTableName};
+        """
+    sql """
+        insert into ${dbName}.`${fromTableName}` select * from 
${dbName}.${fromTableName};
+        """
+    sql """
+        analyze table ${dbName}.`${fromTableName}` WITH SYNC;
+        """
+
+    // 1. delete when no privilege  
/////////////////////////////////////////////////
     connect(user, "${pwd}", context.config.jdbcUrl) {
         test {
             sql """DELETE FROM ${dbName}.${tableName} WHERE id = 3;"""
@@ -62,6 +105,25 @@ suite("test_dml_delete_table_auth","p0,auth_call") {
         def del_res = sql """show DELETE from ${dbName}"""
         assertTrue(del_res.size() == 0)
     }
+    Thread.sleep(70000) // wait for row count report of the tables just 
loaded, optimizer will choose right anti join
+    connect(user, "${pwd}", context.config.jdbcUrl) {
+        test {
+            sql """DELETE FROM ${dbName}.${tableName} 
+                        WHERE NOT EXISTS 
+                        (
+                            SELECT 1 FROM ${dbName}.${fromTableName} 
+                            WHERE ${dbName}.${fromTableName}.id = 
${dbName}.${tableName}.id
+                            AND ${dbName}.${fromTableName}.username = '333'
+                        );"""
+            // LOAD command denied to user xx for table '$dbName.$tableName'
+            exception tableName
+        }
+        checkNereidsExecute("show DELETE from ${dbName}")
+        def del_res = sql """show DELETE from ${dbName}"""
+        assertTrue(del_res.size() == 0)
+    }
+
+    // 2. delete when has load privilege  
/////////////////////////////////////////////////
     sql """grant load_priv on ${dbName}.${tableName} to ${user}"""
     connect(user, "${pwd}", context.config.jdbcUrl) {
         sql """DELETE FROM ${dbName}.${tableName} WHERE id = 3;"""
@@ -69,9 +131,23 @@ suite("test_dml_delete_table_auth","p0,auth_call") {
         logger.info("del_res: " + del_res)
         assertTrue(del_res.size() == 1)
     }
-
     def res = sql """select count(*) from ${dbName}.${tableName};"""
     assertTrue(res[0][0] == 2)
+    
+    connect(user, "${pwd}", context.config.jdbcUrl) {
+        sql """DELETE FROM ${dbName}.${tableName} 
+                    WHERE NOT EXISTS 
+                    (
+                        SELECT 1 FROM ${dbName}.${fromTableName} 
+                        WHERE ${dbName}.${fromTableName}.id = 
${dbName}.${tableName}.id
+                        AND ${dbName}.${fromTableName}.username = '111'
+                    );"""
+        def del_res = sql """show DELETE from ${dbName}"""
+        assertTrue(del_res.size() == 1)  // use delete from using
+    }
+
+    def res2 = sql """select count(*) from ${dbName}.${tableName};"""
+    assertTrue(res2[0][0] == 1)
 
     String tableName1 = 'test_dml_delete_table_auth_tb1'
     String tableName2 = 'test_dml_delete_table_auth_tb2'
@@ -139,3 +215,4 @@ suite("test_dml_delete_table_auth","p0,auth_call") {
     sql """drop database if exists ${dbName}"""
     try_sql("DROP USER ${user}")
 }
+


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

Reply via email to