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

CalvinKirs pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 13c57dcad9e [fix](auth) Add missing privilege checks for several 
Nereids commands (#66218)
13c57dcad9e is described below

commit 13c57dcad9e80528f50ef6475a60b1451d97a1b0
Author: Calvin Kirs <[email protected]>
AuthorDate: Fri Aug 7 14:49:43 2026 +0800

    [fix](auth) Add missing privilege checks for several Nereids commands 
(#66218)
    
    ### What problem does this PR solve?
    
    Problem Summary:
    
    The Nereids path in `StmtExecutor` dispatches straight to
    `Command.run()`, so each command has to enforce its own privileges. A
    number of commands never got that check, and can currently be executed
    by any authenticated user regardless of their grants.
    
    This PR adds the missing checks:
    
    | Command | Required privilege | Follows |
    | --- | --- | --- |
    | `AdminSetEncryptionRootKeyCommand`, `AdminRotateTdeRootKeyCommand` |
    global `ADMIN` | `AdminSetFrontendConfig`, `AdminSetTableStatus`,
    `AdminSetReplicaStatus`, `AdminCleanTrash` |
    | `DropCatalogRecycleBinCommand` | global `ADMIN` | `SHOW CATALOG
    RECYCLE BIN` already requires global `ADMIN`. `RECOVER` uses
    `ALTER_CREATE`, but it is name-scoped while erasing takes a raw object
    id, so it cannot be authorized at db/table level |
    | `CreateDictionaryCommand` | `CREATE` on the dictionary **and**
    `SELECT` on the source table | `CREATE TABLE` / `CREATE MTMV`. The
    `SELECT` check is needed because the load task runs internally, and
    unlike an MTMV the source table is not bound by the planner |
    | `DropDictionaryCommand` | `DROP` on the dictionary | `DROP TABLE` /
    `DROP MTMV` |
    | `AddConstraintCommand`, `DropConstraintCommand` | `ALTER` on the
    table. For a foreign key, also on the referenced table; for dropping a
    primary key, also on every referencing table | the rest of `ALTER TABLE`
    |
    | `WarmUpClusterCommand` | `USAGE` on the source and destination compute
    groups, plus `SELECT` on each table named by `WITH TABLE`. `ON TABLES`
    additionally requires global `ADMIN` | `UseCloudClusterCommand` |
    | `CancelWarmUpJobCommand` | global `ADMIN` | `CloudWarmUpJob` records
    no owner, so a job cannot be scoped to the user who created it |
    | `DropStageCommand` | global `ADMIN` | `CreateStageCommand`, which
    already checked it |
    
    Note that `ADMIN_PRIV` satisfies every predicate used above, so admin
    users are unaffected by any of this.
    
    #### Where the check is placed, and why
    
    - `AddConstraintCommand`: for a foreign key the referenced table is
    checked as well, because adding the constraint registers a reverse
    reference on it.
    - `DropConstraintCommand`: the check sits after the two table-resolution
    paths converge. Resolution can fall back to a name-only lookup, and
    putting the check on the normal path only would let the fallback skip
    it.
    - `DropConstraintCommand`, primary keys: dropping one cascades into
    `ConstraintManager.cascadeDropForeignKeys()`, which deletes the foreign
    key constraint of every referencing table, so `ALTER` is required on
    each of those too. The cascade is atomic, so all of them are checked
    before `dropConstraint()`.
    - `CreateDictionaryCommand`: the check has to run after
    `validateAndSet()`, since that is what fills in the default catalog/db
    names.
    - `WarmUpClusterCommand`, `WITH TABLE`: the per-table `SELECT` check is
    inside the existing resolution loop, before the db/table lookup. It
    authorizes the *internal* fully qualified name, not the catalog written
    in the SQL, because the lookup and the `(db, table, partition)` triple
    stored for the job are internal-catalog only.
    - `WarmUpClusterCommand`, `ON TABLES`: this mode matches tables by glob
    over the whole internal catalog, and
    `CacheHotspotManager.refreshAllTableFilters()` keeps re-matching in the
    background with no identity available to re-authorize new matches. There
    is no fixed table set to authorize, so it requires global `ADMIN` on top
    of the compute group `USAGE`.
    
    #### Incidental changes reviewers should look at
    
    These are not privilege checks, but they fall out of adding them:
    
    1. `AdminRotateTdeRootKeyCommand`, `DropCatalogRecycleBinCommand`,
    `DropStageCommand` had no `validate()` method at all. One was added to
    each and is called at the top of `run()`.
    2. `CreateDictionaryCommand.run()` and `DropDictionaryCommand.run()` now
    declare `throws Exception`.
    3. `CreateDictionaryCommand.run()`: the single try block that wrapped
    `validateAndSet()` + `createDictionary()` is split in two, with the
    check in between. Consequence: an access-denied error is **not** wrapped
    in the `"Failed to create dictionary: ..."` prefix, while the failure
    messages from `validateAndSet()` and `createDictionary()` are unchanged.
    4. `WarmUpClusterCommand.validate()`: order is now cloud-mode → compute
    group `USAGE` (+ `ADMIN` for `ON TABLES`) → compute group
    existence/virtual-group validation → table resolution. The non-cloud
    error message is unchanged, but in cloud mode a user without `USAGE` now
    gets an access-denied error where they previously got "compute group
    doesn't exist".
    5. `WarmUpClusterCommand`, `WITH TABLE`: the `SELECT` check runs before
    the db/table lookup, so a user without `SELECT` gets an access-denied
    error where they previously got "unknown database/table". This is
    deliberate: the error should not tell a user who cannot read the table
    whether it exists.
    6. `DropConstraintCommand`: the two `ALTER` checks are factored into a
    private `checkAlterPriv()`, same shape as the one in
    `AddConstraintCommand`.
    
    #### Open questions
    
    - `WarmUpClusterCommand`: for `WITH TABLE`, global `ADMIN` felt too
    coarse for an operation scoped to compute groups the user already has
    `USAGE` on, so it uses `checkCloudPriv(..., ResourceTypeEnum.CLUSTER)`
    plus per-table `SELECT`. Happy to switch it back to `ADMIN` if the cloud
    maintainers prefer that. `ON TABLES` does require `ADMIN`, since a
    pattern job cannot be authorized per table.
    - `DropConstraintCommand`: requiring `ALTER` on the referencing tables
    means a user who owns the primary key table can no longer drop it once
    somebody else's table references it. Rejecting the cascade and asking
    for the foreign keys to be dropped separately would be the other option;
    happy to switch.
    - `CancelWarmUpJobCommand` keeps `ADMIN` only because there is nothing
    to scope it to. If `CloudWarmUpJob` recorded the submitting user,
    letting that user cancel their own job would be better.
    - Out of scope, but noted while looking around: `ShowWarmUpCommand`
    (`SHOW WARM UP JOB`) and `ShowDictionariesCommand` (`SHOW DICTIONARIES`)
    have no privilege check either. Happy to follow up in a separate PR.
    
    ### Release note
    
    Fix missing privilege checks on `ADMIN SET ENCRYPTION ROOT KEY`, `ADMIN
    ROTATE TDE ROOT KEY`, `DROP CATALOG RECYCLE BIN`, `CREATE/DROP
    DICTIONARY`, `ALTER TABLE ADD/DROP CONSTRAINT`, `WARM UP CLUSTER`,
    `CANCEL WARM UP JOB` and `DROP STAGE`. These statements previously ran
    for any authenticated user.
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [x] Regression test
        - [ ] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    New `auth_call` cases for constraint, dictionary and recycle bin, each
    covering both the denied and the granted path, including the cross-table
    primary key / foreign key cascade. The cloud-only commands (`WARM UP
    CLUSTER`, `CANCEL WARM UP JOB`, `DROP STAGE`) are not covered by a
    regression case.
    
    - Behavior changed:
        - [ ] No.
    - [x] Yes. Users without the privileges listed above can no longer run
    these statements; they now get an access-denied error. See "Incidental
    changes reviewers should look at" above for the error-message and
    ordering changes that come with it.
    
    - Does this need documentation?
        - [ ] No.
    - [x] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    The privilege documentation for these statements should list the
    required privileges.
---
 .../catalog/constraint/ConstraintManager.java      |  18 +++
 .../trees/plans/commands/AddConstraintCommand.java |  16 +++
 .../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      |  25 ++++
 .../plans/commands/DropDictionaryCommand.java      |  12 +-
 .../trees/plans/commands/DropStageCommand.java     |  15 +++
 .../trees/plans/commands/WarmUpClusterCommand.java |  42 ++++++-
 .../cloud/WarmUpClusterOnTablesParseTest.java      |   6 +
 .../auth_call/test_ddl_constraint_auth.groovy      | 129 +++++++++++++++++++++
 .../auth_call/test_ddl_dictionary_auth.groovy      | 106 +++++++++++++++++
 .../suites/auth_call/test_recycle_bin_auth.groovy  |  82 +++++++++++++
 15 files changed, 533 insertions(+), 4 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/ConstraintManager.java
 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/ConstraintManager.java
index cf16af663b6..5044f35d921 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/ConstraintManager.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/constraint/ConstraintManager.java
@@ -126,6 +126,24 @@ public class ConstraintManager implements Writable, 
GsonPostProcessable {
         }
     }
 
+    /**
+     * Snapshot the tables whose foreign keys would be cascade-dropped along 
with the given primary
+     * key constraint. Taken under the read lock: {@link 
PrimaryKeyConstraint#getForeignTableInfos()}
+     * is only a view over a list that {@link #addConstraint} mutates under 
the write lock, so callers
+     * outside the lock must not iterate it directly. Returns an empty list 
for other constraint types.
+     */
+    public List<TableNameInfo> getCascadeDropTables(Constraint constraint) {
+        if (!(constraint instanceof PrimaryKeyConstraint)) {
+            return ImmutableList.of();
+        }
+        readLock();
+        try {
+            return ImmutableList.copyOf(((PrimaryKeyConstraint) 
constraint).getForeignTableInfos());
+        } finally {
+            readUnlock();
+        }
+    }
+
     /**
      * Drop a constraint from the specified table.
      * For PK constraints, cascade-drops all referencing FKs.
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 7f9d2c468f2..5eeb3f80f4e 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
@@ -24,9 +24,12 @@ import 
org.apache.doris.catalog.constraint.ForeignKeyConstraint;
 import org.apache.doris.catalog.constraint.PrimaryKeyConstraint;
 import org.apache.doris.catalog.constraint.UniqueConstraint;
 import org.apache.doris.catalog.info.TableNameInfo;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
 import org.apache.doris.common.Pair;
 import org.apache.doris.info.TableNameInfoUtils;
 import org.apache.doris.mtmv.MTMVUtil;
+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;
@@ -71,6 +74,7 @@ public class AddConstraintCommand extends Command implements 
ForwardWithSync {
         TableNameInfo tableNameInfo = TableNameInfoUtils.fromCatalogDb(
                 table.getDatabase().getCatalog(), table.getDatabase(), table);
         ImmutableList<String> columns = columnsAndTable.first;
+        checkAlterPriv(ctx, tableNameInfo);
 
         Pair<ImmutableList<String>, TableNameInfo> referencedColumnsAndTable = 
null;
         if (constraint.isForeignKey()) {
@@ -79,6 +83,8 @@ public class AddConstraintCommand extends Command implements 
ForwardWithSync {
             TableIf refTable = refColumnsAndTable.second;
             TableNameInfo refTableInfo = TableNameInfoUtils.fromCatalogDb(
                     refTable.getDatabase().getCatalog(), 
refTable.getDatabase(), refTable);
+            // a foreign key also registers a reverse reference on the 
referenced table
+            checkAlterPriv(ctx, refTableInfo);
             referencedColumnsAndTable = Pair.of(refColumnsAndTable.first, 
refTableInfo);
         }
         if (constraint.isForeignKey()) {
@@ -97,6 +103,16 @@ public class AddConstraintCommand extends Command 
implements ForwardWithSync {
         }
     }
 
+    private void checkAlterPriv(ConnectContext ctx, TableNameInfo 
tableNameInfo)
+            throws org.apache.doris.common.AnalysisException {
+        if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx, 
tableNameInfo.getCtl(),
+                tableNameInfo.getDb(), tableNameInfo.getTbl(), 
PrivPredicate.ALTER)) {
+            
ErrorReport.reportAnalysisException(ErrorCode.ERR_TABLEACCESS_DENIED_ERROR, 
"ALTER",
+                    ctx.getQualifiedUser(), ctx.getRemoteIP(),
+                    tableNameInfo.getDb() + ": " + tableNameInfo.getTbl());
+        }
+    }
+
     private void addConstraintAndInvalidate(
             TableNameInfo tableNameInfo, 
org.apache.doris.catalog.constraint.Constraint constraint)
             throws Exception {
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 fb8d506ef97..15419eb20be 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
@@ -22,8 +22,11 @@ import org.apache.doris.catalog.MTMV;
 import org.apache.doris.catalog.TableIf;
 import org.apache.doris.catalog.constraint.Constraint;
 import org.apache.doris.catalog.info.TableNameInfo;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
 import org.apache.doris.info.TableNameInfoUtils;
 import org.apache.doris.mtmv.MTMVUtil;
+import org.apache.doris.mysql.privilege.PrivPredicate;
 import org.apache.doris.nereids.NereidsPlanner;
 import org.apache.doris.nereids.analyzer.UnboundRelation;
 import org.apache.doris.nereids.exceptions.AnalysisException;
@@ -75,17 +78,39 @@ public class DropConstraintCommand extends Command 
implements ForwardWithSync {
                     + "falling back to name-based lookup: {}", name, 
e.getMessage());
             tableNameInfo = extractTableNameFromPlan(ctx);
         }
+        // must be checked on both paths above: table resolution failing 
(which includes an
+        // authorization failure) falls back to a name-only lookup that binds 
nothing.
+        checkAlterPriv(ctx, tableNameInfo);
         Constraint constraint = 
Env.getCurrentEnv().getConstraintManager().getConstraint(tableNameInfo, name);
         if (constraint == null) {
             throw new AnalysisException(
                     String.format("Unknown constraint %s on table %s.", name, 
tableNameInfo));
         }
+        // dropping a primary key cascades into 
ConstraintManager.cascadeDropForeignKeys(), which
+        // deletes the foreign key constraints of every referencing table, so 
those tables have to be
+        // authorized too. Checked before dropConstraint() because the cascade 
is atomic. The snapshot
+        // is taken under the manager lock; a foreign key added after it still 
needs ALTER on its own
+        // table to be created, so it cannot be used to bypass this.
+        for (TableNameInfo fkTableInfo
+                : 
Env.getCurrentEnv().getConstraintManager().getCascadeDropTables(constraint)) {
+            checkAlterPriv(ctx, fkTableInfo);
+        }
         List<MTMV> dependentMtmvs = 
MTMVUtil.getDependentMtmvsByConstraint(tableNameInfo, constraint);
         
Env.getCurrentEnv().getConstraintManager().dropConstraint(tableNameInfo, name, 
false);
         MTMVUtil.invalidateRewriteCachesBestEffort(dependentMtmvs,
                 String.format("after drop constraint %s on table %s", 
constraint.getName(), tableNameInfo));
     }
 
+    private void checkAlterPriv(ConnectContext ctx, TableNameInfo 
tableNameInfo)
+            throws org.apache.doris.common.AnalysisException {
+        if (!Env.getCurrentEnv().getAccessManager().checkTblPriv(ctx, 
tableNameInfo.getCtl(),
+                tableNameInfo.getDb(), tableNameInfo.getTbl(), 
PrivPredicate.ALTER)) {
+            
ErrorReport.reportAnalysisException(ErrorCode.ERR_TABLEACCESS_DENIED_ERROR, 
"ALTER",
+                    ctx.getQualifiedUser(), ctx.getRemoteIP(),
+                    tableNameInfo.getDb() + ": " + tableNameInfo.getTbl());
+        }
+    }
+
     private TableNameInfo extractTableNameFromPlan(ConnectContext ctx) {
         if (!(plan instanceof UnboundRelation)) {
             throw new AnalysisException(
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 fd77fb41779..e568a90e33d 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;
@@ -33,6 +34,8 @@ 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.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/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..0d610c88d36
--- /dev/null
+++ b/regression-test/suites/auth_call/test_ddl_constraint_auth.groovy
@@ -0,0 +1,129 @@
+// 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 is refused as well. Note this goes through the 
normal resolution path:
+    // the name-based fallback in DropConstraintCommand only triggers when 
resolution throws (e.g. an
+    // external table removed out of band), which is not reproducible from a 
suite, so the check on
+    // that branch is covered by inspection only.
+    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