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]

Reply via email to