This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new 3622455da65 branch-4.1: [fix](auth) Add missing privilege checks for
several Nereids commands- #66218 (#68857)
3622455da65 is described below
commit 3622455da65655ec84b559800a01b0f4ac06d919
Author: Calvin Kirs <[email protected]>
AuthorDate: Sat Oct 10 15:40:00 2026 +0800
branch-4.1: [fix](auth) Add missing privilege checks for several Nereids
commands- #66218 (#68857)
### What problem does this PR solve?
Backport #66218 to `branch-4.1` to enforce missing privileges for
Nereids commands. Adapt constraint checks to 4.1's table-owned metadata
and reject changed cascade targets before mutation.
### Release note
Fix missing authorization for constraint, dictionary, recycle-bin,
encryption-key and cloud administration commands.
### Check List (For Author)
- Test: 33 focused FE unit tests and Checkstyle passed. Ported
regression suites were not run locally.
- Behavior changed: Yes. Users without the required privileges receive
an access-denied error.
- Does this need documentation: No (backport).
---
.../trees/plans/commands/AddConstraintCommand.java | 17 +++
.../commands/AdminRotateTdeRootKeyCommand.java | 15 ++
.../commands/AdminSetEncryptionRootKeyCommand.java | 9 ++
.../plans/commands/CancelWarmUpJobCommand.java | 9 ++
.../plans/commands/CreateDictionaryCommand.java | 38 ++++-
.../commands/DropCatalogRecycleBinCommand.java | 15 ++
.../plans/commands/DropConstraintCommand.java | 41 ++++-
.../plans/commands/DropDictionaryCommand.java | 12 +-
.../trees/plans/commands/DropStageCommand.java | 15 ++
.../trees/plans/commands/WarmUpClusterCommand.java | 42 ++++-
.../cloud/WarmUpClusterOnTablesParseTest.java | 6 +
.../commands/ConstraintAuthorizationTest.java | 169 +++++++++++++++++++++
.../auth_call/test_ddl_constraint_auth.groovy | 126 +++++++++++++++
.../auth_call/test_ddl_dictionary_auth.groovy | 106 +++++++++++++
.../suites/auth_call/test_recycle_bin_auth.groovy | 82 ++++++++++
15 files changed, 697 insertions(+), 5 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AddConstraintCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AddConstraintCommand.java
index b7b34ed6185..a73fd38aa20 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AddConstraintCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AddConstraintCommand.java
@@ -17,9 +17,13 @@
package org.apache.doris.nereids.trees.plans.commands;
+import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.TableIf;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.Pair;
import org.apache.doris.common.util.MetaLockUtils;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.NereidsPlanner;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.properties.PhysicalProperties;
@@ -65,10 +69,13 @@ public class AddConstraintCommand extends Command
implements ForwardWithSync {
@Override
public void run(ConnectContext ctx, StmtExecutor executor) throws
Exception {
Pair<ImmutableList<String>, TableIf> columnsAndTable =
extractColumnsAndTable(ctx, constraint.toProject());
+ checkAlterPriv(ctx, columnsAndTable.second);
List<TableIf> tables = Lists.newArrayList(columnsAndTable.second);
Pair<ImmutableList<String>, TableIf> referencedColumnsAndTable = null;
if (constraint.isForeignKey()) {
referencedColumnsAndTable = extractColumnsAndTable(ctx,
constraint.toReferenceProject());
+ // A foreign key also registers a reverse reference on the
referenced table.
+ checkAlterPriv(ctx, referencedColumnsAndTable.second);
tables.add(referencedColumnsAndTable.second);
}
tables.sort((Comparator.comparing(TableIf::getId)));
@@ -88,6 +95,16 @@ public class AddConstraintCommand extends Command implements
ForwardWithSync {
}
}
+ private void checkAlterPriv(ConnectContext ctx, TableIf table)
+ throws org.apache.doris.common.AnalysisException {
+ if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx,
table.getDatabase().getCatalog().getName(),
+ table.getDatabase().getFullName(), table.getName(),
PrivPredicate.ALTER)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_TABLEACCESS_DENIED_ERROR,
"ALTER",
+ ctx.getQualifiedUser(), ctx.getRemoteIP(),
+ table.getDatabase().getFullName() + ": " +
table.getName());
+ }
+ }
+
private Pair<ImmutableList<String>, TableIf>
extractColumnsAndTable(ConnectContext ctx, LogicalPlan plan) {
NereidsPlanner planner = new NereidsPlanner(ctx.getStatementContext());
Plan analyzedPlan = planner.planWithLock(plan, PhysicalProperties.ANY,
ExplainLevel.ANALYZED_PLAN);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminRotateTdeRootKeyCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminRotateTdeRootKeyCommand.java
index 4952c6c586a..469c871f3a9 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminRotateTdeRootKeyCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminRotateTdeRootKeyCommand.java
@@ -17,8 +17,12 @@
package org.apache.doris.nereids.trees.plans.commands;
+import org.apache.doris.catalog.Env;
import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
import org.apache.doris.encryption.KeyManagerInterface;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
import org.apache.doris.qe.ConnectContext;
@@ -52,6 +56,7 @@ public class AdminRotateTdeRootKeyCommand extends Command
implements ForwardWith
@Override
public void run(ConnectContext ctx, StmtExecutor executor) throws
Exception {
+ validate();
KeyManagerInterface keyManager = ctx.getEnv().getKeyManager();
if (keyManager != null) {
keyManager.rotateRootKey(properties);
@@ -60,6 +65,16 @@ public class AdminRotateTdeRootKeyCommand extends Command
implements ForwardWith
}
}
+ /**
+ * validate
+ */
+ public void validate() throws AnalysisException {
+ // check auth
+ if
(!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(),
PrivPredicate.ADMIN)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
"ADMIN");
+ }
+ }
+
@Override
public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
return visitor.visitAdminRotateTdeRootKeyCommand(this, context);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminSetEncryptionRootKeyCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminSetEncryptionRootKeyCommand.java
index 5f98cd83b4c..0e969799c2d 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminSetEncryptionRootKeyCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminSetEncryptionRootKeyCommand.java
@@ -17,9 +17,13 @@
package org.apache.doris.nereids.trees.plans.commands;
+import org.apache.doris.catalog.Env;
import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
import org.apache.doris.encryption.EncryptionKey;
import org.apache.doris.encryption.RootKeyInfo;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
import org.apache.doris.qe.ConnectContext;
@@ -63,6 +67,11 @@ public class AdminSetEncryptionRootKeyCommand extends
Command implements Forward
* validate
*/
public void validate() throws AnalysisException {
+ // check auth
+ if
(!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(),
PrivPredicate.ADMIN)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
"ADMIN");
+ }
+
if (properties == null || properties.isEmpty()) {
throw new AnalysisException("The properties must not be empty");
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CancelWarmUpJobCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CancelWarmUpJobCommand.java
index cfb20910f11..05d27a792a2 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CancelWarmUpJobCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CancelWarmUpJobCommand.java
@@ -17,9 +17,13 @@
package org.apache.doris.nereids.trees.plans.commands;
+import org.apache.doris.catalog.Env;
import org.apache.doris.cloud.catalog.CloudEnv;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.Config;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.analyzer.UnboundSlot;
import org.apache.doris.nereids.trees.expressions.EqualTo;
import org.apache.doris.nereids.trees.expressions.Expression;
@@ -59,6 +63,11 @@ public class CancelWarmUpJobCommand extends Command
implements ForwardWithSync {
* @throws AnalysisException check whether this sql is legal
*/
public void validate(ConnectContext ctx) throws AnalysisException {
+ // check auth
+ if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ctx,
PrivPredicate.ADMIN)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
"ADMIN");
+ }
+
if (!Config.isCloudMode()) {
throw new AnalysisException("The sql is illegal in disk mode ");
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateDictionaryCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateDictionaryCommand.java
index 6b8287200e7..e09d3cf6da7 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateDictionaryCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateDictionaryCommand.java
@@ -18,7 +18,12 @@
package org.apache.doris.nereids.trees.plans.commands;
import org.apache.doris.analysis.StmtType;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
+import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.dictionary.LayoutType;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.commands.info.CreateDictionaryInfo;
@@ -60,12 +65,20 @@ public class CreateDictionaryCommand extends Command
implements ForwardWithSync
}
@Override
- public void run(ConnectContext ctx, StmtExecutor executor) {
+ public void run(ConnectContext ctx, StmtExecutor executor) throws
Exception {
try {
// 1. Validate the dictionary info. names and existence.
createDictionaryInfo.validateAndSet(ctx);
+ } catch (Exception e) {
+ LOG.warn("Failed to create dictionary: {}", e.getMessage());
+ throw new AnalysisException("Failed to create dictionary: " +
e.getMessage());
+ }
+
+ // 2. Check auth. Must run after validateAndSet(), which fills in the
default catalog/db names.
+ checkAuth(ctx);
- // 2. Create dictionary and save it in manager. it will schedule
data load.
+ try {
+ // 3. Create dictionary and save it in manager. it will schedule
data load.
ctx.getEnv().getDictionaryManager().createDictionary(ctx,
createDictionaryInfo);
LOG.info("Created dictionary {} in {} from {}",
createDictionaryInfo.getDictName(),
@@ -75,4 +88,25 @@ public class CreateDictionaryCommand extends Command
implements ForwardWithSync
throw new AnalysisException("Failed to create dictionary: " +
e.getMessage());
}
}
+
+ /**
+ * A dictionary is created in the internal catalog and its data is loaded
by an internal task,
+ * so require CREATE on the dictionary itself and SELECT on the source
table. Without the latter
+ * the dictionary would expose data the creator can not read.
+ */
+ private void checkAuth(ConnectContext ctx) throws
org.apache.doris.common.AnalysisException {
+ if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx,
InternalCatalog.INTERNAL_CATALOG_NAME,
+ createDictionaryInfo.getDbName(),
createDictionaryInfo.getDictName(), PrivPredicate.CREATE)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
"CREATE");
+ }
+
+ String srcCtl = createDictionaryInfo.getSourceCtlName();
+ String srcDb = createDictionaryInfo.getSourceDbName();
+ String srcTbl = createDictionaryInfo.getSourceTableName();
+ if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx, srcCtl,
srcDb, srcTbl,
+ PrivPredicate.SELECT)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_TABLEACCESS_DENIED_ERROR,
"SELECT",
+ ctx.getQualifiedUser(), ctx.getRemoteIP(), srcDb + ": " +
srcTbl);
+ }
+ }
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropCatalogRecycleBinCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropCatalogRecycleBinCommand.java
index eaad053343e..48edc6bc306 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropCatalogRecycleBinCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropCatalogRecycleBinCommand.java
@@ -18,6 +18,9 @@
package org.apache.doris.nereids.trees.plans.commands;
import org.apache.doris.catalog.Env;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
@@ -71,9 +74,21 @@ public class DropCatalogRecycleBinCommand extends Command
implements ForwardWith
@Override
public void run(ConnectContext ctx, StmtExecutor executor) throws
Exception {
+ validate();
Env.getCurrentEnv().dropCatalogRecycleBin(idType, id);
}
+ /**
+ * validate
+ */
+ public void validate() throws org.apache.doris.common.AnalysisException {
+ // Erasing from the recycle bin is irreversible and takes a raw object
id, so it can not be
+ // authorized at db/table level. Restrict it to ADMIN, same as the
other catalog-wide admin ops.
+ if
(!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(),
PrivPredicate.ADMIN)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
"ADMIN");
+ }
+ }
+
@Override
public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
return visitor.visitDropCatalogRecycleBinCommand(this, context);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropConstraintCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropConstraintCommand.java
index 81147411b95..f8cb0373de0 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropConstraintCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropConstraintCommand.java
@@ -17,8 +17,13 @@
package org.apache.doris.nereids.trees.plans.commands;
+import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.TableIf;
import org.apache.doris.catalog.constraint.Constraint;
+import org.apache.doris.catalog.constraint.PrimaryKeyConstraint;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.NereidsPlanner;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.properties.PhysicalProperties;
@@ -31,9 +36,12 @@ import
org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.StmtExecutor;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import java.util.List;
import java.util.Set;
/**
@@ -57,24 +65,55 @@ public class DropConstraintCommand extends Command
implements ForwardWithSync {
@Override
public void run(ConnectContext ctx, StmtExecutor executor) throws
Exception {
TableIf table = extractTable(ctx, plan);
+ checkAlterPriv(ctx, table);
+ List<TableIf> cascadeTables = ImmutableList.of();
+ Constraint constraint;
table.readLock();
try {
- Constraint constraint = table.getConstraintsMapUnsafe().get(name);
+ constraint = table.getConstraintsMapUnsafe().get(name);
if (constraint == null) {
throw new AnalysisException(
String.format("Unknown constraint %s on table %s.",
name, table.getName()));
}
+ if (constraint instanceof PrimaryKeyConstraint) {
+ // The reverse references are updated under the primary
table's write lock.
+ cascadeTables = ((PrimaryKeyConstraint)
constraint).getForeignTables();
+ }
} finally {
table.readUnlock();
}
+ // Dropping a primary key also deletes the foreign keys of its
referencing tables.
+ // Authorize the complete snapshot before any constraint is modified.
+ for (TableIf cascadeTable : cascadeTables) {
+ checkAlterPriv(ctx, cascadeTable);
+ }
+ Set<TableIf> authorizedCascadeTables =
ImmutableSet.copyOf(cascadeTables);
table.writeLock();
try {
+ // Authorization runs outside the table lock. A concurrent
ADD/DROP must not replace
+ // the authorized constraint or add an unauthorized cascade target
before mutation.
+ if (table.getConstraintsMapUnsafe().get(name) != constraint
+ || (constraint instanceof PrimaryKeyConstraint
+ &&
!authorizedCascadeTables.containsAll(((PrimaryKeyConstraint)
constraint).getForeignTables()))) {
+ throw new AnalysisException("Constraint " + name + " on table
" + table.getName()
+ + " changed while checking privileges; retry the
statement");
+ }
table.dropConstraint(name, false);
} finally {
table.writeUnlock();
}
}
+ private void checkAlterPriv(ConnectContext ctx, TableIf table)
+ throws org.apache.doris.common.AnalysisException {
+ if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx,
table.getDatabase().getCatalog().getName(),
+ table.getDatabase().getFullName(), table.getName(),
PrivPredicate.ALTER)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_TABLEACCESS_DENIED_ERROR,
"ALTER",
+ ctx.getQualifiedUser(), ctx.getRemoteIP(),
+ table.getDatabase().getFullName() + ": " +
table.getName());
+ }
+ }
+
private TableIf extractTable(ConnectContext ctx, LogicalPlan plan) {
NereidsPlanner planner = new NereidsPlanner(ctx.getStatementContext());
Plan analyzedPlan = planner.planWithLock(plan, PhysicalProperties.ANY,
ExplainLevel.ANALYZED_PLAN);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropDictionaryCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropDictionaryCommand.java
index 23f58cf9664..3e7a564e58b 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropDictionaryCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropDictionaryCommand.java
@@ -18,6 +18,11 @@
package org.apache.doris.nereids.trees.plans.commands;
import org.apache.doris.analysis.StmtType;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
+import org.apache.doris.datasource.InternalCatalog;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
@@ -57,10 +62,15 @@ public class DropDictionaryCommand extends Command
implements ForwardWithSync {
}
@Override
- public void run(ConnectContext ctx, StmtExecutor executor) {
+ public void run(ConnectContext ctx, StmtExecutor executor) throws
Exception {
if (dbName == null) { // use current database
dbName = ctx.getDatabase();
}
+ // check auth. dictionaries always live in the internal catalog.
+ if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx,
InternalCatalog.INTERNAL_CATALOG_NAME,
+ dbName, dictName, PrivPredicate.DROP)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
"DROP");
+ }
try {
ctx.getEnv().getDictionaryManager().dropDictionary(ctx, dbName,
dictName, ifExists);
} catch (Exception e) {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropStageCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropStageCommand.java
index bd54e875456..e38b40ffadb 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropStageCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropStageCommand.java
@@ -20,6 +20,10 @@ package org.apache.doris.nereids.trees.plans.commands;
import org.apache.doris.analysis.StmtType;
import org.apache.doris.catalog.Env;
import org.apache.doris.cloud.catalog.CloudEnv;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
+import org.apache.doris.common.UserException;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
import org.apache.doris.qe.ConnectContext;
@@ -45,9 +49,20 @@ public class DropStageCommand extends Command implements
ForwardWithSync {
@Override
public void run(ConnectContext ctx, StmtExecutor executor) throws
Exception {
+ validate();
((CloudEnv) Env.getCurrentEnv()).dropStage(this);
}
+ /**
+ * validate
+ */
+ public void validate() throws UserException {
+ // check auth. keep it aligned with CreateStageCommand.
+ if
(!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(),
PrivPredicate.ADMIN)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
"ADMIN");
+ }
+ }
+
@Override
public StmtType stmtType() {
return StmtType.DROP;
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/WarmUpClusterCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/WarmUpClusterCommand.java
index 7d01b337913..74fe129b7b0 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/WarmUpClusterCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/WarmUpClusterCommand.java
@@ -17,6 +17,7 @@
package org.apache.doris.nereids.trees.plans.commands;
+import org.apache.doris.analysis.ResourceTypeEnum;
import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.Database;
import org.apache.doris.catalog.Env;
@@ -32,7 +33,9 @@ import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.Triple;
import org.apache.doris.common.UserException;
+import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.info.TableNameInfo;
+import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.commands.info.WarmUpItem;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
@@ -139,6 +142,14 @@ public class WarmUpClusterCommand extends Command
implements ForwardWithSync {
handleWarmUp(ctx, executor);
}
+ private void checkComputeGroupUsage(ConnectContext ctx, String
computeGroup) throws AnalysisException {
+ if
(!Env.getCurrentEnv().getAccessManager().checkCloudPriv(ctx.getCurrentUserIdentity(),
+ computeGroup, PrivPredicate.USAGE, ResourceTypeEnum.CLUSTER)) {
+ throw new AnalysisException("USAGE denied to user '" +
ctx.getQualifiedUser() + "'@'"
+ + ctx.getRemoteIP() + "' for compute group '" +
computeGroup + "'");
+ }
+ }
+
private void checkWarmupCgs(CloudSystemInfoService cloudSys) throws
AnalysisException {
if (!Strings.isNullOrEmpty(srcCluster)) {
CloudComputeGroupMeta srcCg =
cloudSys.getComputeGroupByName(srcCluster);
@@ -180,6 +191,24 @@ public class WarmUpClusterCommand extends Command
implements ForwardWithSync {
throw new UserException("The sql is just support in cloud mode");
}
+ boolean hasOnTablesRules = onTablesRules != null &&
!onTablesRules.isEmpty();
+
+ // check auth. warming up moves data between compute groups, so
require USAGE on both ends
+ // instead of global ADMIN. Keep it aligned with
UseCloudClusterCommand.
+ checkComputeGroupUsage(connectContext, dstCluster);
+ if (!Strings.isNullOrEmpty(srcCluster)) {
+ checkComputeGroupUsage(connectContext, srcCluster);
+ }
+ if (hasOnTablesRules) {
+ // An ON TABLES job selects tables by pattern over the whole
internal catalog and keeps
+ // re-matching in the background
(CacheHotspotManager.refreshAllTableFilters), so there is
+ // no fixed table set to authorize here and no identity to
re-authorize later matches with.
+ // Require global ADMIN until the job carries its submitter.
+ if
(!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(connectContext,
PrivPredicate.ADMIN)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
"ADMIN");
+ }
+ }
+
CloudSystemInfoService cloudSys = ((CloudSystemInfoService)
Env.getCurrentSystemInfo());
if (!cloudSys.containClusterName(dstCluster)) {
throw new AnalysisException("The dstClusterName " + dstCluster + "
doesn't exist");
@@ -204,7 +233,6 @@ public class WarmUpClusterCommand extends Command
implements ForwardWithSync {
+ " is same with srcClusterName: " + srcCluster);
}
- boolean hasOnTablesRules = onTablesRules != null &&
!onTablesRules.isEmpty();
if (hasOnTablesRules && isWarmUpWithTable) {
throw new AnalysisException("ON TABLES clause cannot be used with
WITH TABLE warmup");
}
@@ -218,6 +246,18 @@ public class WarmUpClusterCommand extends Command
implements ForwardWithSync {
if (Strings.isNullOrEmpty(dbName)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_NO_DB_ERROR, dbName);
}
+ // Warm up only ever resolves and warms the internal catalog
(see the lookup below and
+ // the (db, table, partition) triple stored for the job), so
authorize the internal
+ // name rather than the catalog written in the SQL. Otherwise
SELECT on
+ // 'ext_ctl.db.tbl' would authorize warming up
'internal.db.tbl'.
+ // Checked before the lookup so a denied user learns nothing
about what exists.
+ if
(!Env.getCurrentEnv().getAccessManager().checkTblPriv(connectContext,
+ InternalCatalog.INTERNAL_CATALOG_NAME, dbName,
tableNameInfo.getTbl(),
+ PrivPredicate.SELECT)) {
+
ErrorReport.reportAnalysisException(ErrorCode.ERR_TABLEACCESS_DENIED_ERROR,
"SELECT",
+ connectContext.getQualifiedUser(),
connectContext.getRemoteIP(),
+ dbName + ": " + tableNameInfo.getTbl());
+ }
Database db =
Env.getCurrentInternalCatalog().getDbNullable(dbName);
if (db == null) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_NO_DB_ERROR, dbName);
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/cloud/WarmUpClusterOnTablesParseTest.java
b/fe/fe-core/src/test/java/org/apache/doris/cloud/WarmUpClusterOnTablesParseTest.java
index ceee9533386..8e678302d34 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/cloud/WarmUpClusterOnTablesParseTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/cloud/WarmUpClusterOnTablesParseTest.java
@@ -17,6 +17,7 @@
package org.apache.doris.cloud;
+import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Env;
import org.apache.doris.cloud.OnTablesFilter.TableFilterRule;
import org.apache.doris.cloud.OnTablesFilter.TableFilterRule.RuleType;
@@ -66,6 +67,11 @@ public class WarmUpClusterOnTablesParseTest {
originalSystemInfo = getField(env, Env.class, "systemInfo");
connectContext = new ConnectContext();
connectContext.setEnv(env);
+ // this test covers ON TABLES parsing and validation, not
authorization, so give the
+ // context an identity and let it bypass the privilege checks in
validate()
+ connectContext.setCurrentUserIdentity(UserIdentity.ROOT);
+ connectContext.setNoAuth(true);
+ connectContext.setSkipAuth(true);
connectContext.setThreadLocalInfo();
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ConstraintAuthorizationTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ConstraintAuthorizationTest.java
new file mode 100644
index 00000000000..bdb13bbc9be
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ConstraintAuthorizationTest.java
@@ -0,0 +1,169 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.trees.plans.commands;
+
+import org.apache.doris.analysis.UserIdentity;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.TableIf;
+import org.apache.doris.common.AnalysisException;
+import org.apache.doris.mysql.privilege.AccessControllerManager;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.nereids.parser.NereidsParser;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.StmtExecutor;
+import org.apache.doris.utframe.TestWithFeService;
+
+import mockit.Invocation;
+import mockit.Mock;
+import mockit.MockUp;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+class ConstraintAuthorizationTest extends TestWithFeService {
+ private static final String DB = "constraint_auth_test";
+
+ @Override
+ protected void runBeforeAll() throws Exception {
+ createDatabase(DB);
+ connectContext.setDatabase(DB);
+ for (String table : new String[] {"unique_table", "fk_parent",
"fk_child", "cascade_parent", "cascade_child",
+ "race_parent", "race_child", "race_new_child"}) {
+ createTable("CREATE TABLE " + table + " (id INT, parent_id INT) "
+ + "DISTRIBUTED BY HASH(id) BUCKETS 1 PROPERTIES
('replication_num'='1')");
+ }
+ }
+
+ @Test
+ void testSelectCannotAddOrDropConstraint() throws Exception {
+ ConnectContext user = createLimitedUser("constraint_select_user");
+ String addUnique = "ALTER TABLE unique_table ADD CONSTRAINT uk UNIQUE
(id)";
+ assertDenied(user, addUnique);
+ assertDenied(user, "ALTER TABLE unique_table ADD CONSTRAINT pk PRIMARY
KEY (id)");
+
Assertions.assertTrue(table("unique_table").getConstraintsMap().isEmpty());
+
+ runAs(connectContext, addUnique);
+ assertDenied(user, "ALTER TABLE unique_table DROP CONSTRAINT uk");
+ assertDenied(user, "ALTER TABLE unique_table DROP CONSTRAINT
missing_constraint");
+
Assertions.assertTrue(table("unique_table").getConstraintsMap().containsKey("uk"));
+
+ grantPriv("GRANT ALTER_PRIV ON " + DB + ".unique_table TO
'constraint_select_user'@'%'");
+ runAs(user, "ALTER TABLE unique_table DROP CONSTRAINT uk");
+ runAs(user, addUnique);
+
Assertions.assertTrue(table("unique_table").getConstraintsMap().containsKey("uk"));
+ }
+
+ @Test
+ void testForeignKeyRequiresAlterOnReferencedTable() throws Exception {
+ runAs(connectContext, "ALTER TABLE fk_parent ADD CONSTRAINT pk PRIMARY
KEY (id)");
+ ConnectContext user = createLimitedUser("constraint_fk_user");
+ grantPriv("GRANT ALTER_PRIV ON " + DB + ".fk_child TO
'constraint_fk_user'@'%'");
+ String addForeignKey = "ALTER TABLE fk_child ADD CONSTRAINT fk FOREIGN
KEY (parent_id) "
+ + "REFERENCES fk_parent(id)";
+ assertDenied(user, addForeignKey);
+ Assertions.assertTrue(table("fk_child").getConstraintsMap().isEmpty());
+
+ grantPriv("GRANT ALTER_PRIV ON " + DB + ".fk_parent TO
'constraint_fk_user'@'%'");
+ runAs(user, addForeignKey);
+
Assertions.assertTrue(table("fk_child").getConstraintsMap().containsKey("fk"));
+ }
+
+ @Test
+ void testPrimaryKeyCascadeRequiresAlterOnReferencingTable() throws
Exception {
+ runAs(connectContext, "ALTER TABLE cascade_parent ADD CONSTRAINT pk
PRIMARY KEY (id)");
+ runAs(connectContext, "ALTER TABLE cascade_child ADD CONSTRAINT fk
FOREIGN KEY (parent_id) "
+ + "REFERENCES cascade_parent(id)");
+ ConnectContext user = createLimitedUser("constraint_cascade_user");
+ grantPriv("GRANT ALTER_PRIV ON " + DB + ".cascade_parent TO
'constraint_cascade_user'@'%'");
+
+ String dropPrimaryKey = "ALTER TABLE cascade_parent DROP CONSTRAINT
pk";
+ assertDenied(user, dropPrimaryKey);
+
Assertions.assertTrue(table("cascade_parent").getConstraintsMap().containsKey("pk"));
+
Assertions.assertTrue(table("cascade_child").getConstraintsMap().containsKey("fk"));
+
+ grantPriv("GRANT ALTER_PRIV ON " + DB + ".cascade_child TO
'constraint_cascade_user'@'%'");
+ runAs(user, dropPrimaryKey);
+
Assertions.assertTrue(table("cascade_parent").getConstraintsMap().isEmpty());
+
Assertions.assertTrue(table("cascade_child").getConstraintsMap().isEmpty());
+ }
+
+ @Test
+ void testConcurrentForeignKeyCannotBypassCascadeAuthorization() throws
Exception {
+ runAs(connectContext, "ALTER TABLE race_parent ADD CONSTRAINT pk
PRIMARY KEY (id)");
+ runAs(connectContext, "ALTER TABLE race_child ADD CONSTRAINT fk
FOREIGN KEY (parent_id) "
+ + "REFERENCES race_parent(id)");
+ ConnectContext user = createLimitedUser("constraint_race_user");
+ grantPriv("GRANT ALTER_PRIV ON " + DB + ".race_parent TO
'constraint_race_user'@'%'");
+ grantPriv("GRANT ALTER_PRIV ON " + DB + ".race_child TO
'constraint_race_user'@'%'");
+
+ AtomicBoolean added = new AtomicBoolean();
+ new MockUp<AccessControllerManager>() {
+ @Mock
+ public boolean checkTblPriv(Invocation invocation, ConnectContext
ctx, String ctl,
+ String db, String tableName, PrivPredicate wanted) throws
Exception {
+ if (ctx == user && tableName.equals("race_child") && wanted ==
PrivPredicate.ALTER
+ && added.compareAndSet(false, true)) {
+ runAs(connectContext, "ALTER TABLE race_new_child ADD
CONSTRAINT fk FOREIGN KEY (parent_id) "
+ + "REFERENCES race_parent(id)");
+ }
+ return invocation.proceed();
+ }
+ };
+
+ org.apache.doris.nereids.exceptions.AnalysisException error =
Assertions.assertThrows(
+ org.apache.doris.nereids.exceptions.AnalysisException.class,
+ () -> runAs(user, "ALTER TABLE race_parent DROP CONSTRAINT
pk"));
+ Assertions.assertTrue(added.get());
+ Assertions.assertTrue(error.getMessage().contains("changed while
checking privileges"));
+
Assertions.assertTrue(table("race_parent").getConstraintsMap().containsKey("pk"));
+
Assertions.assertTrue(table("race_child").getConstraintsMap().containsKey("fk"));
+
Assertions.assertTrue(table("race_new_child").getConstraintsMap().containsKey("fk"));
+ }
+
+ private ConnectContext createLimitedUser(String name) throws Exception {
+ addUser(name, false);
+ grantPriv("GRANT SELECT_PRIV ON " + DB + ".* TO '" + name + "'@'%'");
+ ConnectContext user = createDefaultCtx();
+
user.setCurrentUserIdentity(UserIdentity.createAnalyzedUserIdentWithIp(name,
"%"));
+ user.setDatabase(DB);
+ user.setNoAuth(false);
+ user.setSkipAuth(false);
+ connectContext.setThreadLocalInfo();
+ return user;
+ }
+
+ private TableIf table(String name) throws Exception {
+ return
Env.getCurrentInternalCatalog().getDbOrMetaException(DB).getTableOrMetaException(name);
+ }
+
+ private void assertDenied(ConnectContext user, String sql) {
+ AnalysisException error =
Assertions.assertThrows(AnalysisException.class, () -> runAs(user, sql));
+ Assertions.assertTrue(error.getMessage().contains("denied"),
error.getMessage());
+ }
+
+ private void runAs(ConnectContext user, String sql) throws Exception {
+ user.setThreadLocalInfo();
+ try {
+ Command command = (Command) new NereidsParser().parseSingle(sql);
+ command.run(user, new StmtExecutor(user, sql));
+ } finally {
+ connectContext.setThreadLocalInfo();
+ }
+ }
+}
diff --git a/regression-test/suites/auth_call/test_ddl_constraint_auth.groovy
b/regression-test/suites/auth_call/test_ddl_constraint_auth.groovy
new file mode 100644
index 00000000000..62986d20c6b
--- /dev/null
+++ b/regression-test/suites/auth_call/test_ddl_constraint_auth.groovy
@@ -0,0 +1,126 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_ddl_constraint_auth", "p0,auth_call") {
+ String user = 'test_ddl_constraint_auth_user'
+ String pwd = 'C123_567p'
+ String dbName = 'test_ddl_constraint_auth_db'
+ String tableName = 'test_ddl_constraint_auth_tb'
+ String constraintName = 'test_ddl_constraint_auth_uk'
+
+ try_sql("DROP USER ${user}")
+ try_sql """drop database if exists ${dbName}"""
+ sql """CREATE USER '${user}' IDENTIFIED BY '${pwd}'"""
+ sql """grant select_priv on regression_test to ${user}"""
+ //cloud-mode
+ if (isCloudMode()) {
+ def clusters = sql " SHOW CLUSTERS; "
+ assertTrue(!clusters.isEmpty())
+ def validCluster = clusters[0][0]
+ sql """GRANT USAGE_PRIV ON CLUSTER `${validCluster}` TO ${user}""";
+ }
+
+ sql """create database ${dbName}"""
+ sql """
+ CREATE TABLE IF NOT EXISTS ${dbName}.${tableName} (
+ id BIGINT,
+ username VARCHAR(30)
+ )
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES ("replication_num" = "1");
+ """
+
+ // SELECT alone must not be enough to change constraints
+ sql """grant SELECT_PRIV on ${dbName}.${tableName} to ${user}"""
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ sql """use ${dbName}"""
+ test {
+ sql """ALTER TABLE ${tableName} ADD CONSTRAINT ${constraintName}
UNIQUE (id)"""
+ exception "denied"
+ }
+ }
+
+ sql """use ${dbName}"""
+ sql """ALTER TABLE ${tableName} ADD CONSTRAINT ${constraintName} UNIQUE
(id)"""
+
+ // Dropping a constraint also requires ALTER on the target table.
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ sql """use ${dbName}"""
+ test {
+ sql """ALTER TABLE ${tableName} DROP CONSTRAINT
${constraintName}"""
+ exception "denied"
+ }
+ }
+ def constraints = sql """SHOW CONSTRAINTS FROM ${dbName}.${tableName}"""
+ assertTrue(constraints.size() == 1)
+
+ sql """grant ALTER_PRIV on ${dbName}.${tableName} to ${user}"""
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ sql """use ${dbName}"""
+ sql """ALTER TABLE ${tableName} DROP CONSTRAINT ${constraintName}"""
+ sql """ALTER TABLE ${tableName} ADD CONSTRAINT ${constraintName}
UNIQUE (id)"""
+ }
+ constraints = sql """SHOW CONSTRAINTS FROM ${dbName}.${tableName}"""
+ assertTrue(constraints.size() == 1)
+
+ // dropping a primary key cascades into the foreign keys of every
referencing table, so ALTER on
+ // the referencing tables is required as well
+ String pkTable = 'test_ddl_constraint_auth_pk_tb'
+ String fkTable = 'test_ddl_constraint_auth_fk_tb'
+ String pkName = 'test_ddl_constraint_auth_pk'
+ String fkName = 'test_ddl_constraint_auth_fk'
+ sql """
+ CREATE TABLE IF NOT EXISTS ${dbName}.${pkTable} (
+ id BIGINT NOT NULL,
+ username VARCHAR(30)
+ )
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES ("replication_num" = "1");
+ """
+ sql """
+ CREATE TABLE IF NOT EXISTS ${dbName}.${fkTable} (
+ id BIGINT NOT NULL,
+ pk_id BIGINT NOT NULL
+ )
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES ("replication_num" = "1");
+ """
+ sql """ALTER TABLE ${dbName}.${pkTable} ADD CONSTRAINT ${pkName} PRIMARY
KEY (id)"""
+ sql """ALTER TABLE ${dbName}.${fkTable} ADD CONSTRAINT ${fkName} FOREIGN
KEY (pk_id) REFERENCES ${pkTable}(id)"""
+
+ sql """grant ALTER_PRIV on ${dbName}.${pkTable} to ${user}"""
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ sql """use ${dbName}"""
+ test {
+ sql """ALTER TABLE ${pkTable} DROP CONSTRAINT ${pkName}"""
+ exception "denied"
+ }
+ }
+ def fkConstraints = sql """SHOW CONSTRAINTS FROM ${dbName}.${fkTable}"""
+ assertTrue(fkConstraints.size() == 1)
+
+ sql """grant ALTER_PRIV on ${dbName}.${fkTable} to ${user}"""
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ sql """use ${dbName}"""
+ sql """ALTER TABLE ${pkTable} DROP CONSTRAINT ${pkName}"""
+ }
+ fkConstraints = sql """SHOW CONSTRAINTS FROM ${dbName}.${fkTable}"""
+ assertTrue(fkConstraints.isEmpty())
+
+ sql """drop database if exists ${dbName}"""
+ try_sql("DROP USER ${user}")
+}
diff --git a/regression-test/suites/auth_call/test_ddl_dictionary_auth.groovy
b/regression-test/suites/auth_call/test_ddl_dictionary_auth.groovy
new file mode 100644
index 00000000000..394969acf73
--- /dev/null
+++ b/regression-test/suites/auth_call/test_ddl_dictionary_auth.groovy
@@ -0,0 +1,106 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_ddl_dictionary_auth", "p0,auth_call") {
+ String user = 'test_ddl_dictionary_auth_user'
+ String pwd = 'C123_567p'
+ String dbName = 'test_ddl_dictionary_auth_db'
+ String tableName = 'test_ddl_dictionary_auth_tb'
+ String dictName = 'test_ddl_dictionary_auth_dict'
+
+ try_sql("DROP USER ${user}")
+ try_sql """drop database if exists ${dbName}"""
+ sql """CREATE USER '${user}' IDENTIFIED BY '${pwd}'"""
+ sql """grant select_priv on regression_test to ${user}"""
+ //cloud-mode
+ if (isCloudMode()) {
+ def clusters = sql " SHOW CLUSTERS; "
+ assertTrue(!clusters.isEmpty())
+ def validCluster = clusters[0][0]
+ sql """GRANT USAGE_PRIV ON CLUSTER `${validCluster}` TO ${user}""";
+ }
+
+ sql """create database ${dbName}"""
+ sql """
+ CREATE TABLE IF NOT EXISTS ${dbName}.${tableName} (
+ id BIGINT,
+ username VARCHAR(30)
+ )
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES ("replication_num" = "1");
+ """
+ sql """insert into ${dbName}.${tableName} values(1, 'doris')"""
+
+ def createDictSql = """
+ CREATE DICTIONARY ${dbName}.${dictName} USING ${dbName}.${tableName}
+ (
+ id KEY,
+ username VALUE
+ )
+ LAYOUT(HASH_MAP)
+ PROPERTIES('data_lifetime'='600');
+ """
+
+ // no privilege at all on the database
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ test {
+ sql createDictSql
+ exception "denied"
+ }
+ test {
+ sql """DROP DICTIONARY ${dbName}.${dictName};"""
+ exception "denied"
+ }
+ }
+
+ // CREATE on the database is not enough: the dictionary load task runs as
ADMIN, so the
+ // creator must also be able to read the source table itself.
+ sql """grant CREATE_PRIV on ${dbName}.* to ${user}"""
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ test {
+ sql createDictSql
+ exception "denied"
+ }
+ }
+
+ sql """grant SELECT_PRIV on ${dbName}.${tableName} to ${user}"""
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ sql createDictSql
+ }
+ sql """use ${dbName}"""
+ def dictRes = sql """SHOW DICTIONARIES"""
+ assertTrue(dictRes.size() == 1)
+
+ // dropping needs DROP on the database
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ test {
+ sql """DROP DICTIONARY ${dbName}.${dictName};"""
+ exception "denied"
+ }
+ }
+
+ sql """grant DROP_PRIV on ${dbName}.* to ${user}"""
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ sql """DROP DICTIONARY ${dbName}.${dictName};"""
+ }
+ sql """use ${dbName}"""
+ dictRes = sql """SHOW DICTIONARIES"""
+ assertTrue(dictRes.size() == 0)
+
+ sql """drop database if exists ${dbName}"""
+ try_sql("DROP USER ${user}")
+}
diff --git a/regression-test/suites/auth_call/test_recycle_bin_auth.groovy
b/regression-test/suites/auth_call/test_recycle_bin_auth.groovy
new file mode 100644
index 00000000000..ea99772dabf
--- /dev/null
+++ b/regression-test/suites/auth_call/test_recycle_bin_auth.groovy
@@ -0,0 +1,82 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_recycle_bin_auth", "p0,auth_call") {
+ String user = 'test_recycle_bin_auth_user'
+ String pwd = 'C123_567p'
+ String dbName = 'test_recycle_bin_auth_db'
+ String tableName = 'test_recycle_bin_auth_tb'
+
+ try_sql("DROP USER ${user}")
+ try_sql """drop database if exists ${dbName}"""
+ sql """CREATE USER '${user}' IDENTIFIED BY '${pwd}'"""
+ sql """grant select_priv on regression_test to ${user}"""
+ //cloud-mode
+ if (isCloudMode()) {
+ def clusters = sql " SHOW CLUSTERS; "
+ assertTrue(!clusters.isEmpty())
+ def validCluster = clusters[0][0]
+ sql """GRANT USAGE_PRIV ON CLUSTER `${validCluster}` TO ${user}""";
+ }
+
+ sql """create database ${dbName}"""
+ sql """
+ CREATE TABLE IF NOT EXISTS ${dbName}.${tableName} (
+ id BIGINT,
+ username VARCHAR(30)
+ )
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES ("replication_num" = "1");
+ """
+ // give the user full privileges on the database. erasing the recycle bin
must still be
+ // refused: it takes a raw object id and is irreversible, so it is ADMIN
only.
+ sql """grant ALL on ${dbName}.* to ${user}"""
+
+ sql """drop table ${dbName}.${tableName}"""
+ def binRes = sql """SHOW CATALOG RECYCLE BIN WHERE NAME = "${tableName}"
"""
+ assertTrue(binRes.size() > 0)
+ def tableId = binRes[0][3]
+ logger.info("recycled table id: " + tableId)
+
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ test {
+ sql """DROP CATALOG RECYCLE BIN WHERE 'TableId' = ${tableId};"""
+ exception "denied"
+ }
+ test {
+ sql """DROP CATALOG RECYCLE BIN WHERE 'DbId' = ${tableId};"""
+ exception "denied"
+ }
+ test {
+ sql """DROP CATALOG RECYCLE BIN WHERE 'PartitionId' =
${tableId};"""
+ exception "denied"
+ }
+ }
+ // still there after the denied attempts
+ def afterRes = sql """SHOW CATALOG RECYCLE BIN WHERE NAME = "${tableName}"
"""
+ assertTrue(afterRes.size() == binRes.size())
+
+ sql """grant admin_priv on *.*.* to ${user}"""
+ connect(user, "${pwd}", context.config.jdbcUrl) {
+ sql """DROP CATALOG RECYCLE BIN WHERE 'TableId' = ${tableId};"""
+ }
+ def erasedRes = sql """SHOW CATALOG RECYCLE BIN WHERE NAME =
"${tableName}" """
+ assertTrue(erasedRes.size() < binRes.size())
+
+ sql """drop database if exists ${dbName}"""
+ try_sql("DROP USER ${user}")
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]