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]