You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@doris.apache.org by mo...@apache.org on 2022/06/23 06:10:16 UTC

[doris] branch master updated: [feature-wip](multi-catalog)(resubmit) add catalog level privileges (#10345)

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

morningman 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 6a54fc2fe5 [feature-wip](multi-catalog)(resubmit) add catalog level privileges (#10345)
6a54fc2fe5 is described below

commit 6a54fc2fe5a538db23e765fe43a9354fcb15c5ce
Author: Ashin Gau <As...@users.noreply.github.com>
AuthorDate: Thu Jun 23 14:10:11 2022 +0800

    [feature-wip](multi-catalog)(resubmit) add catalog level privileges (#10345)
---
 .../org/apache/doris/common/FeMetaVersion.java     |   4 +-
 .../java/org/apache/doris/analysis/Analyzer.java   |  13 +-
 .../java/org/apache/doris/analysis/GrantStmt.java  |   4 +-
 .../java/org/apache/doris/analysis/RevokeStmt.java |   2 +-
 .../org/apache/doris/analysis/ShowRolesStmt.java   |   1 +
 .../java/org/apache/doris/analysis/TableName.java  |  88 +++++++---
 .../org/apache/doris/analysis/TablePattern.java    | 102 +++++++----
 .../org/apache/doris/common/CaseSensibility.java   |   1 +
 .../java/org/apache/doris/common/ErrorCode.java    |   2 +-
 .../java/org/apache/doris/common/FeNameFormat.java |   8 +
 .../org/apache/doris/common/proc/AuthProcDir.java  |   4 +-
 .../doris/mysql/privilege/CatalogPrivEntry.java    | 143 ++++++++++++++++
 .../doris/mysql/privilege/CatalogPrivTable.java    |  73 ++++++++
 .../apache/doris/mysql/privilege/DbPrivEntry.java  |  52 +++---
 .../apache/doris/mysql/privilege/DbPrivTable.java  |  31 +---
 .../org/apache/doris/mysql/privilege/PaloAuth.java | 188 ++++++++++++++++-----
 .../apache/doris/mysql/privilege/PrivEntry.java    |  16 ++
 .../apache/doris/mysql/privilege/PrivTable.java    |  21 +++
 .../apache/doris/mysql/privilege/RoleManager.java  |  78 +++------
 .../doris/mysql/privilege/TablePrivEntry.java      |  60 +++----
 .../doris/mysql/privilege/TablePrivTable.java      |  37 ++--
 .../doris/mysql/privilege/UserPrivTable.java       |  52 +++---
 .../java/org/apache/doris/qe/ConnectContext.java   |  11 ++
 .../org/apache/doris/analysis/AccessTestUtil.java  |  17 ++
 .../apache/doris/analysis/DropTableStmtTest.java   |   5 +
 .../apache/doris/analysis/VirtualSlotRefTest.java  |   6 +
 .../apache/doris/ldap/LdapPrivsCheckerTest.java    |  10 +-
 .../org/apache/doris/mysql/MysqlProtoTest.java     |   5 +
 .../org/apache/doris/mysql/privilege/AuthTest.java |   7 +-
 .../doris/mysql/privilege/PrivEntryTest.java       |   6 +-
 .../java/org/apache/doris/policy/PolicyTest.java   |   2 +-
 31 files changed, 746 insertions(+), 303 deletions(-)

diff --git a/fe/fe-common/src/main/java/org/apache/doris/common/FeMetaVersion.java b/fe/fe-common/src/main/java/org/apache/doris/common/FeMetaVersion.java
index 70cacd7a1c..1a5d6da4ce 100644
--- a/fe/fe-common/src/main/java/org/apache/doris/common/FeMetaVersion.java
+++ b/fe/fe-common/src/main/java/org/apache/doris/common/FeMetaVersion.java
@@ -40,8 +40,10 @@ public final class FeMetaVersion {
     public static final int VERSION_109 = 109;
     // For routine load user info
     public static final int VERSION_110 = 110;
+    // add catalog PrivTable in PaloAuth to support unified privilege management
+    public static final int VERSION_111 = 111;
     // note: when increment meta version, should assign the latest version to VERSION_CURRENT
-    public static final int VERSION_CURRENT = VERSION_110;
+    public static final int VERSION_CURRENT = VERSION_111;
 
     // all logs meta version should >= the minimum version, so that we could remove many if clause, for example
     // if (FE_METAVERSION < VERSION_94) ...
diff --git a/fe/fe-core/src/main/java/org/apache/doris/analysis/Analyzer.java b/fe/fe-core/src/main/java/org/apache/doris/analysis/Analyzer.java
index 997891253f..ebd5bc6dcb 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/analysis/Analyzer.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/analysis/Analyzer.java
@@ -1365,10 +1365,13 @@ public class Analyzer {
      * is already fully qualified, returns tableName.
      */
     public TableName getFqTableName(TableName tableName) {
-        if (tableName.isFullyQualified()) {
-            return tableName;
+        if (Strings.isNullOrEmpty(tableName.getCtl())) {
+            tableName.setCtl(getDefaultCatalog());
         }
-        return new TableName(getDefaultDb(), tableName.getTbl());
+        if (Strings.isNullOrEmpty(tableName.getDb())) {
+            tableName.setDb(getDefaultDb());
+        }
+        return tableName;
     }
 
     public TupleId getTupleId(SlotId slotId) {
@@ -1935,6 +1938,10 @@ public class Analyzer {
         return globalState.context.getConnectionId();
     }
 
+    public String getDefaultCatalog() {
+        return globalState.context.getDefaultCatalog();
+    }
+
     public String getDefaultDb() {
         return globalState.context.getDatabase();
     }
diff --git a/fe/fe-core/src/main/java/org/apache/doris/analysis/GrantStmt.java b/fe/fe-core/src/main/java/org/apache/doris/analysis/GrantStmt.java
index 4e849e7c61..4ad8fe8073 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/analysis/GrantStmt.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/analysis/GrantStmt.java
@@ -107,7 +107,7 @@ public class GrantStmt extends DdlStmt {
         }
 
         if (tblPattern != null) {
-            tblPattern.analyze(analyzer.getClusterName());
+            tblPattern.analyze(analyzer);
         } else {
             // TODO(wyb): spark-load
             if (!Config.enable_spark_load) {
@@ -148,7 +148,7 @@ public class GrantStmt extends DdlStmt {
         // Rule 1
         if (tblPattern.getPrivLevel() != PrivLevel.GLOBAL && (privileges.contains(PaloPrivilege.ADMIN_PRIV)
                 || privileges.contains(PaloPrivilege.NODE_PRIV))) {
-            throw new AnalysisException("ADMIN_PRIV and NODE_PRIV can only be granted on *.*");
+            throw new AnalysisException("ADMIN_PRIV and NODE_PRIV can only be granted on *.*.*");
         }
 
         // Rule 2
diff --git a/fe/fe-core/src/main/java/org/apache/doris/analysis/RevokeStmt.java b/fe/fe-core/src/main/java/org/apache/doris/analysis/RevokeStmt.java
index c84f490ddf..98b36b4968 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/analysis/RevokeStmt.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/analysis/RevokeStmt.java
@@ -97,7 +97,7 @@ public class RevokeStmt extends DdlStmt {
         }
 
         if (tblPattern != null) {
-            tblPattern.analyze(analyzer.getClusterName());
+            tblPattern.analyze(analyzer);
         } else {
             // TODO(wyb): spark-load
             if (!Config.enable_spark_load) {
diff --git a/fe/fe-core/src/main/java/org/apache/doris/analysis/ShowRolesStmt.java b/fe/fe-core/src/main/java/org/apache/doris/analysis/ShowRolesStmt.java
index 47b9bacda2..4e8c96e2b9 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/analysis/ShowRolesStmt.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/analysis/ShowRolesStmt.java
@@ -36,6 +36,7 @@ public class ShowRolesStmt extends ShowStmt {
         builder.addColumn(new Column("Name", ScalarType.createVarchar(100)));
         builder.addColumn(new Column("Users", ScalarType.createVarchar(100)));
         builder.addColumn(new Column("GlobalPrivs", ScalarType.createVarchar(300)));
+        builder.addColumn(new Column("CatalogPrivs", ScalarType.createVarchar(300)));
         builder.addColumn(new Column("DatabasePrivs", ScalarType.createVarchar(300)));
         builder.addColumn(new Column("TablePrivs", ScalarType.createVarchar(300)));
         builder.addColumn(new Column("ResourcePrivs", ScalarType.createVarchar(300)));
diff --git a/fe/fe-core/src/main/java/org/apache/doris/analysis/TableName.java b/fe/fe-core/src/main/java/org/apache/doris/analysis/TableName.java
index 047d518e16..5e71b26c82 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/analysis/TableName.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/analysis/TableName.java
@@ -25,32 +25,57 @@ import org.apache.doris.cluster.ClusterNamespace;
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.ErrorCode;
 import org.apache.doris.common.ErrorReport;
+import org.apache.doris.common.FeMetaVersion;
 import org.apache.doris.common.io.Text;
 import org.apache.doris.common.io.Writable;
+import org.apache.doris.datasource.InternalDataSource;
+import org.apache.doris.persist.gson.GsonUtils;
 
 import com.google.common.base.Strings;
+import com.google.gson.annotations.SerializedName;
 
 import java.io.DataInput;
 import java.io.DataOutput;
 import java.io.IOException;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 public class TableName implements Writable {
+    @SerializedName(value = "ctl")
+    private String ctl;
+    @SerializedName(value = "tbl")
     private String tbl;
+    @SerializedName(value = "db")
     private String db;
 
     public TableName() {
 
     }
 
-    public TableName(String db, String tbl) {
+    public TableName(String ctl, String db, String tbl) {
         if (Catalog.isStoredTableNamesLowerCase() && !Strings.isNullOrEmpty(tbl)) {
             tbl = tbl.toLowerCase();
         }
+        this.ctl = ctl;
         this.db = db;
         this.tbl = tbl;
     }
 
+    /**
+     * Initialize catalog in analyze.
+     */
+    public TableName(String db, String tbl) {
+        this(null, db, tbl);
+    }
+
     public void analyze(Analyzer analyzer) throws AnalysisException {
+        if (Strings.isNullOrEmpty(ctl)) {
+            ctl = analyzer.getDefaultCatalog();
+            if (Strings.isNullOrEmpty(ctl)) {
+                ctl = InternalDataSource.INTERNAL_DS_NAME;
+            }
+        }
         if (Strings.isNullOrEmpty(db)) {
             db = analyzer.getDefaultDb();
             if (Strings.isNullOrEmpty(db)) {
@@ -68,6 +93,14 @@ public class TableName implements Writable {
         }
     }
 
+    public String getCtl() {
+        return ctl;
+    }
+
+    public void setCtl(String ctl) {
+        this.ctl = ctl;
+    }
+
     public String getDb() {
         return db;
     }
@@ -85,33 +118,30 @@ public class TableName implements Writable {
     }
 
     /**
-     * Returns true if this name has a non-empty database field and a non-empty
-     * table name.
+     * Returns true if this name has a non-empty catalog and a non-empty database field
+     * and a non-empty table name.
      */
     public boolean isFullyQualified() {
-        return db != null && !db.isEmpty() && !tbl.isEmpty();
+        return Stream.of(ctl, db, tbl).noneMatch(Strings::isNullOrEmpty);
     }
 
     public String getNoClusterString() {
-        if (db == null) {
-            return tbl;
-        } else {
-            String dbName = ClusterNamespace.getNameFromFullName(db);
-            if (dbName == null) {
-                return db + "." + tbl;
-            } else {
-                return dbName + "." + tbl;
-            }
-        }
+        return Stream.of(ctl, ClusterNamespace.getNameFromFullName(db), tbl)
+                .filter(Objects::nonNull)
+                .collect(Collectors.joining("."));
     }
 
     @Override
     public String toString() {
-        if (db == null) {
-            return tbl;
-        } else {
-            return db + "." + tbl;
+        StringBuilder stringBuilder = new StringBuilder();
+        if (ctl != null && !ctl.equals(InternalDataSource.INTERNAL_DS_NAME)) {
+            stringBuilder.append(ctl).append(".");
         }
+        if (db != null) {
+            stringBuilder.append(db).append(".");
+        }
+        stringBuilder.append(tbl);
+        return stringBuilder.toString();
     }
 
     @Override
@@ -127,6 +157,9 @@ public class TableName implements Writable {
 
     public String toSql() {
         StringBuilder stringBuilder = new StringBuilder();
+        if (ctl != null && !ctl.equals(InternalDataSource.INTERNAL_DS_NAME)) {
+            stringBuilder.append("`").append(ctl).append("`.");
+        }
         if (db != null) {
             stringBuilder.append("`").append(db).append("`.");
         }
@@ -136,17 +169,24 @@ public class TableName implements Writable {
 
     @Override
     public void write(DataOutput out) throws IOException {
-        Text.writeString(out, db);
-        Text.writeString(out, tbl);
+        String json = GsonUtils.GSON.toJson(this);
+        Text.writeString(out, json);
     }
 
     public void readFields(DataInput in) throws IOException {
-        db = Text.readString(in);
-        tbl = Text.readString(in);
+        if (Catalog.getCurrentCatalogJournalVersion() >= FeMetaVersion.VERSION_111) {
+            TableName fromJson = GsonUtils.GSON.fromJson(Text.readString(in), TableName.class);
+            ctl = fromJson.ctl;
+            db = fromJson.db;
+            tbl = fromJson.tbl;
+        } else {
+            ctl = InternalDataSource.INTERNAL_DS_NAME;
+            db = Text.readString(in);
+            tbl = Text.readString(in);
+        }
     }
 
     public TableName cloneWithoutAnalyze() {
-        TableName tableName = new TableName(this.db, this.tbl);
-        return tableName;
+        return new TableName(this.ctl, this.db, this.tbl);
     }
 }
diff --git a/fe/fe-core/src/main/java/org/apache/doris/analysis/TablePattern.java b/fe/fe-core/src/main/java/org/apache/doris/analysis/TablePattern.java
index 6f6a3d393a..27eac89e6b 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/analysis/TablePattern.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/analysis/TablePattern.java
@@ -17,33 +17,46 @@
 
 package org.apache.doris.analysis;
 
+import org.apache.doris.catalog.Catalog;
 import org.apache.doris.cluster.ClusterNamespace;
 import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.FeMetaVersion;
 import org.apache.doris.common.FeNameFormat;
 import org.apache.doris.common.io.Text;
 import org.apache.doris.common.io.Writable;
+import org.apache.doris.datasource.InternalDataSource;
 import org.apache.doris.mysql.privilege.PaloAuth.PrivLevel;
+import org.apache.doris.persist.gson.GsonUtils;
 
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
+import com.google.gson.annotations.SerializedName;
 
 import java.io.DataInput;
 import java.io.DataOutput;
 import java.io.IOException;
-
-// only the following 3 formats are allowed
-// db.tbl
-// *.*
-// db.*
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Three-segment-format: catalog.database.table. If the lower segment is specific,
+ * the higher segment can't be a wildcard. The following examples are not allowed:
+ * "ctl1.*.table1", "*.*.table2", "*.db1.*", ...
+ */
 public class TablePattern implements Writable {
+    @SerializedName(value = "ctl")
+    private String ctl;
+    @SerializedName(value = "db")
     private String db;
+    @SerializedName(value = "tbl")
     private String tbl;
     boolean isAnalyzed = false;
 
     public static TablePattern ALL;
 
     static {
-        ALL = new TablePattern("*", "*");
+        ALL = new TablePattern("*", "*", "*");
         try {
             ALL.analyze("");
         } catch (AnalysisException e) {
@@ -54,11 +67,23 @@ public class TablePattern implements Writable {
     private TablePattern() {
     }
 
+    public TablePattern(String ctl, String db, String tbl) {
+        this.ctl = Strings.isNullOrEmpty(ctl) ? "*" : ctl;
+        this.db = Strings.isNullOrEmpty(db) ? "*" : db;
+        this.tbl = Strings.isNullOrEmpty(tbl) ? "*" : tbl;
+    }
+
     public TablePattern(String db, String tbl) {
+        this.ctl = null;
         this.db = Strings.isNullOrEmpty(db) ? "*" : db;
         this.tbl = Strings.isNullOrEmpty(tbl) ? "*" : tbl;
     }
 
+    public String getQualifiedCtl() {
+        Preconditions.checkState(isAnalyzed);
+        return ctl;
+    }
+
     public String getQualifiedDb() {
         Preconditions.checkState(isAnalyzed);
         return db;
@@ -70,23 +95,39 @@ public class TablePattern implements Writable {
 
     public PrivLevel getPrivLevel() {
         Preconditions.checkState(isAnalyzed);
-        if (db.equals("*")) {
+        if (ctl.equals("*")) {
             return PrivLevel.GLOBAL;
-        } else if (!tbl.equals("*")) {
+        } else if (db.equals("*")) {
+            return PrivLevel.CATALOG;
+        } else if (tbl.equals("*")) {
+            return PrivLevel.DATABASE;
+        } else {
             return PrivLevel.TABLE;
+        }
+    }
+
+    public void analyze(Analyzer analyzer) throws AnalysisException {
+        if (ctl == null) {
+            analyze(analyzer.getDefaultCatalog(), analyzer.getClusterName());
         } else {
-            return PrivLevel.DATABASE;
+            analyze(analyzer.getClusterName());
         }
     }
 
-    public void analyze(String clusterName) throws AnalysisException {
+    private void analyze(String catalogName, String clusterName) throws AnalysisException {
         if (isAnalyzed) {
             return;
         }
-        if (db.equals("*") && !tbl.equals("*")) {
+        this.ctl = Strings.isNullOrEmpty(catalogName) ? InternalDataSource.INTERNAL_DS_NAME : catalogName;
+        if ((!tbl.equals("*") && (db.equals("*") || ctl.equals("*")))
+                || (!db.equals("*") && ctl.equals("*"))) {
             throw new AnalysisException("Do not support format: " + toString());
         }
 
+        if (!ctl.equals("*")) {
+            FeNameFormat.checkCatalogName(ctl);
+        }
+
         if (!db.equals("*")) {
             FeNameFormat.checkDbName(db);
             db = ClusterNamespace.getFullName(clusterName, db);
@@ -98,9 +139,21 @@ public class TablePattern implements Writable {
         isAnalyzed = true;
     }
 
+    public void analyze(String clusterName) throws AnalysisException {
+        analyze(ctl, clusterName);
+    }
+
     public static TablePattern read(DataInput in) throws IOException {
-        TablePattern tablePattern = new TablePattern();
-        tablePattern.readFields(in);
+        TablePattern tablePattern;
+        if (Catalog.getCurrentCatalogJournalVersion() >= FeMetaVersion.VERSION_111) {
+            tablePattern = GsonUtils.GSON.fromJson(Text.readString(in), TablePattern.class);
+        } else {
+            String ctl = InternalDataSource.INTERNAL_DS_NAME;
+            String db = Text.readString(in);
+            String tbl = Text.readString(in);
+            tablePattern = new TablePattern(ctl, db, tbl);
+        }
+        tablePattern.isAnalyzed = true;
         return tablePattern;
     }
 
@@ -110,34 +163,25 @@ public class TablePattern implements Writable {
             return false;
         }
         TablePattern other = (TablePattern) obj;
-        return db.equals(other.getQualifiedDb()) && tbl.equals(other.getTbl());
+        return ctl.equals(other.getQualifiedCtl()) && db.equals(other.getQualifiedDb()) && tbl.equals(other.getTbl());
     }
 
     @Override
     public int hashCode() {
-        int result = 17;
-        result = 31 * result + db.hashCode();
-        result = 31 * result + tbl.hashCode();
-        return result;
+        return Stream.of(ctl, db, tbl).filter(Objects::nonNull)
+                .map(String::hashCode)
+                .reduce(17, (acc, h) -> 31 * acc + h);
     }
 
     @Override
     public String toString() {
-        StringBuilder sb = new StringBuilder();
-        sb.append(db).append(".").append(tbl);
-        return sb.toString();
+        return Stream.of(ctl, db, tbl).filter(Objects::nonNull).collect(Collectors.joining("."));
     }
 
     @Override
     public void write(DataOutput out) throws IOException {
         Preconditions.checkState(isAnalyzed);
-        Text.writeString(out, db);
-        Text.writeString(out, tbl);
-    }
-
-    public void readFields(DataInput in) throws IOException {
-        db = Text.readString(in);
-        tbl = Text.readString(in);
-        isAnalyzed = true;
+        String json = GsonUtils.GSON.toJson(this);
+        Text.writeString(out, json);
     }
 }
diff --git a/fe/fe-core/src/main/java/org/apache/doris/common/CaseSensibility.java b/fe/fe-core/src/main/java/org/apache/doris/common/CaseSensibility.java
index 651581a3c6..6d5da6e65f 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/common/CaseSensibility.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/common/CaseSensibility.java
@@ -22,6 +22,7 @@ package org.apache.doris.common;
  **/
 public enum CaseSensibility {
     CLUSTER(true),
+    CATALOG(true),
     DATABASE(true),
     TABLE(true),
     ROLLUP(true),
diff --git a/fe/fe-core/src/main/java/org/apache/doris/common/ErrorCode.java b/fe/fe-core/src/main/java/org/apache/doris/common/ErrorCode.java
index 35dedadc30..a8f73b9773 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/common/ErrorCode.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/common/ErrorCode.java
@@ -1686,7 +1686,7 @@ public enum ErrorCode {
                     + "Use `SHOW PARTITIONS FROM %s` to see the currently partitions of this table. "),
     ERROR_SQL_AND_LIMITATIONS_SET_IN_ONE_RULE(5084, new byte[]{'4', '2', '0', '0', '0'},
             "sql/sqlHash and partition_num/tablet_num/cardinality cannot be set in one rule."),
-    ;
+    ERR_WRONG_CATALOG_NAME(5085, new byte[]{'4', '2', '0', '0', '0'}, "Incorrect catalog name '%s'");
 
     // This is error code
     private final int code;
diff --git a/fe/fe-core/src/main/java/org/apache/doris/common/FeNameFormat.java b/fe/fe-core/src/main/java/org/apache/doris/common/FeNameFormat.java
index 1f3db6a4a6..ea12ba5758 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/common/FeNameFormat.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/common/FeNameFormat.java
@@ -18,6 +18,7 @@
 package org.apache.doris.common;
 
 import org.apache.doris.alter.SchemaChangeHandler;
+import org.apache.doris.datasource.InternalDataSource;
 import org.apache.doris.mysql.privilege.PaloRole;
 import org.apache.doris.system.SystemInfoService;
 
@@ -42,6 +43,13 @@ public class FeNameFormat {
         }
     }
 
+    public static void checkCatalogName(String catalogName) throws AnalysisException {
+        if (!InternalDataSource.INTERNAL_DS_NAME.equals(catalogName)
+                && (Strings.isNullOrEmpty(catalogName) || !catalogName.matches(COMMON_NAME_REGEX))) {
+            ErrorReport.reportAnalysisException(ErrorCode.ERR_WRONG_CATALOG_NAME, catalogName);
+        }
+    }
+
     public static void checkDbName(String dbName) throws AnalysisException {
         if (Strings.isNullOrEmpty(dbName) || !dbName.matches(COMMON_NAME_REGEX)) {
             ErrorReport.reportAnalysisException(ErrorCode.ERR_WRONG_DB_NAME, dbName);
diff --git a/fe/fe-core/src/main/java/org/apache/doris/common/proc/AuthProcDir.java b/fe/fe-core/src/main/java/org/apache/doris/common/proc/AuthProcDir.java
index 33e3b2854c..6f11b4dd4e 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/common/proc/AuthProcDir.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/common/proc/AuthProcDir.java
@@ -31,8 +31,8 @@ import com.google.common.collect.ImmutableList;
  */
 public class AuthProcDir implements ProcDirInterface {
     public static final ImmutableList<String> TITLE_NAMES = new ImmutableList.Builder<String>()
-            .add("UserIdentity").add("Password").add("GlobalPrivs").add("DatabasePrivs")
-            .add("TablePrivs").add("ResourcePrivs").build();
+            .add("UserIdentity").add("Password").add("GlobalPrivs").add("CatalogPrivs")
+            .add("DatabasePrivs").add("TablePrivs").add("ResourcePrivs").build();
 
     private PaloAuth auth;
 
diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/CatalogPrivEntry.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/CatalogPrivEntry.java
new file mode 100644
index 0000000000..e55a6c1874
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/CatalogPrivEntry.java
@@ -0,0 +1,143 @@
+// 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.mysql.privilege;
+
+import org.apache.doris.catalog.Catalog;
+import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.CaseSensibility;
+import org.apache.doris.common.FeMetaVersion;
+import org.apache.doris.common.PatternMatcher;
+import org.apache.doris.common.io.Text;
+import org.apache.doris.datasource.InternalDataSource;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+
+public class CatalogPrivEntry extends PrivEntry {
+    protected static final String ANY_CTL = "*";
+
+    protected PatternMatcher ctlPattern;
+    protected String origCtl;
+    protected boolean isAnyCtl;
+
+    protected CatalogPrivEntry() {
+    }
+
+    protected CatalogPrivEntry(PatternMatcher userPattern, String user,
+                               PatternMatcher hostPattern, String origHost,
+                               PatternMatcher ctlPattern, String origCtl,
+                               boolean isDomain, PrivBitSet privSet) {
+        super(hostPattern, origHost, userPattern, user, isDomain, privSet);
+        this.ctlPattern = ctlPattern;
+        this.origCtl = origCtl;
+        if (origCtl.equals(ANY_CTL)) {
+            isAnyCtl = true;
+        }
+    }
+
+    public static CatalogPrivEntry create(String user, String host, String ctl, boolean isDomain, PrivBitSet privs)
+            throws AnalysisException {
+        PatternMatcher hostPattern = PatternMatcher.createMysqlPattern(host, CaseSensibility.HOST.getCaseSensibility());
+
+        PatternMatcher ctlPattern = createCtlPatternMatcher(ctl);
+
+        PatternMatcher userPattern = PatternMatcher.createFlatPattern(user, CaseSensibility.USER.getCaseSensibility());
+
+        if (privs.containsNodePriv() || privs.containsResourcePriv()) {
+            throw new AnalysisException("Datasource privilege can not contains node or resource privileges: " + privs);
+        }
+
+        return new CatalogPrivEntry(userPattern, user, hostPattern, host, ctlPattern, ctl, isDomain, privs);
+    }
+
+    private static PatternMatcher createCtlPatternMatcher(String ctl) throws AnalysisException {
+        boolean ctlCaseSensibility = CaseSensibility.CATALOG.getCaseSensibility();
+        return PatternMatcher.createFlatPattern(ctl, ctlCaseSensibility, ctl.equals(ANY_CTL));
+    }
+
+    public PatternMatcher getCtlPattern() {
+        return ctlPattern;
+    }
+
+    public String getOrigCtl() {
+        return origCtl;
+    }
+
+    public boolean isAnyCtl() {
+        return isAnyCtl;
+    }
+
+    @Override
+    public int compareTo(PrivEntry other) {
+        if (!(other instanceof CatalogPrivEntry)) {
+            throw new ClassCastException("cannot cast " + other.getClass().toString() + " to " + this.getClass());
+        }
+
+        CatalogPrivEntry otherEntry = (CatalogPrivEntry) other;
+        return compareAssist(origUser, otherEntry.origUser,
+                             origHost, otherEntry.origHost,
+                             origCtl, otherEntry.origCtl);
+    }
+
+    @Override
+    public boolean keyMatch(PrivEntry other) {
+        if (!(other instanceof CatalogPrivEntry)) {
+            return false;
+        }
+
+        CatalogPrivEntry otherEntry = (CatalogPrivEntry) other;
+        return origUser.equals(otherEntry.origUser) && origHost.equals(otherEntry.origHost)
+                && origCtl.equals(otherEntry.origCtl) && isDomain == otherEntry.isDomain;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("catalog privilege. user: %s, host: %s, ctl: %s, priv: %s, set by resolver: %b",
+                origUser, origHost, origCtl, privSet.toString(), isSetByDomainResolver);
+    }
+
+    @Override
+    public void write(DataOutput out) throws IOException {
+        if (!isClassNameWrote) {
+            String className = CatalogPrivEntry.class.getCanonicalName();
+            Text.writeString(out, className);
+            isClassNameWrote = true;
+        }
+        super.write(out);
+        Text.writeString(out, origCtl);
+        isClassNameWrote = false;
+    }
+
+    public void readFields(DataInput in) throws IOException {
+        super.readFields(in);
+
+        if (Catalog.getCurrentCatalogJournalVersion() >= FeMetaVersion.VERSION_111) {
+            origCtl = Text.readString(in);
+        } else {
+            origCtl = InternalDataSource.INTERNAL_DS_NAME;
+        }
+        try {
+            ctlPattern = createCtlPatternMatcher(origCtl);
+        } catch (AnalysisException e) {
+            throw new IOException(e);
+        }
+        isAnyCtl = origCtl.equals(ANY_CTL);
+    }
+
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/CatalogPrivTable.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/CatalogPrivTable.java
new file mode 100644
index 0000000000..a1febfbf37
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/CatalogPrivTable.java
@@ -0,0 +1,73 @@
+// 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.mysql.privilege;
+
+import org.apache.doris.analysis.UserIdentity;
+import org.apache.doris.common.io.Text;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.DataOutput;
+import java.io.IOException;
+
+/*
+ * CatalogPrivTable saves all catalog level privs
+ */
+public class CatalogPrivTable extends PrivTable {
+    private static final Logger LOG = LogManager.getLogger(CatalogPrivTable.class);
+
+    /*
+     * Return first priv which match the user@host on ctl.* The returned priv will be
+     * saved in 'savedPrivs'.
+     */
+    public void getPrivs(UserIdentity currentUser, String ctl, PrivBitSet savedPrivs) {
+        CatalogPrivEntry matchedEntry = null;
+        for (PrivEntry entry : entries) {
+            CatalogPrivEntry dsPrivEntry = (CatalogPrivEntry) entry;
+
+            if (!dsPrivEntry.match(currentUser, true)) {
+                continue;
+            }
+
+            // check catalog
+            if (!dsPrivEntry.isAnyCtl() && !dsPrivEntry.getCtlPattern().match(ctl)) {
+                continue;
+            }
+
+            matchedEntry = dsPrivEntry;
+            break;
+        }
+        if (matchedEntry == null) {
+            return;
+        }
+
+        savedPrivs.or(matchedEntry.getPrivSet());
+    }
+
+    @Override
+    public void write(DataOutput out) throws IOException {
+        if (!isClassNameWrote) {
+            String className = CatalogPrivTable.class.getCanonicalName();
+            Text.writeString(out, className);
+            isClassNameWrote = true;
+        }
+
+        super.write(out);
+    }
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/DbPrivEntry.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/DbPrivEntry.java
index a8f1337df7..826bbe2a98 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/DbPrivEntry.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/DbPrivEntry.java
@@ -28,7 +28,7 @@ import java.io.DataInput;
 import java.io.DataOutput;
 import java.io.IOException;
 
-public class DbPrivEntry extends PrivEntry {
+public class DbPrivEntry extends CatalogPrivEntry {
     protected static final String ANY_DB = "*";
 
     protected PatternMatcher dbPattern;
@@ -38,9 +38,12 @@ public class DbPrivEntry extends PrivEntry {
     protected DbPrivEntry() {
     }
 
-    protected DbPrivEntry(PatternMatcher hostPattern, String origHost, PatternMatcher dbPattern, String origDb,
-            PatternMatcher userPattern, String user, boolean isDomain, PrivBitSet privSet) {
-        super(hostPattern, origHost, userPattern, user, isDomain, privSet);
+    protected DbPrivEntry(PatternMatcher userPattern, String user,
+                          PatternMatcher hostPattern, String origHost,
+                          PatternMatcher ctlPattern, String origCtl,
+                          PatternMatcher dbPattern, String origDb,
+                          boolean isDomain, PrivBitSet privSet) {
+        super(userPattern, user, hostPattern, origHost, ctlPattern, origCtl, isDomain, privSet);
         this.dbPattern = dbPattern;
         this.origDb = origDb;
         if (origDb.equals(ANY_DB)) {
@@ -48,10 +51,15 @@ public class DbPrivEntry extends PrivEntry {
         }
     }
 
-    public static DbPrivEntry create(String host, String db, String user, boolean isDomain, PrivBitSet privs)
-            throws AnalysisException {
+    public static DbPrivEntry create(
+            String user, String host,
+            String ctl, String db,
+            boolean isDomain, PrivBitSet privs) throws AnalysisException {
         PatternMatcher hostPattern = PatternMatcher.createMysqlPattern(host, CaseSensibility.HOST.getCaseSensibility());
 
+        PatternMatcher ctlPattern = PatternMatcher.createFlatPattern(
+                ctl, CaseSensibility.CATALOG.getCaseSensibility(), ctl.equals(ANY_CTL));
+
         PatternMatcher dbPattern = createDbPatternMatcher(db);
 
         PatternMatcher userPattern = PatternMatcher.createFlatPattern(user, CaseSensibility.USER.getCaseSensibility());
@@ -60,7 +68,7 @@ public class DbPrivEntry extends PrivEntry {
             throw new AnalysisException("Db privilege can not contains global or resource privileges: " + privs);
         }
 
-        return new DbPrivEntry(hostPattern, host, dbPattern, db, userPattern, user, isDomain, privs);
+        return new DbPrivEntry(userPattern, user, hostPattern, host, ctlPattern, ctl, dbPattern, db, isDomain, privs);
     }
 
     private static PatternMatcher createDbPatternMatcher(String db) throws AnalysisException {
@@ -92,17 +100,10 @@ public class DbPrivEntry extends PrivEntry {
         }
 
         DbPrivEntry otherEntry = (DbPrivEntry) other;
-        int res = origHost.compareTo(otherEntry.origHost);
-        if (res != 0) {
-            return -res;
-        }
-
-        res = origDb.compareTo(otherEntry.origDb);
-        if (res != 0) {
-            return -res;
-        }
-
-        return -origUser.compareTo(otherEntry.origUser);
+        return compareAssist(origUser, otherEntry.origUser,
+                             origHost, otherEntry.origHost,
+                             origCtl, otherEntry.origCtl,
+                             origDb, otherEntry.origDb);
     }
 
     @Override
@@ -112,20 +113,15 @@ public class DbPrivEntry extends PrivEntry {
         }
 
         DbPrivEntry otherEntry = (DbPrivEntry) other;
-        if (origHost.equals(otherEntry.origHost) && origUser.equals(otherEntry.origUser)
-                && origDb.equals(otherEntry.origDb) && isDomain == otherEntry.isDomain) {
-            return true;
-        }
-        return false;
+        return origUser.equals(otherEntry.origUser) && origHost.equals(otherEntry.origHost)
+                && origCtl.equals(otherEntry.origCtl) && origDb.equals(otherEntry.origDb)
+                && isDomain == otherEntry.isDomain;
     }
 
     @Override
     public String toString() {
-        StringBuilder sb = new StringBuilder();
-        sb.append("db priv. host: ").append(origHost).append(", db: ").append(origDb);
-        sb.append(", user: ").append(origUser);
-        sb.append(", priv: ").append(privSet).append(", set by resolver: ").append(isSetByDomainResolver);
-        return sb.toString();
+        return String.format("database privilege. user: %s, host: %s, ctl: %s, db: %s, priv: %s, set by resolver: %b",
+                origUser, origHost, origCtl, origDb, privSet.toString(), isSetByDomainResolver);
     }
 
     @Override
diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/DbPrivTable.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/DbPrivTable.java
index a16c8dab9f..87ef9ad50e 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/DbPrivTable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/DbPrivTable.java
@@ -34,10 +34,10 @@ public class DbPrivTable extends PrivTable {
     private static final Logger LOG = LogManager.getLogger(DbPrivTable.class);
 
     /*
-     * Return first priv which match the user@host on db.* The returned priv will be
+     * Return first priv which match the user@host on ctl.db.* The returned priv will be
      * saved in 'savedPrivs'.
      */
-    public void getPrivs(UserIdentity currentUser, String db, PrivBitSet savedPrivs) {
+    public void getPrivs(UserIdentity currentUser, String ctl, String db, PrivBitSet savedPrivs) {
         DbPrivEntry matchedEntry = null;
         for (PrivEntry entry : entries) {
             DbPrivEntry dbPrivEntry = (DbPrivEntry) entry;
@@ -46,6 +46,11 @@ public class DbPrivTable extends PrivTable {
                 continue;
             }
 
+            // check catalog
+            if (!dbPrivEntry.isAnyCtl() && !dbPrivEntry.getCtlPattern().match(ctl)) {
+                continue;
+            }
+
             // check db
             if (!dbPrivEntry.isAnyDb() && !dbPrivEntry.getDbPattern().match(db)) {
                 continue;
@@ -61,28 +66,6 @@ public class DbPrivTable extends PrivTable {
         savedPrivs.or(matchedEntry.getPrivSet());
     }
 
-    /*
-     * Check if user@host has specified privilege on any database
-     */
-    public boolean hasPriv(String host, String user, PrivPredicate wanted) {
-        for (PrivEntry entry : entries) {
-            DbPrivEntry dbPrivEntry = (DbPrivEntry) entry;
-            // check host
-            if (!dbPrivEntry.isAnyHost() && !dbPrivEntry.getHostPattern().match(host)) {
-                continue;
-            }
-            // check user
-            if (!dbPrivEntry.isAnyUser() && !dbPrivEntry.getUserPattern().match(user)) {
-                continue;
-            }
-            // check priv
-            if (dbPrivEntry.privSet.satisfy(wanted)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     public boolean hasClusterPriv(ConnectContext ctx, String clusterName) {
         for (PrivEntry entry : entries) {
             DbPrivEntry dbPrivEntry = (DbPrivEntry) entry;
diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/PaloAuth.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/PaloAuth.java
index 2dcb317ff8..9459f14ed9 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/PaloAuth.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/PaloAuth.java
@@ -27,6 +27,7 @@ import org.apache.doris.analysis.RevokeStmt;
 import org.apache.doris.analysis.SetLdapPassVar;
 import org.apache.doris.analysis.SetPassVar;
 import org.apache.doris.analysis.SetUserPropertyStmt;
+import org.apache.doris.analysis.TableName;
 import org.apache.doris.analysis.TablePattern;
 import org.apache.doris.analysis.UserIdentity;
 import org.apache.doris.catalog.AuthorizationInfo;
@@ -42,6 +43,7 @@ import org.apache.doris.common.LdapConfig;
 import org.apache.doris.common.Pair;
 import org.apache.doris.common.UserException;
 import org.apache.doris.common.io.Writable;
+import org.apache.doris.datasource.InternalDataSource;
 import org.apache.doris.ldap.LdapPrivsChecker;
 import org.apache.doris.load.DppConfig;
 import org.apache.doris.persist.LdapInfo;
@@ -53,6 +55,7 @@ import org.apache.doris.thrift.TPrivilegeStatus;
 
 import com.google.common.base.Joiner;
 import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import org.apache.logging.log4j.LogManager;
@@ -65,6 +68,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Collectors;
 
 public class PaloAuth implements Writable {
     private static final Logger LOG = LogManager.getLogger(PaloAuth.class);
@@ -75,8 +79,10 @@ public class PaloAuth implements Writable {
     public static final String ADMIN_USER = "admin";
     // unknown user does not have any privilege, this is just to be compatible with old version.
     public static final String UNKNOWN_USER = "unknown";
+    private static final String DEFAULT_CATALOG = InternalDataSource.INTERNAL_DS_NAME;
 
     private UserPrivTable userPrivTable = new UserPrivTable();
+    private CatalogPrivTable catalogPrivTable = new CatalogPrivTable();
     private DbPrivTable dbPrivTable = new DbPrivTable();
     private TablePrivTable tablePrivTable = new TablePrivTable();
     private ResourcePrivTable resourcePrivTable = new ResourcePrivTable();
@@ -105,7 +111,7 @@ public class PaloAuth implements Writable {
     }
 
     public enum PrivLevel {
-        GLOBAL, DATABASE, TABLE, RESOURCE
+        GLOBAL, CATALOG, DATABASE, TABLE, RESOURCE
     }
 
     public PaloAuth() {
@@ -165,12 +171,39 @@ public class PaloAuth implements Writable {
                 false /* not delete entry if priv is empty, because global priv entry has password */);
     }
 
-    private void grantDbPrivs(UserIdentity userIdentity, String db, boolean errOnExist, boolean errOnNonExist,
-            PrivBitSet privs) throws DdlException {
+    private void grantCatalogPrivs(UserIdentity userIdentity, String ctl,
+            boolean errOnExist, boolean errOnNonExist, PrivBitSet privs) throws DdlException {
+        CatalogPrivEntry entry;
+        try {
+            entry = CatalogPrivEntry.create(userIdentity.getQualifiedUser(), userIdentity.getHost(),
+                    ctl, userIdentity.isDomain(), privs);
+            entry.setSetByDomainResolver(false);
+        } catch (AnalysisException e) {
+            throw new DdlException(e.getMessage());
+        }
+        catalogPrivTable.addEntry(entry, errOnExist, errOnNonExist);
+    }
+
+    private void revokeCatalogPrivs(UserIdentity userIdentity, String ctl,
+            PrivBitSet privs, boolean errOnNonExist) throws DdlException {
+        CatalogPrivEntry entry;
+        try {
+            entry = CatalogPrivEntry.create(userIdentity.getQualifiedUser(), userIdentity.getHost(),
+                    ctl, userIdentity.isDomain(), privs);
+            entry.setSetByDomainResolver(false);
+        } catch (AnalysisException e) {
+            throw new DdlException(e.getMessage());
+        }
+
+        catalogPrivTable.revoke(entry, errOnNonExist, true /* delete entry when empty */);
+    }
+
+    private void grantDbPrivs(UserIdentity userIdentity, String ctl, String db,
+            boolean errOnExist, boolean errOnNonExist, PrivBitSet privs) throws DdlException {
         DbPrivEntry entry;
         try {
-            entry = DbPrivEntry.create(userIdentity.getHost(), db, userIdentity.getQualifiedUser(),
-                    userIdentity.isDomain(), privs);
+            entry = DbPrivEntry.create(userIdentity.getQualifiedUser(), userIdentity.getHost(),
+                    ctl, db, userIdentity.isDomain(), privs);
             entry.setSetByDomainResolver(false);
         } catch (AnalysisException e) {
             throw new DdlException(e.getMessage());
@@ -178,12 +211,12 @@ public class PaloAuth implements Writable {
         dbPrivTable.addEntry(entry, errOnExist, errOnNonExist);
     }
 
-    private void revokeDbPrivs(UserIdentity userIdentity, String db, PrivBitSet privs, boolean errOnNonExist)
-            throws DdlException {
+    private void revokeDbPrivs(UserIdentity userIdentity, String ctl, String db,
+            PrivBitSet privs, boolean errOnNonExist) throws DdlException {
         DbPrivEntry entry;
         try {
-            entry = DbPrivEntry.create(userIdentity.getHost(), db, userIdentity.getQualifiedUser(),
-                    userIdentity.isDomain(), privs);
+            entry = DbPrivEntry.create(userIdentity.getQualifiedUser(), userIdentity.getHost(),
+                    ctl, db, userIdentity.isDomain(), privs);
             entry.setSetByDomainResolver(false);
         } catch (AnalysisException e) {
             throw new DdlException(e.getMessage());
@@ -192,12 +225,12 @@ public class PaloAuth implements Writable {
         dbPrivTable.revoke(entry, errOnNonExist, true /* delete entry when empty */);
     }
 
-    private void grantTblPrivs(UserIdentity userIdentity, String db, String tbl, boolean errOnExist,
-            boolean errOnNonExist, PrivBitSet privs) throws DdlException {
+    private void grantTblPrivs(UserIdentity userIdentity, String ctl, String db, String tbl,
+            boolean errOnExist, boolean errOnNonExist, PrivBitSet privs) throws DdlException {
         TablePrivEntry entry;
         try {
-            entry = TablePrivEntry.create(userIdentity.getHost(), db, userIdentity.getQualifiedUser(), tbl,
-                    userIdentity.isDomain(), privs);
+            entry = TablePrivEntry.create(userIdentity.getQualifiedUser(), userIdentity.getHost(),
+                    ctl, db, tbl, userIdentity.isDomain(), privs);
             entry.setSetByDomainResolver(false);
         } catch (AnalysisException e) {
             throw new DdlException(e.getMessage());
@@ -205,12 +238,12 @@ public class PaloAuth implements Writable {
         tablePrivTable.addEntry(entry, errOnExist, errOnNonExist);
     }
 
-    private void revokeTblPrivs(UserIdentity userIdentity, String db, String tbl, PrivBitSet privs,
-            boolean errOnNonExist) throws DdlException {
+    private void revokeTblPrivs(UserIdentity userIdentity, String ctl, String db, String tbl,
+            PrivBitSet privs, boolean errOnNonExist) throws DdlException {
         TablePrivEntry entry;
         try {
-            entry = TablePrivEntry.create(userIdentity.getHost(), db, userIdentity.getQualifiedUser(), tbl,
-                    userIdentity.isDomain(), privs);
+            entry = TablePrivEntry.create(userIdentity.getQualifiedUser(), userIdentity.getHost(),
+                    ctl, db, tbl, userIdentity.isDomain(), privs);
             entry.setSetByDomainResolver(false);
         } catch (AnalysisException e) {
             throw new DdlException(e.getMessage());
@@ -324,11 +357,15 @@ public class PaloAuth implements Writable {
         return checkDbPriv(ctx.getCurrentUserIdentity(), qualifiedDb, wanted);
     }
 
+    public boolean checkDbPriv(UserIdentity currentUser, String db, PrivPredicate wanted) {
+        return checkDbPriv(currentUser, DEFAULT_CATALOG, db, wanted);
+    }
+
     /*
      * Check if 'user'@'host' on 'db' has 'wanted' priv.
      * If the given db is null, which means it will no check if database name is matched.
      */
-    public boolean checkDbPriv(UserIdentity currentUser, String db, PrivPredicate wanted) {
+    public boolean checkDbPriv(UserIdentity currentUser, String ctl, String db, PrivPredicate wanted) {
         if (!Config.enable_auth_check) {
             return true;
         }
@@ -340,12 +377,13 @@ public class PaloAuth implements Writable {
 
         PrivBitSet savedPrivs = PrivBitSet.of();
         if (checkGlobalInternal(currentUser, wanted, savedPrivs)
-                || checkDbInternal(currentUser, db, wanted, savedPrivs)) {
+                || checkCatalogInternal(currentUser, ctl, wanted, savedPrivs)
+                || checkDbInternal(currentUser, ctl, db, wanted, savedPrivs)) {
             return true;
         }
 
         // if user has any privs of table in this db, and the wanted priv is SHOW, return true
-        if (db != null && wanted == PrivPredicate.SHOW && checkTblWithDb(currentUser, db)) {
+        if (ctl != null && db != null && wanted == PrivPredicate.SHOW && checkTblWithDb(currentUser, ctl, db)) {
             return true;
         }
 
@@ -358,21 +396,31 @@ public class PaloAuth implements Writable {
      * So we have to check if user has any privs of tables in this database.
      * if so, the database should be visible to this user.
      */
-    private boolean checkTblWithDb(UserIdentity currentUser, String db) {
+    private boolean checkTblWithDb(UserIdentity currentUser, String ctl, String db) {
         readLock();
         try {
             return (isLdapAuthEnabled() && LdapPrivsChecker.hasPrivsOfDb(currentUser, db))
-                    || tablePrivTable.hasPrivsOfDb(currentUser, db);
+                    || tablePrivTable.hasPrivsOfDb(currentUser, ctl, db);
         } finally {
             readUnlock();
         }
     }
 
+    public boolean checkTblPriv(ConnectContext ctx, String qualifiedCtl,
+                                String qualifiedDb, String tbl, PrivPredicate wanted) {
+        return checkTblPriv(ctx.getCurrentUserIdentity(), qualifiedCtl, qualifiedDb, tbl, wanted);
+    }
+
     public boolean checkTblPriv(ConnectContext ctx, String qualifiedDb, String tbl, PrivPredicate wanted) {
-        return checkTblPriv(ctx.getCurrentUserIdentity(), qualifiedDb, tbl, wanted);
+        return checkTblPriv(ctx, DEFAULT_CATALOG, qualifiedDb, tbl, wanted);
     }
 
-    public boolean checkTblPriv(UserIdentity currentUser, String db, String tbl, PrivPredicate wanted) {
+    public boolean checkTblPriv(ConnectContext ctx, TableName tableName, PrivPredicate wanted) {
+        Preconditions.checkState(tableName.isFullyQualified());
+        return checkTblPriv(ctx, tableName.getCtl(), tableName.getDb(), wanted);
+    }
+
+    public boolean checkTblPriv(UserIdentity currentUser, String ctl, String db, String tbl, PrivPredicate wanted) {
         if (!Config.enable_auth_check) {
             return true;
         }
@@ -383,8 +431,9 @@ public class PaloAuth implements Writable {
 
         PrivBitSet savedPrivs = PrivBitSet.of();
         if (checkGlobalInternal(currentUser, wanted, savedPrivs)
-                || checkDbInternal(currentUser, db, wanted, savedPrivs)
-                || checkTblInternal(currentUser, db, tbl, wanted, savedPrivs)) {
+                || checkCatalogInternal(currentUser, ctl, wanted, savedPrivs)
+                || checkDbInternal(currentUser, ctl, db, wanted, savedPrivs)
+                || checkTblInternal(currentUser, ctl, db, tbl, wanted, savedPrivs)) {
             return true;
         }
 
@@ -392,6 +441,10 @@ public class PaloAuth implements Writable {
         return false;
     }
 
+    public boolean checkTblPriv(UserIdentity currentUser, String db, String tbl, PrivPredicate wanted) {
+        return checkTblPriv(currentUser, DEFAULT_CATALOG, db, tbl, wanted);
+    }
+
     public boolean checkResourcePriv(ConnectContext ctx, String resourceName, PrivPredicate wanted) {
         return checkResourcePriv(ctx.getCurrentUserIdentity(), resourceName, wanted);
     }
@@ -485,7 +538,22 @@ public class PaloAuth implements Writable {
         }
     }
 
-    private boolean checkDbInternal(UserIdentity currentUser, String db, PrivPredicate wanted,
+    private boolean checkCatalogInternal(UserIdentity currentUser, String ctl,
+                                         PrivPredicate wanted, PrivBitSet savedPrivs) {
+        // TODO(gaoxin): check privileges by ldap.
+        readLock();
+        try {
+            catalogPrivTable.getPrivs(currentUser, ctl, savedPrivs);
+            if (PaloPrivilege.satisfy(savedPrivs, wanted)) {
+                return true;
+            }
+        } finally {
+            readUnlock();
+        }
+        return false;
+    }
+
+    private boolean checkDbInternal(UserIdentity currentUser, String ctl, String db, PrivPredicate wanted,
                                     PrivBitSet savedPrivs) {
         if (isLdapAuthEnabled() && LdapPrivsChecker.hasDbPrivFromLdap(currentUser, db, wanted)) {
             return true;
@@ -493,7 +561,7 @@ public class PaloAuth implements Writable {
 
         readLock();
         try {
-            dbPrivTable.getPrivs(currentUser, db, savedPrivs);
+            dbPrivTable.getPrivs(currentUser, ctl, db, savedPrivs);
             if (PaloPrivilege.satisfy(savedPrivs, wanted)) {
                 return true;
             }
@@ -503,7 +571,7 @@ public class PaloAuth implements Writable {
         return false;
     }
 
-    private boolean checkTblInternal(UserIdentity currentUser, String db, String tbl,
+    private boolean checkTblInternal(UserIdentity currentUser, String ctl, String db, String tbl,
                                      PrivPredicate wanted, PrivBitSet savedPrivs) {
         if (isLdapAuthEnabled() && LdapPrivsChecker.hasTblPrivFromLdap(currentUser, db, tbl, wanted)) {
             return true;
@@ -511,7 +579,7 @@ public class PaloAuth implements Writable {
 
         readLock();
         try {
-            tablePrivTable.getPrivs(currentUser, db, tbl, savedPrivs);
+            tablePrivTable.getPrivs(currentUser, ctl, db, tbl, savedPrivs);
             if (PaloPrivilege.satisfy(savedPrivs, wanted)) {
                 return true;
             }
@@ -607,7 +675,7 @@ public class PaloAuth implements Writable {
 
             if (!userIdent.getQualifiedUser().equals(ROOT_USER) && !userIdent.getQualifiedUser().equals(ADMIN_USER)) {
                 // grant read privs to database information_schema
-                TablePattern tblPattern = new TablePattern(InfoSchemaDb.DATABASE_NAME, "*");
+                TablePattern tblPattern = new TablePattern(DEFAULT_CATALOG, InfoSchemaDb.DATABASE_NAME, "*");
                 try {
                     tblPattern.analyze(ClusterNamespace.getClusterNameFromFullName(userIdent.getQualifiedUser()));
                 } catch (AnalysisException e) {
@@ -681,6 +749,7 @@ public class PaloAuth implements Writable {
 
             // we don't check if user exists
             userPrivTable.dropUser(userIdent);
+            catalogPrivTable.dropUser(userIdent);
             dbPrivTable.dropUser(userIdent);
             tablePrivTable.dropUser(userIdent);
             resourcePrivTable.dropUser(userIdent);
@@ -815,14 +884,22 @@ public class PaloAuth implements Writable {
                                      errOnNonExist,
                                      privs);
                     break;
+                case CATALOG:
+                    grantCatalogPrivs(userIdent, tblPattern.getQualifiedCtl(),
+                                      false /* err on exist */,
+                                      false /* err on non exist */,
+                                      privs);
+                    break;
                 case DATABASE:
-                    grantDbPrivs(userIdent, tblPattern.getQualifiedDb(),
+                    grantDbPrivs(userIdent, tblPattern.getQualifiedCtl(),
+                                 tblPattern.getQualifiedDb(),
                                  false /* err on exist */,
                                  false /* err on non exist */,
                                  privs);
                     break;
                 case TABLE:
-                    grantTblPrivs(userIdent, tblPattern.getQualifiedDb(),
+                    grantTblPrivs(userIdent, tblPattern.getQualifiedCtl(),
+                                  tblPattern.getQualifiedDb(),
                                   tblPattern.getTbl(),
                                   false /* err on exist */,
                                   false /* err on non exist */,
@@ -971,12 +1048,16 @@ public class PaloAuth implements Writable {
                 case GLOBAL:
                     revokeGlobalPrivs(userIdent, privs, errOnNonExist);
                     break;
+                case CATALOG:
+                    revokeCatalogPrivs(userIdent, tblPattern.getQualifiedCtl(), privs, errOnNonExist);
+                    break;
                 case DATABASE:
-                    revokeDbPrivs(userIdent, tblPattern.getQualifiedDb(), privs, errOnNonExist);
+                    revokeDbPrivs(userIdent, tblPattern.getQualifiedCtl(),
+                            tblPattern.getQualifiedDb(), privs, errOnNonExist);
                     break;
                 case TABLE:
-                    revokeTblPrivs(userIdent, tblPattern.getQualifiedDb(), tblPattern.getTbl(), privs,
-                                   errOnNonExist);
+                    revokeTblPrivs(userIdent, tblPattern.getQualifiedCtl(), tblPattern.getQualifiedDb(),
+                            tblPattern.getTbl(), privs, errOnNonExist);
                     break;
                 default:
                     Preconditions.checkNotNull(null, tblPattern.getPrivLevel());
@@ -1311,6 +1392,17 @@ public class PaloAuth implements Writable {
             }
         }
 
+        // catalog
+        String ctlPrivs = catalogPrivTable.entries.stream()
+                .filter(entry -> entry.match(userIdent, true))
+                .map(entry -> String.format("%s: %s (%b)",
+                        ((CatalogPrivEntry) entry).getOrigCtl(), entry.privSet, entry.isSetByDomainResolver()))
+                .collect(Collectors.joining("; "));
+        if (Strings.isNullOrEmpty(ctlPrivs)) {
+            ctlPrivs = FeConstants.null_string;
+        }
+        userAuthInfo.add(ctlPrivs);
+
         // db
         List<String> dbPrivs = Lists.newArrayList();
         Set<String> addedDbs = Sets.newHashSet();
@@ -1326,16 +1418,16 @@ public class PaloAuth implements Writable {
             PrivBitSet savedPrivs = dEntry.getPrivSet().copy();
             savedPrivs.or(LdapPrivsChecker.getDbPrivFromLdap(userIdent, dEntry.getOrigDb()));
             addedDbs.add(dEntry.getOrigDb());
-            dbPrivs.add(dEntry.getOrigDb() + ": " + savedPrivs.toString()
-                    + " (" + entry.isSetByDomainResolver() + ")");
+            dbPrivs.add(String.format("%s.%s: %s (%b)", dEntry.getOrigCtl(), dEntry.getOrigDb(),
+                    savedPrivs, dEntry.isSetByDomainResolver()));
         }
         // Add privs from ldap groups that have not been added in Doris.
         if (LdapPrivsChecker.hasLdapPrivs(userIdent)) {
             Map<TablePattern, PrivBitSet> ldapDbPrivs = LdapPrivsChecker.getLdapAllDbPrivs(userIdent);
             for (Map.Entry<TablePattern, PrivBitSet> entry : ldapDbPrivs.entrySet()) {
                 if (!addedDbs.contains(entry.getKey().getQualifiedDb())) {
-                    dbPrivs.add(entry.getKey().getQualifiedDb() + ": "
-                            + entry.getValue().toString() + " (" + false + ")");
+                    dbPrivs.add(String.format("%s.%s: %s (%b)", entry.getKey().getQualifiedCtl(),
+                            entry.getKey().getQualifiedDb(), entry.getValue(), false));
                 }
             }
         }
@@ -1361,17 +1453,15 @@ public class PaloAuth implements Writable {
             PrivBitSet savedPrivs = tEntry.getPrivSet().copy();
             savedPrivs.or(LdapPrivsChecker.getTblPrivFromLdap(userIdent, tEntry.getOrigDb(), tEntry.getOrigTbl()));
             addedtbls.add(tEntry.getOrigDb().concat(".").concat(tEntry.getOrigTbl()));
-            tblPrivs.add(tEntry.getOrigDb() + "." + tEntry.getOrigTbl() + ": "
-                    + savedPrivs.toString()
-                    + " (" + entry.isSetByDomainResolver() + ")");
+            tblPrivs.add(String.format("%s.%s.%s: %s (%b)", tEntry.getOrigCtl(), tEntry.getOrigDb(),
+                    tEntry.getOrigTbl(), savedPrivs, tEntry.isSetByDomainResolver()));
         }
         // Add privs from ldap groups that have not been added in Doris.
         if (LdapPrivsChecker.hasLdapPrivs(userIdent)) {
             Map<TablePattern, PrivBitSet> ldapTblPrivs = LdapPrivsChecker.getLdapAllTblPrivs(userIdent);
             for (Map.Entry<TablePattern, PrivBitSet> entry : ldapTblPrivs.entrySet()) {
                 if (!addedtbls.contains(entry.getKey().getQualifiedDb().concat(".").concat(entry.getKey().getTbl()))) {
-                    tblPrivs.add(entry.getKey().getQualifiedDb().concat(".").concat(entry.getKey().getTbl())
-                            .concat(": ").concat(entry.getValue().toString()).concat(" (false)"));
+                    tblPrivs.add(String.format("%s: %s (%b)", entry.getKey(), entry.getValue(), false));
                 }
             }
         }
@@ -1662,6 +1752,7 @@ public class PaloAuth implements Writable {
         // role manager must be first, because role should be exist before any user
         roleManager.write(out);
         userPrivTable.write(out);
+        catalogPrivTable.write(out);
         dbPrivTable.write(out);
         tablePrivTable.write(out);
         resourcePrivTable.write(out);
@@ -1672,6 +1763,13 @@ public class PaloAuth implements Writable {
     public void readFields(DataInput in) throws IOException {
         roleManager = RoleManager.read(in);
         userPrivTable = (UserPrivTable) PrivTable.read(in);
+        if (Catalog.getCurrentCatalogJournalVersion() >= FeMetaVersion.VERSION_111) {
+            catalogPrivTable = (CatalogPrivTable) PrivTable.read(in);
+        } else {
+            catalogPrivTable = userPrivTable.degradeToInternalCatalogPriv();
+            LOG.info("Load PaloAuth from meta version < {}, degrade UserPrivTable to CatalogPrivTable",
+                    FeMetaVersion.VERSION_111);
+        }
         dbPrivTable = (DbPrivTable) PrivTable.read(in);
         tablePrivTable = (TablePrivTable) PrivTable.read(in);
         resourcePrivTable = (ResourcePrivTable) PrivTable.read(in);
diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/PrivEntry.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/PrivEntry.java
index 906c29b6da..8f59a59777 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/PrivEntry.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/PrivEntry.java
@@ -24,6 +24,7 @@ import org.apache.doris.common.PatternMatcher;
 import org.apache.doris.common.io.Text;
 import org.apache.doris.common.io.Writable;
 
+import com.google.common.base.Preconditions;
 import org.apache.commons.lang.NotImplementedException;
 
 import java.io.DataInput;
@@ -253,4 +254,19 @@ public abstract class PrivEntry implements Comparable<PrivEntry>, Writable {
     public int compareTo(PrivEntry o) {
         throw new NotImplementedException();
     }
+
+    /**
+     * Help derived classes compare in the order of 'user', 'host', 'catalog', 'db', 'ctl'.
+     * Compare strings[i] with strings[i+1] successively, return if the comparison value is not 0 in current loop.
+     */
+    protected static int compareAssist(String... strings) {
+        Preconditions.checkState(strings.length % 2 == 0);
+        for (int i = 0; i < strings.length; i += 2) {
+            int res = strings[i].compareTo(strings[i + 1]);
+            if (res != 0) {
+                return res;
+            }
+        }
+        return 0;
+    }
 }
diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/PrivTable.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/PrivTable.java
index ce2f7738c3..054ca2e62e 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/PrivTable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/PrivTable.java
@@ -45,6 +45,27 @@ public abstract class PrivTable implements Writable {
     // see PrivEntry for more detail
     protected boolean isClassNameWrote = false;
 
+    /*
+     * Check if user@host has specified privilege
+     */
+    public boolean hasPriv(String host, String user, PrivPredicate wanted) {
+        for (PrivEntry entry : entries) {
+            // check host
+            if (!entry.isAnyHost() && !entry.getHostPattern().match(host)) {
+                continue;
+            }
+            // check user
+            if (!entry.isAnyUser() && !entry.getUserPattern().match(user)) {
+                continue;
+            }
+            // check priv
+            if (entry.privSet.satisfy(wanted)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /*
      * Add an entry to priv table.
      * If entry already exists and errOnExist is false, we try to reset or merge the new priv entry with existing one.
diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/RoleManager.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/RoleManager.java
index 38a0683228..19e0c703f0 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/RoleManager.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/RoleManager.java
@@ -26,6 +26,7 @@ import org.apache.doris.common.io.Writable;
 import org.apache.doris.mysql.privilege.PaloAuth.PrivLevel;
 
 import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 
@@ -34,6 +35,9 @@ import java.io.DataOutput;
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 public class RoleManager implements Writable {
     private Map<String, PaloRole> roles = Maps.newHashMap();
@@ -132,60 +136,26 @@ public class RoleManager implements Writable {
             info.add(role.getRoleName());
             info.add(Joiner.on(", ").join(role.getUsers()));
 
-            // global
-            boolean hasGlobal = false;
-            for (Map.Entry<TablePattern, PrivBitSet> entry : role.getTblPatternToPrivs().entrySet()) {
-                if (entry.getKey().getPrivLevel() == PrivLevel.GLOBAL) {
-                    hasGlobal = true;
-                    info.add(entry.getValue().toString());
-                    // global priv should only has one
-                    break;
-                }
-            }
-            if (!hasGlobal) {
-                info.add(FeConstants.null_string);
-            }
-
-            // db
-            List<String> tmp = Lists.newArrayList();
-            for (Map.Entry<TablePattern, PrivBitSet> entry : role.getTblPatternToPrivs().entrySet()) {
-                if (entry.getKey().getPrivLevel() == PrivLevel.DATABASE) {
-                    tmp.add(entry.getKey().toString() + ": " + entry.getValue().toString());
-                }
-            }
-            if (tmp.isEmpty()) {
-                info.add(FeConstants.null_string);
-            } else {
-                info.add(Joiner.on("; ").join(tmp));
-            }
-
-
-            // tbl
-            tmp.clear();
-            for (Map.Entry<TablePattern, PrivBitSet> entry : role.getTblPatternToPrivs().entrySet()) {
-                if (entry.getKey().getPrivLevel() == PrivLevel.TABLE) {
-                    tmp.add(entry.getKey().toString() + ": " + entry.getValue().toString());
-                }
-            }
-            if (tmp.isEmpty()) {
-                info.add(FeConstants.null_string);
-            } else {
-                info.add(Joiner.on("; ").join(tmp));
-            }
-
-            // resource
-            tmp.clear();
-            for (Map.Entry<ResourcePattern, PrivBitSet> entry : role.getResourcePatternToPrivs().entrySet()) {
-                if (entry.getKey().getPrivLevel() == PrivLevel.RESOURCE) {
-                    tmp.add(entry.getKey().toString() + ": " + entry.getValue().toString());
-                }
-            }
-            if (tmp.isEmpty()) {
-                info.add(FeConstants.null_string);
-            } else {
-                info.add(Joiner.on("; ").join(tmp));
-            }
-
+            Map<PrivLevel, String> infoMap = role.getTblPatternToPrivs().entrySet().stream()
+                    .collect(Collectors.groupingBy(entry -> entry.getKey().getPrivLevel())).entrySet().stream()
+                    .collect(Collectors.toMap(Entry::getKey, entry -> {
+                        if (entry.getKey() == PrivLevel.GLOBAL) {
+                            return entry.getValue().stream().findFirst().map(priv -> priv.getValue().toString())
+                                    .orElse(FeConstants.null_string);
+                        } else {
+                            return entry.getValue().stream()
+                                    .map(priv -> priv.getKey() + ": " + priv.getValue())
+                                    .collect(Collectors.joining("; "));
+                        }
+                    }));
+            Stream.of(PrivLevel.GLOBAL, PrivLevel.CATALOG, PrivLevel.DATABASE, PrivLevel.TABLE, PrivLevel.RESOURCE)
+                    .forEach(level -> {
+                        String infoItem = infoMap.get(level);
+                        if (Strings.isNullOrEmpty(infoItem)) {
+                            infoItem = FeConstants.null_string;
+                        }
+                        info.add(infoItem);
+                    });
             results.add(info);
         }
     }
diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/TablePrivEntry.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/TablePrivEntry.java
index c85a1f2912..7304d31922 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/TablePrivEntry.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/TablePrivEntry.java
@@ -36,10 +36,13 @@ public class TablePrivEntry extends DbPrivEntry {
     protected TablePrivEntry() {
     }
 
-    private TablePrivEntry(PatternMatcher hostPattern, String origHost, PatternMatcher dbPattern, String origDb,
-            PatternMatcher userPattern, String user, PatternMatcher tblPattern, String origTbl,
-            boolean isDomain, PrivBitSet privSet) {
-        super(hostPattern, origHost, dbPattern, origDb, userPattern, user, isDomain, privSet);
+    private TablePrivEntry(PatternMatcher userPattern, String user,
+                           PatternMatcher hostPattern, String origHost,
+                           PatternMatcher ctlPattern, String origCtl,
+                           PatternMatcher dbPattern, String origDb,
+                           PatternMatcher tblPattern, String origTbl,
+                           boolean isDomain, PrivBitSet privSet) {
+        super(userPattern, user, hostPattern, origHost, ctlPattern, origCtl, dbPattern, origDb, isDomain, privSet);
         this.tblPattern = tblPattern;
         this.origTbl = origTbl;
         if (origTbl.equals(ANY_TBL)) {
@@ -47,12 +50,15 @@ public class TablePrivEntry extends DbPrivEntry {
         }
     }
 
-    public static TablePrivEntry create(String host, String db, String user, String tbl, boolean isDomain,
-            PrivBitSet privs) throws AnalysisException {
+    public static TablePrivEntry create(String user, String host,
+            String ctl, String db, String tbl,
+            boolean isDomain, PrivBitSet privs) throws AnalysisException {
         PatternMatcher hostPattern = PatternMatcher.createMysqlPattern(host, CaseSensibility.HOST.getCaseSensibility());
         PatternMatcher dbPattern = PatternMatcher.createFlatPattern(
                 db, CaseSensibility.DATABASE.getCaseSensibility(), db.equals(ANY_DB));
         PatternMatcher userPattern = PatternMatcher.createFlatPattern(user, CaseSensibility.USER.getCaseSensibility());
+        PatternMatcher ctlPattern = PatternMatcher.createFlatPattern(
+                ctl, CaseSensibility.CATALOG.getCaseSensibility(), ctl.equals(ANY_CTL));
 
         PatternMatcher tblPattern = PatternMatcher.createFlatPattern(
                 tbl, CaseSensibility.TABLE.getCaseSensibility(), tbl.equals(ANY_TBL));
@@ -61,8 +67,8 @@ public class TablePrivEntry extends DbPrivEntry {
             throw new AnalysisException("Table privilege can not contains global or resource privileges: " + privs);
         }
 
-        return new TablePrivEntry(hostPattern, host, dbPattern, db,
-                userPattern, user, tblPattern, tbl, isDomain, privs);
+        return new TablePrivEntry(userPattern, user, hostPattern, host,
+                ctlPattern, ctl, dbPattern, db, tblPattern, tbl, isDomain, privs);
     }
 
     public PatternMatcher getTblPattern() {
@@ -84,22 +90,11 @@ public class TablePrivEntry extends DbPrivEntry {
         }
 
         TablePrivEntry otherEntry = (TablePrivEntry) other;
-        int res = origHost.compareTo(otherEntry.origHost);
-        if (res != 0) {
-            return -res;
-        }
-
-        res = origDb.compareTo(otherEntry.origDb);
-        if (res != 0) {
-            return -res;
-        }
-
-        res = origUser.compareTo(otherEntry.origUser);
-        if (res != 0) {
-            return -res;
-        }
-
-        return -origTbl.compareTo(otherEntry.origTbl);
+        return compareAssist(origUser, otherEntry.origUser,
+                             origHost, otherEntry.origHost,
+                             origCtl, otherEntry.origCtl,
+                             origDb, otherEntry.origDb,
+                             origTbl, otherEntry.origTbl);
     }
 
     @Override
@@ -109,21 +104,16 @@ public class TablePrivEntry extends DbPrivEntry {
         }
 
         TablePrivEntry otherEntry = (TablePrivEntry) other;
-        if (origHost.equals(otherEntry.origHost) && origUser.equals(otherEntry.origUser)
-                && origDb.equals(otherEntry.origDb) && origTbl.equals(otherEntry.origTbl)
-                && isDomain == otherEntry.isDomain) {
-            return true;
-        }
-        return false;
+        return origUser.equals(otherEntry.origUser) && origHost.equals(otherEntry.origHost)
+                && origCtl.equals(otherEntry.origCtl) && origDb.equals(otherEntry.origDb)
+                && origTbl.equals(otherEntry.origTbl) && isDomain == otherEntry.isDomain;
     }
 
     @Override
     public String toString() {
-        StringBuilder sb = new StringBuilder();
-        sb.append("db priv. host: ").append(origHost).append(", db: ").append(origDb);
-        sb.append(", user: ").append(origUser).append(", tbl: ").append(origTbl);
-        sb.append(", priv: ").append(privSet).append(", set by resolver: ").append(isSetByDomainResolver);
-        return sb.toString();
+        return String.format("table privilege. user: %s, host: %s, "
+                        + "ctl: %s, db: %s, tbl: %s, priv: %s, set by resolver: %b",
+                origUser, origHost, origCtl, origDb, origTbl, privSet.toString(), isSetByDomainResolver);
     }
 
     @Override
diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/TablePrivTable.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/TablePrivTable.java
index 0de7816981..475452ac29 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/TablePrivTable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/TablePrivTable.java
@@ -32,10 +32,10 @@ import java.io.IOException;
 public class TablePrivTable extends PrivTable {
 
     /*
-     * Return first priv which match the user@host on db.tbl The returned priv will
+     * Return first priv which match the user@host on ctl.db.tbl The returned priv will
      * be saved in 'savedPrivs'.
      */
-    public void getPrivs(UserIdentity currentUser, String db, String tbl, PrivBitSet savedPrivs) {
+    public void getPrivs(UserIdentity currentUser, String ctl, String db, String tbl, PrivBitSet savedPrivs) {
         TablePrivEntry matchedEntry = null;
         for (PrivEntry entry : entries) {
             TablePrivEntry tblPrivEntry = (TablePrivEntry) entry;
@@ -43,6 +43,11 @@ public class TablePrivTable extends PrivTable {
                 continue;
             }
 
+            // check catalog
+            if (!tblPrivEntry.isAnyCtl() && !tblPrivEntry.getCtlPattern().match(ctl)) {
+                continue;
+            }
+
             // check db
             Preconditions.checkState(!tblPrivEntry.isAnyDb());
             if (!tblPrivEntry.getDbPattern().match(db)) {
@@ -64,33 +69,17 @@ public class TablePrivTable extends PrivTable {
         savedPrivs.or(matchedEntry.getPrivSet());
     }
 
-    /*
-     * Check if user@host has specified privilege on any table
-     */
-    public boolean hasPriv(String host, String user, PrivPredicate wanted) {
+    public boolean hasPrivsOfDb(UserIdentity currentUser, String ctl, String db) {
         for (PrivEntry entry : entries) {
             TablePrivEntry tblPrivEntry = (TablePrivEntry) entry;
-            // check host
-            if (!tblPrivEntry.isAnyHost() && !tblPrivEntry.getHostPattern().match(host)) {
-                continue;
-            }
-            // check user
-            if (!tblPrivEntry.isAnyUser() && !tblPrivEntry.getUserPattern().match(user)) {
+
+            if (!tblPrivEntry.match(currentUser, true)) {
                 continue;
             }
-            // check priv
-            if (tblPrivEntry.privSet.satisfy(wanted)) {
-                return true;
-            }
-        }
-        return false;
-    }
 
-    public boolean hasPrivsOfDb(UserIdentity currentUser, String db) {
-        for (PrivEntry entry : entries) {
-            TablePrivEntry tblPrivEntry = (TablePrivEntry) entry;
-
-            if (!tblPrivEntry.match(currentUser, true)) {
+            // check catalog
+            Preconditions.checkState(!tblPrivEntry.isAnyCtl());
+            if (!tblPrivEntry.getCtlPattern().match(ctl)) {
                 continue;
             }
 
diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/UserPrivTable.java b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/UserPrivTable.java
index 2ae20c3658..c50f5569bf 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/UserPrivTable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/UserPrivTable.java
@@ -20,6 +20,7 @@ package org.apache.doris.mysql.privilege;
 import org.apache.doris.analysis.UserIdentity;
 import org.apache.doris.common.DdlException;
 import org.apache.doris.common.io.Text;
+import org.apache.doris.datasource.InternalDataSource;
 import org.apache.doris.mysql.MysqlPassword;
 
 import org.apache.logging.log4j.LogManager;
@@ -27,6 +28,7 @@ import org.apache.logging.log4j.Logger;
 
 import java.io.DataOutput;
 import java.io.IOException;
+import java.util.LinkedList;
 import java.util.List;
 
 /*
@@ -57,27 +59,6 @@ public class UserPrivTable extends PrivTable {
         savedPrivs.or(matchedEntry.getPrivSet());
     }
 
-    /*
-     * Check if user@host has specified privilege
-     */
-    public boolean hasPriv(String host, String user, PrivPredicate wanted) {
-        for (PrivEntry entry : entries) {
-            GlobalPrivEntry globalPrivEntry = (GlobalPrivEntry) entry;
-            // check host
-            if (!globalPrivEntry.isAnyHost() && !globalPrivEntry.getHostPattern().match(host)) {
-                continue;
-            }
-            // check user
-            if (!globalPrivEntry.isAnyUser() && !globalPrivEntry.getUserPattern().match(user)) {
-                continue;
-            }
-            if (globalPrivEntry.getPrivSet().satisfy(wanted)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     // validate the connection by host, user and password.
     // return true if this connection is valid, and 'savedPrivs' save all global privs got from user table.
     // if currentUser is not null, save the current user identity
@@ -196,4 +177,33 @@ public class UserPrivTable extends PrivTable {
 
         super.write(out);
     }
+
+    /**
+     * When replay UserPrivTable from journal whose FeMetaVersion < VERSION_111, the global-level privileges should
+     * degrade to internal-catalog-level privileges.
+     */
+    public CatalogPrivTable degradeToInternalCatalogPriv() throws IOException {
+        CatalogPrivTable catalogPrivTable = new CatalogPrivTable();
+        List<PrivEntry> degradedEntries = new LinkedList<>();
+        for (PrivEntry privEntry : entries) {
+            GlobalPrivEntry globalPrivEntry = (GlobalPrivEntry) privEntry;
+            if (!globalPrivEntry.match(UserIdentity.ROOT, true)
+                    && !globalPrivEntry.match(UserIdentity.ADMIN, true)
+                    && !globalPrivEntry.privSet.isEmpty()) {
+                try {
+                    CatalogPrivEntry entry = CatalogPrivEntry.create(globalPrivEntry.origUser, globalPrivEntry.origHost,
+                            InternalDataSource.INTERNAL_DS_NAME, globalPrivEntry.isDomain, globalPrivEntry.privSet);
+                    entry.setSetByDomainResolver(false);
+                    catalogPrivTable.addEntry(entry, false, false);
+                    degradedEntries.add(globalPrivEntry);
+                } catch (Exception e) {
+                    throw new IOException(e.getMessage());
+                }
+            }
+        }
+        for (PrivEntry degraded : degradedEntries) {
+            dropEntry(degraded);
+        }
+        return catalogPrivTable;
+    }
 }
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/ConnectContext.java b/fe/fe-core/src/main/java/org/apache/doris/qe/ConnectContext.java
index 31f71ecb40..724278af10 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/ConnectContext.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/ConnectContext.java
@@ -23,6 +23,7 @@ import org.apache.doris.catalog.Database;
 import org.apache.doris.cluster.ClusterNamespace;
 import org.apache.doris.common.UserException;
 import org.apache.doris.common.util.DebugUtil;
+import org.apache.doris.datasource.InternalDataSource;
 import org.apache.doris.datasource.SessionContext;
 import org.apache.doris.mysql.MysqlCapability;
 import org.apache.doris.mysql.MysqlChannel;
@@ -108,6 +109,7 @@ public class ConnectContext {
     // Catalog: put catalog here is convenient for unit test,
     // because catalog is singleton, hard to mock
     protected Catalog catalog;
+    protected String defaultCatalog = InternalDataSource.INTERNAL_DS_NAME;
     protected boolean isSend;
 
     protected AuditEventBuilder auditEventBuilder = new AuditEventBuilder();
@@ -290,6 +292,7 @@ public class ConnectContext {
 
     public void setCatalog(Catalog catalog) {
         this.catalog = catalog;
+        defaultCatalog = catalog.getInternalDataSource().getName();
     }
 
     public Catalog getCatalog() {
@@ -410,6 +413,14 @@ public class ConnectContext {
         return serverCapability;
     }
 
+    public String getDefaultCatalog() {
+        return defaultCatalog;
+    }
+
+    public void changeDefaultCatalog(String catalogName) {
+        defaultCatalog = catalogName;
+    }
+
     public String getDatabase() {
         return currentDb;
     }
diff --git a/fe/fe-core/src/test/java/org/apache/doris/analysis/AccessTestUtil.java b/fe/fe-core/src/test/java/org/apache/doris/analysis/AccessTestUtil.java
index 8054dba45b..19043643af 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/analysis/AccessTestUtil.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/analysis/AccessTestUtil.java
@@ -33,6 +33,7 @@ import org.apache.doris.catalog.SinglePartitionInfo;
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.DdlException;
 import org.apache.doris.common.jmockit.Deencapsulation;
+import org.apache.doris.datasource.InternalDataSource;
 import org.apache.doris.load.Load;
 import org.apache.doris.mysql.privilege.PaloAuth;
 import org.apache.doris.mysql.privilege.PrivPredicate;
@@ -319,6 +320,10 @@ public class AccessTestUtil {
         Analyzer analyzer = new Analyzer(fetchAdminCatalog(), new ConnectContext(null));
         new Expectations(analyzer) {
             {
+                analyzer.getDefaultCatalog();
+                minTimes = 0;
+                result = InternalDataSource.INTERNAL_DS_NAME;
+
                 analyzer.getDefaultDb();
                 minTimes = 0;
                 result = withCluster ? prefix + "testDb" : "testDb";
@@ -351,6 +356,10 @@ public class AccessTestUtil {
         Analyzer analyzer = new Analyzer(fetchBlockCatalog(), new ConnectContext(null));
         new Expectations(analyzer) {
             {
+                analyzer.getDefaultCatalog();
+                minTimes = 0;
+                result = InternalDataSource.INTERNAL_DS_NAME;
+
                 analyzer.getDefaultDb();
                 minTimes = 0;
                 result = "testCluster:testDb";
@@ -371,6 +380,10 @@ public class AccessTestUtil {
         Analyzer analyzer = new Analyzer(fetchBlockCatalog(), new ConnectContext(null));
         new Expectations(analyzer) {
             {
+                analyzer.getDefaultCatalog();
+                minTimes = 0;
+                result = InternalDataSource.INTERNAL_DS_NAME;
+
                 analyzer.getDefaultDb();
                 minTimes = 0;
                 result = "";
@@ -479,6 +492,10 @@ public class AccessTestUtil {
         Analyzer analyzer = new Analyzer(catalog, new ConnectContext(null));
         new Expectations(analyzer) {
             {
+                analyzer.getDefaultCatalog();
+                minTimes = 0;
+                result = InternalDataSource.INTERNAL_DS_NAME;
+
                 analyzer.getDefaultDb();
                 minTimes = 0;
                 result = "testDb";
diff --git a/fe/fe-core/src/test/java/org/apache/doris/analysis/DropTableStmtTest.java b/fe/fe-core/src/test/java/org/apache/doris/analysis/DropTableStmtTest.java
index 7204d4b3b9..7a6fdaa437 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/analysis/DropTableStmtTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/analysis/DropTableStmtTest.java
@@ -19,6 +19,7 @@ package org.apache.doris.analysis;
 
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.UserException;
+import org.apache.doris.datasource.InternalDataSource;
 import org.apache.doris.mysql.privilege.MockedAuth;
 import org.apache.doris.mysql.privilege.PaloAuth;
 import org.apache.doris.qe.ConnectContext;
@@ -49,6 +50,10 @@ public class DropTableStmtTest {
 
         new Expectations() {
             {
+                noDbAnalyzer.getDefaultCatalog();
+                minTimes = 0;
+                result = InternalDataSource.INTERNAL_DS_NAME;
+
                 noDbAnalyzer.getDefaultDb();
                 minTimes = 0;
                 result = "";
diff --git a/fe/fe-core/src/test/java/org/apache/doris/analysis/VirtualSlotRefTest.java b/fe/fe-core/src/test/java/org/apache/doris/analysis/VirtualSlotRefTest.java
index 2676411b89..f7e3abc3a7 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/analysis/VirtualSlotRefTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/analysis/VirtualSlotRefTest.java
@@ -19,6 +19,8 @@ package org.apache.doris.analysis;
 
 import org.apache.doris.catalog.Type;
 import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.FeMetaVersion;
+import org.apache.doris.meta.MetaContext;
 
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Multimap;
@@ -49,6 +51,10 @@ public class VirtualSlotRefTest {
     @Before
     public void setUp() throws IOException, AnalysisException {
         Analyzer analyzerBase = AccessTestUtil.fetchTableAnalyzer();
+        // read objects from file
+        MetaContext metaContext = new MetaContext();
+        metaContext.setMetaVersion(FeMetaVersion.VERSION_CURRENT);
+        metaContext.setThreadLocalInfo();
         analyzer = new Analyzer(analyzerBase.getCatalog(), analyzerBase.getContext());
         String[] cols = {"k1", "k2", "k3"};
         slots = new ArrayList<>();
diff --git a/fe/fe-core/src/test/java/org/apache/doris/ldap/LdapPrivsCheckerTest.java b/fe/fe-core/src/test/java/org/apache/doris/ldap/LdapPrivsCheckerTest.java
index 6e705ab6b1..81a48f3712 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/ldap/LdapPrivsCheckerTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/ldap/LdapPrivsCheckerTest.java
@@ -22,6 +22,7 @@ import org.apache.doris.analysis.TablePattern;
 import org.apache.doris.analysis.UserIdentity;
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.LdapConfig;
+import org.apache.doris.datasource.InternalDataSource;
 import org.apache.doris.mysql.privilege.PaloPrivilege;
 import org.apache.doris.mysql.privilege.PaloRole;
 import org.apache.doris.mysql.privilege.PrivBitSet;
@@ -38,6 +39,7 @@ import java.util.Map;
 
 public class LdapPrivsCheckerTest {
     private static final String CLUSTER = "default_cluster";
+    private static final String INTERNAL = InternalDataSource.INTERNAL_DS_NAME;
     private static final String DB = "palodb";
     private static final String TABLE_DB = "tabledb";
     private static final String TABLE1 = "table1";
@@ -63,13 +65,13 @@ public class LdapPrivsCheckerTest {
                 PaloRole role = new PaloRole("");
                 Map<TablePattern, PrivBitSet> tblPatternToPrivs = role.getTblPatternToPrivs();
 
-                TablePattern global = new TablePattern("*", "*");
+                TablePattern global = new TablePattern("*", "*", "*");
                 tblPatternToPrivs.put(global, PrivBitSet.of(PaloPrivilege.SELECT_PRIV, PaloPrivilege.CREATE_PRIV));
-                TablePattern db = new TablePattern(DB, "*");
+                TablePattern db = new TablePattern(INTERNAL, DB, "*");
                 tblPatternToPrivs.put(db, PrivBitSet.of(PaloPrivilege.SELECT_PRIV, PaloPrivilege.LOAD_PRIV));
-                TablePattern tbl1 = new TablePattern(TABLE_DB, TABLE1);
+                TablePattern tbl1 = new TablePattern(INTERNAL, TABLE_DB, TABLE1);
                 tblPatternToPrivs.put(tbl1, PrivBitSet.of(PaloPrivilege.SELECT_PRIV, PaloPrivilege.ALTER_PRIV));
-                TablePattern tbl2 = new TablePattern(TABLE_DB, TABLE2);
+                TablePattern tbl2 = new TablePattern(INTERNAL, TABLE_DB, TABLE2);
                 tblPatternToPrivs.put(tbl2, PrivBitSet.of(PaloPrivilege.SELECT_PRIV, PaloPrivilege.DROP_PRIV));
 
                 Map<ResourcePattern, PrivBitSet> resourcePatternToPrivs = role.getResourcePatternToPrivs();
diff --git a/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlProtoTest.java b/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlProtoTest.java
index ff90477279..eb4459918b 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlProtoTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlProtoTest.java
@@ -23,6 +23,7 @@ import org.apache.doris.catalog.Database;
 import org.apache.doris.cluster.ClusterNamespace;
 import org.apache.doris.common.DdlException;
 import org.apache.doris.common.LdapConfig;
+import org.apache.doris.datasource.InternalDataSource;
 import org.apache.doris.ldap.LdapAuthenticate;
 import org.apache.doris.ldap.LdapClient;
 import org.apache.doris.mysql.privilege.PaloAuth;
@@ -82,6 +83,10 @@ public class MysqlProtoTest {
                     }
                 };
 
+                catalog.getInternalDataSource();
+                minTimes = 0;
+                result = new InternalDataSource();
+
                 catalog.getDbNullable(anyString);
                 minTimes = 0;
                 result = new Database();
diff --git a/fe/fe-core/src/test/java/org/apache/doris/mysql/privilege/AuthTest.java b/fe/fe-core/src/test/java/org/apache/doris/mysql/privilege/AuthTest.java
index 886783896a..4c2d7e7ce9 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/mysql/privilege/AuthTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/mysql/privilege/AuthTest.java
@@ -35,6 +35,7 @@ import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.Config;
 import org.apache.doris.common.DdlException;
 import org.apache.doris.common.UserException;
+import org.apache.doris.datasource.InternalDataSource;
 import org.apache.doris.persist.EditLog;
 import org.apache.doris.persist.PrivInfo;
 import org.apache.doris.qe.ConnectContext;
@@ -102,6 +103,10 @@ public class AuthTest {
                 minTimes = 0;
                 result = SystemInfoService.DEFAULT_CLUSTER;
 
+                analyzer.getDefaultCatalog();
+                minTimes = 0;
+                result = InternalDataSource.INTERNAL_DS_NAME;
+
                 Catalog.getCurrentCatalog();
                 minTimes = 0;
                 result = catalog;
@@ -1242,7 +1247,7 @@ public class AuthTest {
             }
         };
         Assert.assertFalse(auth.checkGlobalPriv(ctx, PrivPredicate.OPERATOR));
-        grantStmt = new GrantStmt(opUser, null, new TablePattern("*", "*"), privileges);
+        grantStmt = new GrantStmt(opUser, null, new TablePattern("*", "*", "*"), privileges);
         // first, use op_user itself to grant node_priv, which is not allowed
         try {
             new Expectations() {
diff --git a/fe/fe-core/src/test/java/org/apache/doris/mysql/privilege/PrivEntryTest.java b/fe/fe-core/src/test/java/org/apache/doris/mysql/privilege/PrivEntryTest.java
index 71f6990191..8e9a3f173b 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/mysql/privilege/PrivEntryTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/mysql/privilege/PrivEntryTest.java
@@ -26,7 +26,7 @@ public class PrivEntryTest {
     @Test
     public void testNameWithUnderscores() throws Exception {
         TablePrivEntry tablePrivEntry = TablePrivEntry.create(
-                "127.%", "db_db1", "user1", "tbl_tbl1", false,
+                "user1", "127.%", "__internal", "db_db1", "tbl_tbl1", false,
                 PrivBitSet.of(PaloPrivilege.SELECT_PRIV, PaloPrivilege.DROP_PRIV));
         // pattern match
         Assert.assertFalse(tablePrivEntry.getDbPattern().match("db-db1"));
@@ -38,11 +38,11 @@ public class PrivEntryTest {
         userIdentity.setIsAnalyzed();
 
         PrivBitSet privs1 = PrivBitSet.of();
-        tablePrivTable.getPrivs(userIdentity, "db#db1", "tbl#tbl1", privs1);
+        tablePrivTable.getPrivs(userIdentity, "##internal", "db#db1", "tbl#tbl1", privs1);
         Assert.assertFalse(PaloPrivilege.satisfy(privs1, PrivPredicate.DROP));
 
         PrivBitSet privs2 = PrivBitSet.of();
-        tablePrivTable.getPrivs(userIdentity, "db_db1", "tbl_tbl1", privs2);
+        tablePrivTable.getPrivs(userIdentity, "__internal", "db_db1", "tbl_tbl1", privs2);
         Assert.assertTrue(PaloPrivilege.satisfy(privs2, PrivPredicate.DROP));
     }
 }
diff --git a/fe/fe-core/src/test/java/org/apache/doris/policy/PolicyTest.java b/fe/fe-core/src/test/java/org/apache/doris/policy/PolicyTest.java
index 3b162804df..27612dfb77 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/policy/PolicyTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/policy/PolicyTest.java
@@ -66,7 +66,7 @@ public class PolicyTest extends TestWithFeService {
         CreateUserStmt createUserStmt = new CreateUserStmt(new UserDesc(user));
         Catalog.getCurrentCatalog().getAuth().createUser(createUserStmt);
         List<AccessPrivilege> privileges = Lists.newArrayList(AccessPrivilege.ADMIN_PRIV);
-        TablePattern tablePattern = new TablePattern("*", "*");
+        TablePattern tablePattern = new TablePattern("*", "*", "*");
         tablePattern.analyze(SystemInfoService.DEFAULT_CLUSTER);
         GrantStmt grantStmt = new GrantStmt(user, null, tablePattern, privileges);
         Catalog.getCurrentCatalog().getAuth().grant(grantStmt);


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@doris.apache.org
For additional commands, e-mail: commits-help@doris.apache.org