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