From 02d66042dd43359133d3d1293c71a5d1df8fba45 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Wed, 28 Jan 2026 15:22:54 +0800 Subject: [PATCH] [Feat](Snapshot) require root privilege for cluster snapshot commands (#60239) Cluster snapshot operations are currently restricted to root user only. Some customers need to allow ADMIN users to manage cluster snapshots without granting root access. Solution Add a new configuration cluster_snapshot_min_privilege to control the minimum privilege level: - root (default): Only root user can execute - admin: Users with ADMIN privilege can execute Affects 4 cluster snapshot commands and 2 information_schema tables (cluster_snapshots, cluster_snapshot_properties). --- .../java/org/apache/doris/common/Config.java | 8 + .../rules/analysis/UserAuthentication.java | 32 +- .../AdminCreateClusterSnapshotCommand.java | 18 +- .../AdminDropClusterSnapshotCommand.java | 18 +- .../AdminSetAutoClusterSnapshotCommand.java | 18 +- ...etClusterSnapshotFeatureSwitchCommand.java | 18 +- .../analysis/UserAuthenticationTest.java | 396 ++++++++++++++++++ ...AdminCreateClusterSnapshotCommandTest.java | 101 ++++- .../AdminDropClusterSnapshotCommandTest.java | 99 ++++- ...dminSetAutoClusterSnapshotCommandTest.java | 103 ++++- ...usterSnapshotFeatureSwitchCommandTest.java | 99 ++++- 11 files changed, 860 insertions(+), 50 deletions(-) create mode 100644 fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/UserAuthenticationTest.java diff --git a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java index 921796670c48b5..a6efbe143aeaa5 100644 --- a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java +++ b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java @@ -3762,6 +3762,14 @@ public static int metaServiceRpcRetryTimes() { @ConfField(mutable = true) public static long cloud_auto_snapshot_min_interval_seconds = 3600; + @ConfField(mutable = true, description = { + "cluster snapshot 相关操作的最低权限要求。可选值:'root'(仅 root 用户可执行)或 'admin'(ADMIN 权限用户可执行)。默认值为 'root'。", + "The minimum privilege required for cluster snapshot operations. " + + "Valid values: 'root' (only root user can execute)" + + " or 'admin' (users with ADMIN privilege can execute). " + + "Default is 'root'."}) + public static String cluster_snapshot_min_privilege = "root"; + @ConfField(mutable = true) public static long multi_part_upload_part_size_in_bytes = 256 * 1024 * 1024L; // 256MB @ConfField(mutable = true) diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/UserAuthentication.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/UserAuthentication.java index 6bfe9efb3d4c1d..9d0fbd39e4ad84 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/UserAuthentication.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/UserAuthentication.java @@ -17,8 +17,13 @@ package org.apache.doris.nereids.rules.analysis; +import org.apache.doris.analysis.UserIdentity; import org.apache.doris.catalog.DatabaseIf; +import org.apache.doris.catalog.Env; +import org.apache.doris.catalog.InfoSchemaDb; import org.apache.doris.catalog.TableIf; +import org.apache.doris.cluster.ClusterNamespace; +import org.apache.doris.common.Config; import org.apache.doris.common.ErrorCode; import org.apache.doris.common.ErrorReport; import org.apache.doris.common.UserException; @@ -47,11 +52,34 @@ public static void checkPermission(TableIf table, ConnectContext connectContext, } String tableName = table.getName(); DatabaseIf db = table.getDatabase(); - // when table instanceof FunctionGenTable,db will be null + // when table instanceof FunctionGenTable, db will be null if (db == null) { return; } - String dbName = db.getFullName(); + String dbName = ClusterNamespace.getNameFromFullName(db.getFullName()); + + // Special handling: cluster snapshot related tables in information_schema + // require privilege based on configuration + if (dbName.equalsIgnoreCase(InfoSchemaDb.DATABASE_NAME) + && (tableName.equalsIgnoreCase("cluster_snapshots") + || tableName.equalsIgnoreCase("cluster_snapshot_properties"))) { + if ("admin".equalsIgnoreCase(Config.cluster_snapshot_min_privilege)) { + // When configured as admin, check ADMIN privilege + if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(connectContext, PrivPredicate.ADMIN)) { + ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, + PrivPredicate.ADMIN.getPrivs().toString()); + } + } else { + // Default or configured as root, check if user is root + UserIdentity currentUser = connectContext.getCurrentUserIdentity(); + if (currentUser == null || !currentUser.isRootUser()) { + ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, + "root privilege"); + } + } + return; // privilege check passed, allow access + } + CatalogIf catalog = db.getCatalog(); if (catalog == null) { return; diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminCreateClusterSnapshotCommand.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminCreateClusterSnapshotCommand.java index ab5c9460bf8d3c..b62a960bede49e 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminCreateClusterSnapshotCommand.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminCreateClusterSnapshotCommand.java @@ -18,6 +18,7 @@ package org.apache.doris.nereids.trees.plans.commands; import org.apache.doris.analysis.StmtType; +import org.apache.doris.analysis.UserIdentity; import org.apache.doris.catalog.Env; import org.apache.doris.cloud.catalog.CloudEnv; import org.apache.doris.cloud.snapshot.CloudSnapshotHandler; @@ -73,9 +74,20 @@ public void validate(ConnectContext ctx) throws AnalysisException { if (!Config.isCloudMode()) { throw new AnalysisException("The sql is illegal in disk mode "); } - if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ctx, PrivPredicate.ADMIN)) { - ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, - PrivPredicate.ADMIN.getPrivs().toString()); + // Check privilege based on configuration + if ("admin".equalsIgnoreCase(Config.cluster_snapshot_min_privilege)) { + // When configured as admin, check ADMIN privilege + if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ctx, PrivPredicate.ADMIN)) { + ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, + PrivPredicate.ADMIN.getPrivs().toString()); + } + } else { + // Default or configured as root, check if user is root + UserIdentity currentUser = ctx.getCurrentUserIdentity(); + if (currentUser == null || !currentUser.isRootUser()) { + ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, + "root privilege"); + } } for (Map.Entry entry : properties.entrySet()) { diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminDropClusterSnapshotCommand.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminDropClusterSnapshotCommand.java index b131f8a8cc819a..a545113321eb89 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminDropClusterSnapshotCommand.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminDropClusterSnapshotCommand.java @@ -18,6 +18,7 @@ package org.apache.doris.nereids.trees.plans.commands; import org.apache.doris.analysis.StmtType; +import org.apache.doris.analysis.UserIdentity; import org.apache.doris.catalog.Env; import org.apache.doris.cloud.proto.Cloud; import org.apache.doris.cloud.rpc.MetaServiceProxy; @@ -70,9 +71,20 @@ public void validate(ConnectContext ctx) throws AnalysisException { if (!Config.isCloudMode()) { throw new AnalysisException("The sql is illegal in disk mode "); } - if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ctx, PrivPredicate.ADMIN)) { - ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, - PrivPredicate.ADMIN.getPrivs().toString()); + // Check privilege based on configuration + if ("admin".equalsIgnoreCase(Config.cluster_snapshot_min_privilege)) { + // When configured as admin, check ADMIN privilege + if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ctx, PrivPredicate.ADMIN)) { + ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, + PrivPredicate.ADMIN.getPrivs().toString()); + } + } else { + // Default or configured as root, check if user is root + UserIdentity currentUser = ctx.getCurrentUserIdentity(); + if (currentUser == null || !currentUser.isRootUser()) { + ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, + "root privilege"); + } } if (key == null || !key.equalsIgnoreCase(SNAPSHOT_ID)) { diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminSetAutoClusterSnapshotCommand.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminSetAutoClusterSnapshotCommand.java index b450ada674f5df..2a873c32790e97 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminSetAutoClusterSnapshotCommand.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminSetAutoClusterSnapshotCommand.java @@ -18,6 +18,7 @@ package org.apache.doris.nereids.trees.plans.commands; import org.apache.doris.analysis.StmtType; +import org.apache.doris.analysis.UserIdentity; import org.apache.doris.catalog.Env; import org.apache.doris.cloud.catalog.CloudEnv; import org.apache.doris.cloud.proto.Cloud; @@ -92,9 +93,20 @@ public void validate(ConnectContext ctx) throws AnalysisException { if (!Config.isCloudMode()) { throw new AnalysisException("The sql is illegal in disk mode "); } - if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ctx, PrivPredicate.ADMIN)) { - ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, - PrivPredicate.ADMIN.getPrivs().toString()); + // Check privilege based on configuration + if ("admin".equalsIgnoreCase(Config.cluster_snapshot_min_privilege)) { + // When configured as admin, check ADMIN privilege + if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ctx, PrivPredicate.ADMIN)) { + ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, + PrivPredicate.ADMIN.getPrivs().toString()); + } + } else { + // Default or configured as root, check if user is root + UserIdentity currentUser = ctx.getCurrentUserIdentity(); + if (currentUser == null || !currentUser.isRootUser()) { + ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, + "root privilege"); + } } if (properties.isEmpty()) { diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminSetClusterSnapshotFeatureSwitchCommand.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminSetClusterSnapshotFeatureSwitchCommand.java index 7b873ae65dda31..9b2d86123157a8 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminSetClusterSnapshotFeatureSwitchCommand.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/AdminSetClusterSnapshotFeatureSwitchCommand.java @@ -18,6 +18,7 @@ package org.apache.doris.nereids.trees.plans.commands; import org.apache.doris.analysis.StmtType; +import org.apache.doris.analysis.UserIdentity; import org.apache.doris.catalog.Env; import org.apache.doris.cloud.catalog.CloudEnv; import org.apache.doris.cloud.proto.Cloud; @@ -73,9 +74,20 @@ public void validate(ConnectContext ctx) throws AnalysisException { if (!Config.isCloudMode()) { throw new AnalysisException("The sql is illegal in disk mode "); } - if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ctx, PrivPredicate.ADMIN)) { - ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, - PrivPredicate.ADMIN.getPrivs().toString()); + // Check privilege based on configuration + if ("admin".equalsIgnoreCase(Config.cluster_snapshot_min_privilege)) { + // When configured as admin, check ADMIN privilege + if (!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ctx, PrivPredicate.ADMIN)) { + ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, + PrivPredicate.ADMIN.getPrivs().toString()); + } + } else { + // Default or configured as root, check if user is root + UserIdentity currentUser = ctx.getCurrentUserIdentity(); + if (currentUser == null || !currentUser.isRootUser()) { + ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, + "root privilege"); + } } } diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/UserAuthenticationTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/UserAuthenticationTest.java new file mode 100644 index 00000000000000..b026dad95d488a --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/UserAuthenticationTest.java @@ -0,0 +1,396 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.doris.nereids.rules.analysis; + +import org.apache.doris.analysis.UserIdentity; +import org.apache.doris.catalog.DatabaseIf; +import org.apache.doris.catalog.Env; +import org.apache.doris.catalog.InfoSchemaDb; +import org.apache.doris.catalog.TableIf; +import org.apache.doris.common.AnalysisException; +import org.apache.doris.common.Config; +import org.apache.doris.datasource.CatalogIf; +import org.apache.doris.mysql.privilege.AccessControllerManager; +import org.apache.doris.mysql.privilege.PrivPredicate; +import org.apache.doris.qe.ConnectContext; +import org.apache.doris.qe.SessionVariable; + +import mockit.Expectations; +import mockit.Mocked; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test for UserAuthentication privilege checks. + */ +public class UserAuthenticationTest { + @Mocked + private Env env; + @Mocked + private ConnectContext connectContext; + @Mocked + private AccessControllerManager accessControllerManager; + @Mocked + private SessionVariable sessionVariable; + @Mocked + private TableIf table; + @Mocked + private DatabaseIf db; + @Mocked + private CatalogIf catalog; + + private String originalMinPrivilege; + + @BeforeEach + public void setUp() { + originalMinPrivilege = Config.cluster_snapshot_min_privilege; + } + + @AfterEach + public void tearDown() { + Config.cluster_snapshot_min_privilege = originalMinPrivilege; + } + + /** + * Test that a table named "cluster_snapshots" in a normal database (not information_schema) + * does NOT trigger the special privilege check for cluster snapshot tables. + * This verifies that only information_schema.cluster_snapshots requires special privileges. + */ + @Test + public void testSameNameTableInNormalDbNotTriggerSpecialCheck() throws Exception { + // Set to root mode - if special check is triggered, non-root user would be denied + Config.cluster_snapshot_min_privilege = "root"; + + UserIdentity normalUser = new UserIdentity("normal_user", "%"); + normalUser.setIsAnalyzed(); + + new Expectations() { + { + // Table setup - same name as cluster snapshot table but in a normal database + table.getName(); + minTimes = 0; + result = "cluster_snapshots"; // Same name as the special table + + table.getDatabase(); + minTimes = 0; + result = db; + + // Database is NOT information_schema, it's a normal user database + db.getFullName(); + minTimes = 0; + result = "mydb"; // Normal database, not information_schema + + db.getCatalog(); + minTimes = 0; + result = catalog; + + catalog.getName(); + minTimes = 0; + result = "internal"; + + // ConnectContext setup + connectContext.getSessionVariable(); + minTimes = 0; + result = sessionVariable; + + sessionVariable.isPlayNereidsDump(); + minTimes = 0; + result = false; + + connectContext.getEnv(); + minTimes = 0; + result = env; + + env.getAccessManager(); + minTimes = 0; + result = accessControllerManager; + + // Normal SELECT privilege check should be called (not special root check) + // and should return true to allow access + accessControllerManager.checkTblPriv(connectContext, "internal", "mydb", + "cluster_snapshots", PrivPredicate.SELECT); + minTimes = 1; // This MUST be called, proving we're using normal privilege check + result = true; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = normalUser; + } + }; + + // Should NOT throw exception - normal user can access mydb.cluster_snapshots + // because it's not the special information_schema table + Assertions.assertDoesNotThrow(() -> + UserAuthentication.checkPermission(table, connectContext, null)); + } + + /** + * Test that a table named "cluster_snapshot_properties" in a normal database + * does NOT trigger the special privilege check. + */ + @Test + public void testSameNamePropertiesTableInNormalDbNotTriggerSpecialCheck() throws Exception { + Config.cluster_snapshot_min_privilege = "root"; + + UserIdentity normalUser = new UserIdentity("normal_user", "%"); + normalUser.setIsAnalyzed(); + + new Expectations() { + { + table.getName(); + minTimes = 0; + result = "cluster_snapshot_properties"; + + table.getDatabase(); + minTimes = 0; + result = db; + + db.getFullName(); + minTimes = 0; + result = "user_database"; + + db.getCatalog(); + minTimes = 0; + result = catalog; + + catalog.getName(); + minTimes = 0; + result = "internal"; + + connectContext.getSessionVariable(); + minTimes = 0; + result = sessionVariable; + + sessionVariable.isPlayNereidsDump(); + minTimes = 0; + result = false; + + connectContext.getEnv(); + minTimes = 0; + result = env; + + env.getAccessManager(); + minTimes = 0; + result = accessControllerManager; + + accessControllerManager.checkTblPriv(connectContext, "internal", "user_database", + "cluster_snapshot_properties", PrivPredicate.SELECT); + minTimes = 1; + result = true; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = normalUser; + } + }; + + Assertions.assertDoesNotThrow(() -> + UserAuthentication.checkPermission(table, connectContext, null)); + } + + /** + * Test that information_schema.cluster_snapshots DOES trigger special privilege check + * and denies non-root user in root mode. + */ + @Test + public void testInfoSchemaClusterSnapshotsRequiresRootPrivilege() { + Config.cluster_snapshot_min_privilege = "root"; + + UserIdentity normalUser = new UserIdentity("normal_user", "%"); + normalUser.setIsAnalyzed(); + + new Expectations() { + { + table.getName(); + minTimes = 0; + result = "cluster_snapshots"; + + table.getDatabase(); + minTimes = 0; + result = db; + + // This IS information_schema + db.getFullName(); + minTimes = 0; + result = InfoSchemaDb.DATABASE_NAME; + + connectContext.getSessionVariable(); + minTimes = 0; + result = sessionVariable; + + sessionVariable.isPlayNereidsDump(); + minTimes = 0; + result = false; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = normalUser; + } + }; + + // Should throw AnalysisException because non-root user cannot access + // information_schema.cluster_snapshots in root mode + Assertions.assertThrows(AnalysisException.class, () -> + UserAuthentication.checkPermission(table, connectContext, null)); + } + + /** + * Test that information_schema.cluster_snapshots allows root user. + */ + @Test + public void testInfoSchemaClusterSnapshotsAllowsRootUser() { + Config.cluster_snapshot_min_privilege = "root"; + + new Expectations() { + { + table.getName(); + minTimes = 0; + result = "cluster_snapshots"; + + table.getDatabase(); + minTimes = 0; + result = db; + + db.getFullName(); + minTimes = 0; + result = InfoSchemaDb.DATABASE_NAME; + + connectContext.getSessionVariable(); + minTimes = 0; + result = sessionVariable; + + sessionVariable.isPlayNereidsDump(); + minTimes = 0; + result = false; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = UserIdentity.ROOT; + } + }; + + // Root user should be able to access + Assertions.assertDoesNotThrow(() -> + UserAuthentication.checkPermission(table, connectContext, null)); + } + + /** + * Test that cluster:information_schema.cluster_snapshots (with cluster prefix) + * DOES trigger special privilege check. + * This verifies ClusterNamespace.getNameFromFullName correctly strips the cluster prefix. + */ + @Test + public void testInfoSchemaWithClusterPrefixTriggersSpecialCheck() { + Config.cluster_snapshot_min_privilege = "root"; + + UserIdentity normalUser = new UserIdentity("normal_user", "%"); + normalUser.setIsAnalyzed(); + + new Expectations() { + { + table.getName(); + minTimes = 0; + result = "cluster_snapshots"; + + table.getDatabase(); + minTimes = 0; + result = db; + + // Database name with cluster prefix - ClusterNamespace.getNameFromFullName + // should strip "default_cluster:" and return "information_schema" + db.getFullName(); + minTimes = 0; + result = "default_cluster:information_schema"; + + connectContext.getSessionVariable(); + minTimes = 0; + result = sessionVariable; + + sessionVariable.isPlayNereidsDump(); + minTimes = 0; + result = false; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = normalUser; + } + }; + + // Should throw AnalysisException because after stripping cluster prefix, + // the db name is "information_schema", which triggers special privilege check + Assertions.assertThrows(AnalysisException.class, () -> + UserAuthentication.checkPermission(table, connectContext, null)); + } + + /** + * Test that information_schema.cluster_snapshots allows admin user in admin mode. + */ + @Test + public void testInfoSchemaClusterSnapshotsAllowsAdminInAdminMode() { + Config.cluster_snapshot_min_privilege = "admin"; + + UserIdentity adminUser = new UserIdentity("admin", "%"); + adminUser.setIsAnalyzed(); + + new Expectations() { + { + table.getName(); + minTimes = 0; + result = "cluster_snapshots"; + + table.getDatabase(); + minTimes = 0; + result = db; + + db.getFullName(); + minTimes = 0; + result = InfoSchemaDb.DATABASE_NAME; + + connectContext.getSessionVariable(); + minTimes = 0; + result = sessionVariable; + + sessionVariable.isPlayNereidsDump(); + minTimes = 0; + result = false; + + Env.getCurrentEnv(); + minTimes = 0; + result = env; + + env.getAccessManager(); + minTimes = 0; + result = accessControllerManager; + + accessControllerManager.checkGlobalPriv(connectContext, PrivPredicate.ADMIN); + minTimes = 0; + result = true; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = adminUser; + } + }; + + // Admin user should be able to access in admin mode + Assertions.assertDoesNotThrow(() -> + UserAuthentication.checkPermission(table, connectContext, null)); + } +} diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AdminCreateClusterSnapshotCommandTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AdminCreateClusterSnapshotCommandTest.java index 31f63a5bf388b4..401a600bc876fd 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AdminCreateClusterSnapshotCommandTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AdminCreateClusterSnapshotCommandTest.java @@ -17,6 +17,7 @@ package org.apache.doris.nereids.trees.plans.commands; +import org.apache.doris.analysis.UserIdentity; import org.apache.doris.catalog.Env; import org.apache.doris.common.AnalysisException; import org.apache.doris.common.Config; @@ -26,7 +27,9 @@ import mockit.Expectations; import mockit.Mocked; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.HashMap; @@ -36,9 +39,21 @@ public class AdminCreateClusterSnapshotCommandTest { @Mocked private Env env; @Mocked - private AccessControllerManager accessControllerManager; - @Mocked private ConnectContext connectContext; + @Mocked + private AccessControllerManager accessControllerManager; + + private String originalMinPrivilege; + + @BeforeEach + public void setUp() { + originalMinPrivilege = Config.cluster_snapshot_min_privilege; + } + + @AfterEach + public void tearDown() { + Config.cluster_snapshot_min_privilege = originalMinPrivilege; + } private void runBefore() throws Exception { new Expectations() { @@ -47,10 +62,6 @@ private void runBefore() throws Exception { minTimes = 0; result = env; - env.getAccessManager(); - minTimes = 0; - result = accessControllerManager; - ConnectContext.get(); minTimes = 0; result = connectContext; @@ -59,9 +70,10 @@ private void runBefore() throws Exception { minTimes = 0; result = true; - accessControllerManager.checkGlobalPriv(connectContext, PrivPredicate.ADMIN); + // Mock root user for privilege check + connectContext.getCurrentUserIdentity(); minTimes = 0; - result = true; + result = UserIdentity.ROOT; } }; } @@ -97,7 +109,74 @@ public void testValidateNormal() throws Exception { } @Test - public void testValidateNoPriviledge() { + public void testValidateNoPrivilegeRootMode() { + // Test root mode (default): admin user should be denied + Config.cluster_snapshot_min_privilege = "root"; + + UserIdentity nonRootUser = new UserIdentity("admin", "%"); + nonRootUser.setIsAnalyzed(); + + new Expectations() { + { + Env.getCurrentEnv(); + minTimes = 0; + result = env; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = nonRootUser; + } + }; + Config.deploy_mode = "cloud"; + + Map properties = new HashMap<>(); + AdminCreateClusterSnapshotCommand command = new AdminCreateClusterSnapshotCommand(properties); + Assertions.assertThrows(AnalysisException.class, () -> command.validate(connectContext), + "Access denied; you need (at least one of) the (root privilege) privilege(s) for this operation"); + } + + @Test + public void testValidateAdminModeWithAdminUser() { + // Test admin mode: admin user with ADMIN privilege should be allowed + Config.cluster_snapshot_min_privilege = "admin"; + + UserIdentity adminUser = new UserIdentity("admin", "%"); + adminUser.setIsAnalyzed(); + + new Expectations() { + { + Env.getCurrentEnv(); + minTimes = 0; + result = env; + + env.getAccessManager(); + minTimes = 0; + result = accessControllerManager; + + accessControllerManager.checkGlobalPriv(connectContext, PrivPredicate.ADMIN); + minTimes = 0; + result = true; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = adminUser; + } + }; + Config.deploy_mode = "cloud"; + + Map properties = ImmutableMap.of("ttl", "3600", "label", "test"); + AdminCreateClusterSnapshotCommand command = new AdminCreateClusterSnapshotCommand(properties); + Assertions.assertDoesNotThrow(() -> command.validate(connectContext)); + } + + @Test + public void testValidateAdminModeWithNormalUser() { + // Test admin mode: normal user without ADMIN privilege should be denied + Config.cluster_snapshot_min_privilege = "admin"; + + UserIdentity normalUser = new UserIdentity("normal_user", "%"); + normalUser.setIsAnalyzed(); + new Expectations() { { Env.getCurrentEnv(); @@ -111,6 +190,10 @@ public void testValidateNoPriviledge() { accessControllerManager.checkGlobalPriv(connectContext, PrivPredicate.ADMIN); minTimes = 0; result = false; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = normalUser; } }; Config.deploy_mode = "cloud"; diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AdminDropClusterSnapshotCommandTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AdminDropClusterSnapshotCommandTest.java index f9a289e7e909d0..49a13623b613eb 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AdminDropClusterSnapshotCommandTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AdminDropClusterSnapshotCommandTest.java @@ -17,6 +17,7 @@ package org.apache.doris.nereids.trees.plans.commands; +import org.apache.doris.analysis.UserIdentity; import org.apache.doris.catalog.Env; import org.apache.doris.common.AnalysisException; import org.apache.doris.common.Config; @@ -26,16 +27,30 @@ import mockit.Expectations; import mockit.Mocked; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class AdminDropClusterSnapshotCommandTest { @Mocked private Env env; @Mocked - private AccessControllerManager accessControllerManager; - @Mocked private ConnectContext connectContext; + @Mocked + private AccessControllerManager accessControllerManager; + + private String originalMinPrivilege; + + @BeforeEach + public void setUp() { + originalMinPrivilege = Config.cluster_snapshot_min_privilege; + } + + @AfterEach + public void tearDown() { + Config.cluster_snapshot_min_privilege = originalMinPrivilege; + } private void runBefore() throws Exception { new Expectations() { @@ -44,10 +59,6 @@ private void runBefore() throws Exception { minTimes = 0; result = env; - env.getAccessManager(); - minTimes = 0; - result = accessControllerManager; - ConnectContext.get(); minTimes = 0; result = connectContext; @@ -56,9 +67,10 @@ private void runBefore() throws Exception { minTimes = 0; result = true; - accessControllerManager.checkGlobalPriv(connectContext, PrivPredicate.ADMIN); + // Mock root user for privilege check + connectContext.getCurrentUserIdentity(); minTimes = 0; - result = true; + result = UserIdentity.ROOT; } }; } @@ -94,7 +106,72 @@ public void testValidateNormal() throws Exception { } @Test - public void testValidateNoPriviledge() { + public void testValidateNoPrivilegeRootMode() { + // Test root mode (default): admin user should be denied + Config.cluster_snapshot_min_privilege = "root"; + + UserIdentity nonRootUser = new UserIdentity("admin", "%"); + nonRootUser.setIsAnalyzed(); + + new Expectations() { + { + Env.getCurrentEnv(); + minTimes = 0; + result = env; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = nonRootUser; + } + }; + Config.deploy_mode = "cloud"; + + AdminDropClusterSnapshotCommand command = new AdminDropClusterSnapshotCommand("snapshot_id", "323741"); + Assertions.assertThrows(AnalysisException.class, () -> command.validate(connectContext), + "Access denied; you need (at least one of) the (root privilege) privilege(s) for this operation"); + } + + @Test + public void testValidateAdminModeWithAdminUser() { + // Test admin mode: admin user with ADMIN privilege should be allowed + Config.cluster_snapshot_min_privilege = "admin"; + + UserIdentity adminUser = new UserIdentity("admin", "%"); + adminUser.setIsAnalyzed(); + + new Expectations() { + { + Env.getCurrentEnv(); + minTimes = 0; + result = env; + + env.getAccessManager(); + minTimes = 0; + result = accessControllerManager; + + accessControllerManager.checkGlobalPriv(connectContext, PrivPredicate.ADMIN); + minTimes = 0; + result = true; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = adminUser; + } + }; + Config.deploy_mode = "cloud"; + + AdminDropClusterSnapshotCommand command = new AdminDropClusterSnapshotCommand("snapshot_id", "323741"); + Assertions.assertDoesNotThrow(() -> command.validate(connectContext)); + } + + @Test + public void testValidateAdminModeWithNormalUser() { + // Test admin mode: normal user without ADMIN privilege should be denied + Config.cluster_snapshot_min_privilege = "admin"; + + UserIdentity normalUser = new UserIdentity("normal_user", "%"); + normalUser.setIsAnalyzed(); + new Expectations() { { Env.getCurrentEnv(); @@ -108,6 +185,10 @@ public void testValidateNoPriviledge() { accessControllerManager.checkGlobalPriv(connectContext, PrivPredicate.ADMIN); minTimes = 0; result = false; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = normalUser; } }; Config.deploy_mode = "cloud"; diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AdminSetAutoClusterSnapshotCommandTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AdminSetAutoClusterSnapshotCommandTest.java index 6779d42fce7e29..65c8cd8f8f60a9 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AdminSetAutoClusterSnapshotCommandTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AdminSetAutoClusterSnapshotCommandTest.java @@ -17,6 +17,7 @@ package org.apache.doris.nereids.trees.plans.commands; +import org.apache.doris.analysis.UserIdentity; import org.apache.doris.catalog.Env; import org.apache.doris.common.AnalysisException; import org.apache.doris.common.Config; @@ -26,7 +27,9 @@ import mockit.Expectations; import mockit.Mocked; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.HashMap; @@ -36,9 +39,21 @@ public class AdminSetAutoClusterSnapshotCommandTest { @Mocked private Env env; @Mocked - private AccessControllerManager accessControllerManager; - @Mocked private ConnectContext connectContext; + @Mocked + private AccessControllerManager accessControllerManager; + + private String originalMinPrivilege; + + @BeforeEach + public void setUp() { + originalMinPrivilege = Config.cluster_snapshot_min_privilege; + } + + @AfterEach + public void tearDown() { + Config.cluster_snapshot_min_privilege = originalMinPrivilege; + } private void runBefore() throws Exception { new Expectations() { @@ -47,10 +62,6 @@ private void runBefore() throws Exception { minTimes = 0; result = env; - env.getAccessManager(); - minTimes = 0; - result = accessControllerManager; - ConnectContext.get(); minTimes = 0; result = connectContext; @@ -59,9 +70,10 @@ private void runBefore() throws Exception { minTimes = 0; result = true; - accessControllerManager.checkGlobalPriv(connectContext, PrivPredicate.ADMIN); + // Mock root user for privilege check + connectContext.getCurrentUserIdentity(); minTimes = 0; - result = true; + result = UserIdentity.ROOT; } }; } @@ -117,7 +129,76 @@ public void testValidateNormal() throws Exception { } @Test - public void testValidateNoPriviledge() { + public void testValidateNoPrivilegeRootMode() { + // Test root mode (default): admin user should be denied + Config.cluster_snapshot_min_privilege = "root"; + + UserIdentity nonRootUser = new UserIdentity("admin", "%"); + nonRootUser.setIsAnalyzed(); + + new Expectations() { + { + Env.getCurrentEnv(); + minTimes = 0; + result = env; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = nonRootUser; + } + }; + Config.deploy_mode = "cloud"; + + Map properties = new HashMap<>(); + AdminSetAutoClusterSnapshotCommand command = new AdminSetAutoClusterSnapshotCommand(properties); + Assertions.assertThrows(AnalysisException.class, () -> command.validate(connectContext), + "Access denied; you need (at least one of) the (root privilege) privilege(s) for this operation"); + } + + @Test + public void testValidateAdminModeWithAdminUser() { + // Test admin mode: admin user with ADMIN privilege should be allowed + Config.cluster_snapshot_min_privilege = "admin"; + + UserIdentity adminUser = new UserIdentity("admin", "%"); + adminUser.setIsAnalyzed(); + + new Expectations() { + { + Env.getCurrentEnv(); + minTimes = 0; + result = env; + + env.getAccessManager(); + minTimes = 0; + result = accessControllerManager; + + accessControllerManager.checkGlobalPriv(connectContext, PrivPredicate.ADMIN); + minTimes = 0; + result = true; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = adminUser; + } + }; + Config.deploy_mode = "cloud"; + + Map properties = new HashMap<>(); + properties.put("max_reserved_snapshots", "10"); + properties.put("snapshot_interval_seconds", "3600"); + AdminSetAutoClusterSnapshotCommand command = new AdminSetAutoClusterSnapshotCommand(properties); + Assertions.assertDoesNotThrow(() -> command.validate(connectContext)); + } + + @Test + public void testValidateAdminModeWithNormalUser() { + // Test admin mode: normal user without ADMIN privilege should be denied + Config.cluster_snapshot_min_privilege = "admin"; + + UserIdentity normalUser = new UserIdentity("normal_user", "%"); + normalUser.setIsAnalyzed(); + new Expectations() { { Env.getCurrentEnv(); @@ -131,6 +212,10 @@ public void testValidateNoPriviledge() { accessControllerManager.checkGlobalPriv(connectContext, PrivPredicate.ADMIN); minTimes = 0; result = false; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = normalUser; } }; Config.deploy_mode = "cloud"; diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AdminSetClusterSnapshotFeatureSwitchCommandTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AdminSetClusterSnapshotFeatureSwitchCommandTest.java index 63121ebc01ba01..a36a34933692bb 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AdminSetClusterSnapshotFeatureSwitchCommandTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/AdminSetClusterSnapshotFeatureSwitchCommandTest.java @@ -17,6 +17,7 @@ package org.apache.doris.nereids.trees.plans.commands; +import org.apache.doris.analysis.UserIdentity; import org.apache.doris.catalog.Env; import org.apache.doris.common.AnalysisException; import org.apache.doris.common.Config; @@ -26,16 +27,30 @@ import mockit.Expectations; import mockit.Mocked; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class AdminSetClusterSnapshotFeatureSwitchCommandTest { @Mocked private Env env; @Mocked - private AccessControllerManager accessControllerManager; - @Mocked private ConnectContext connectContext; + @Mocked + private AccessControllerManager accessControllerManager; + + private String originalMinPrivilege; + + @BeforeEach + public void setUp() { + originalMinPrivilege = Config.cluster_snapshot_min_privilege; + } + + @AfterEach + public void tearDown() { + Config.cluster_snapshot_min_privilege = originalMinPrivilege; + } private void runBefore() throws Exception { new Expectations() { @@ -44,10 +59,6 @@ private void runBefore() throws Exception { minTimes = 0; result = env; - env.getAccessManager(); - minTimes = 0; - result = accessControllerManager; - ConnectContext.get(); minTimes = 0; result = connectContext; @@ -56,9 +67,10 @@ private void runBefore() throws Exception { minTimes = 0; result = true; - accessControllerManager.checkGlobalPriv(connectContext, PrivPredicate.ADMIN); + // Mock root user for privilege check + connectContext.getCurrentUserIdentity(); minTimes = 0; - result = true; + result = UserIdentity.ROOT; } }; } @@ -81,7 +93,72 @@ public void testValidateNormal() throws Exception { } @Test - public void testValidateNoPriviledge() { + public void testValidateNoPrivilegeRootMode() { + // Test root mode (default): admin user should be denied + Config.cluster_snapshot_min_privilege = "root"; + + UserIdentity nonRootUser = new UserIdentity("admin", "%"); + nonRootUser.setIsAnalyzed(); + + new Expectations() { + { + Env.getCurrentEnv(); + minTimes = 0; + result = env; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = nonRootUser; + } + }; + Config.deploy_mode = "cloud"; + + AdminSetClusterSnapshotFeatureSwitchCommand command = new AdminSetClusterSnapshotFeatureSwitchCommand(true); + Assertions.assertThrows(AnalysisException.class, () -> command.validate(connectContext), + "Access denied; you need (at least one of) the (root privilege) privilege(s) for this operation"); + } + + @Test + public void testValidateAdminModeWithAdminUser() { + // Test admin mode: admin user with ADMIN privilege should be allowed + Config.cluster_snapshot_min_privilege = "admin"; + + UserIdentity adminUser = new UserIdentity("admin", "%"); + adminUser.setIsAnalyzed(); + + new Expectations() { + { + Env.getCurrentEnv(); + minTimes = 0; + result = env; + + env.getAccessManager(); + minTimes = 0; + result = accessControllerManager; + + accessControllerManager.checkGlobalPriv(connectContext, PrivPredicate.ADMIN); + minTimes = 0; + result = true; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = adminUser; + } + }; + Config.deploy_mode = "cloud"; + + AdminSetClusterSnapshotFeatureSwitchCommand command = new AdminSetClusterSnapshotFeatureSwitchCommand(true); + Assertions.assertDoesNotThrow(() -> command.validate(connectContext)); + } + + @Test + public void testValidateAdminModeWithNormalUser() { + // Test admin mode: normal user without ADMIN privilege should be denied + Config.cluster_snapshot_min_privilege = "admin"; + + UserIdentity normalUser = new UserIdentity("normal_user", "%"); + normalUser.setIsAnalyzed(); + new Expectations() { { Env.getCurrentEnv(); @@ -95,6 +172,10 @@ public void testValidateNoPriviledge() { accessControllerManager.checkGlobalPriv(connectContext, PrivPredicate.ADMIN); minTimes = 0; result = false; + + connectContext.getCurrentUserIdentity(); + minTimes = 0; + result = normalUser; } }; Config.deploy_mode = "cloud";