This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.0
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.0 by this push:
new b8ceb5966fc branch-4.0: [fix](auth) delete command improperly detects
the LOAD permission of non target tables #60837 (#60863)
b8ceb5966fc is described below
commit b8ceb5966fc2a01fc8024511a7ba147ac9ae8a1a
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Feb 27 14:13:55 2026 +0800
branch-4.0: [fix](auth) delete command improperly detects the LOAD
permission of non target tables #60837 (#60863)
Cherry-picked from #60837
Co-authored-by: Lijia Liu <[email protected]>
---
.../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]