From b0f9a66de36c9f0cfc2d1ca275696f0247afe80c Mon Sep 17 00:00:00 2001 From: root Date: Sun, 17 May 2026 19:21:44 +0800 Subject: [PATCH] [feature](fe) Support dropping row policies by roles in batch ### What problem does this PR solve? Issue Number: close #xxx Problem Summary: Currently, dropping row policies for multiple roles requires executing DROP ROW POLICY statement for each policy individually, which is cumbersome when a role has many policies. This PR adds a new syntax DROP ROW POLICY FOR ROLE role1, role2, ... to drop all row policies bound to the specified roles in one statement. ### Release note Support new syntax: DROP ROW POLICY FOR ROLE role1, role2, ... to drop all row policies bound to specified roles in batch. ### Check List (For Author) - Test: Regression test, Unit Test - Behavior changed: Yes. New SQL syntax DROP ROW POLICY FOR ROLE supported - Does this need documentation: Yes --- .../org/apache/doris/nereids/DorisParser.g4 | 2 + .../nereids/parser/LogicalPlanBuilder.java | 9 ++ .../doris/nereids/trees/plans/PlanType.java | 1 + .../commands/DropRowPolicyByRolesCommand.java | 85 ++++++++++++ .../trees/plans/visitor/CommandVisitor.java | 5 + .../org/apache/doris/policy/PolicyMgr.java | 16 +++ .../DropRowPolicyByRolesCommandTest.java | 106 ++++++++++++++ .../test_drop_row_policy_by_roles.groovy | 130 ++++++++++++++++++ 8 files changed, 354 insertions(+) create mode 100644 fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropRowPolicyByRolesCommand.java create mode 100644 fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/DropRowPolicyByRolesCommandTest.java create mode 100644 regression-test/suites/account_p0/test_drop_row_policy_by_roles.groovy diff --git a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 index 7c251b2a61b452..2fac6913a30f5c 100644 --- a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 +++ b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 @@ -366,6 +366,8 @@ supportedDropStatement | DROP INDEX (IF EXISTS)? name=identifier ON tableName=multipartIdentifier partitionSpec? #dropIndex | DROP RESOURCE (IF EXISTS)? name=identifierOrText #dropResource + | DROP ROW POLICY FOR ROLE roleNames+=identifierOrText + (COMMA roleNames+=identifierOrText)* #dropRowPolicyByRoles | DROP ROW POLICY (IF EXISTS)? policyName=identifier ON tableName=multipartIdentifier (FOR (userIdentify | ROLE roleName=identifier))? #dropRowPolicy diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java index 2938d9efb7b487..866876e287b039 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java @@ -744,6 +744,7 @@ import org.apache.doris.nereids.trees.plans.commands.DropResourceCommand; import org.apache.doris.nereids.trees.plans.commands.DropRoleCommand; import org.apache.doris.nereids.trees.plans.commands.DropRoleMappingCommand; +import org.apache.doris.nereids.trees.plans.commands.DropRowPolicyByRolesCommand; import org.apache.doris.nereids.trees.plans.commands.DropRowPolicyCommand; import org.apache.doris.nereids.trees.plans.commands.DropSqlBlockRuleCommand; import org.apache.doris.nereids.trees.plans.commands.DropStageCommand; @@ -9020,6 +9021,14 @@ public LogicalPlan visitDropRowPolicy(DorisParser.DropRowPolicyContext ctx) { return new DropRowPolicyCommand(ifExist, policyName, tableNameInfo, userIdentity, roleName); } + @Override + public LogicalPlan visitDropRowPolicyByRoles(DorisParser.DropRowPolicyByRolesContext ctx) { + List roleNames = ctx.roleNames.stream() + .map(ParseTree::getText) + .collect(Collectors.toList()); + return new DropRowPolicyByRolesCommand(roleNames); + } + @Override public LogicalPlan visitTransactionBegin(DorisParser.TransactionBeginContext ctx) { if (ctx.LABEL() != null) { diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java index 09acde2ad6f5a2..bb9d65c157f01e 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java @@ -443,6 +443,7 @@ public enum PlanType { STOP_DATA_SYNC_JOB_COMMAND, DROP_RESOURCE_COMMAND, DROP_ROW_POLICY_COMMAND, + DROP_ROW_POLICY_BY_ROLES_COMMAND, TRANSACTION_BEGIN_COMMAND, TRANSACTION_COMMIT_COMMAND, TRANSACTION_ROLLBACK_COMMAND, diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropRowPolicyByRolesCommand.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropRowPolicyByRolesCommand.java new file mode 100644 index 00000000000000..4720b72eceab2c --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/DropRowPolicyByRolesCommand.java @@ -0,0 +1,85 @@ +// 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.StmtType; +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.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; +import org.apache.doris.qe.StmtExecutor; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * DropRowPolicyByRolesCommand + * Drop all row policies bound to the specified roles. + * Syntax: DROP ROW POLICY FOR ROLE role1, role2, ... + **/ +public class DropRowPolicyByRolesCommand extends DropCommand { + private final List roleNames; + + public DropRowPolicyByRolesCommand(List roleNames) { + super(PlanType.DROP_ROW_POLICY_BY_ROLES_COMMAND); + this.roleNames = roleNames; + } + + @Override + public void doRun(ConnectContext ctx, StmtExecutor executor) throws Exception { + validate(ctx); + Set uniqueRoleNames = new LinkedHashSet<>(roleNames); + Env.getCurrentEnv().getPolicyMgr().dropRowPoliciesByRoles(new ArrayList<>(uniqueRoleNames)); + } + + /** + * validate + */ + public void validate(ConnectContext ctx) throws AnalysisException { + for (String roleName : roleNames) { + if (roleName == null || roleName.isEmpty()) { + throw new AnalysisException("role name is empty"); + } + } + if (!Env.getCurrentEnv().getAccessManager() + .checkGlobalPriv(ConnectContext.get(), PrivPredicate.GRANT)) { + ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, + PrivPredicate.GRANT.getPrivs().toString()); + } + } + + public List getRoleNames() { + return roleNames; + } + + @Override + public R accept(PlanVisitor visitor, C context) { + return visitor.visitDropRowPolicyByRolesCommand(this, context); + } + + @Override + public StmtType stmtType() { + return StmtType.DROP; + } +} diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java index b0da1e801cc97e..057629cbe5b4c6 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java @@ -130,6 +130,7 @@ import org.apache.doris.nereids.trees.plans.commands.DropResourceCommand; import org.apache.doris.nereids.trees.plans.commands.DropRoleCommand; import org.apache.doris.nereids.trees.plans.commands.DropRoleMappingCommand; +import org.apache.doris.nereids.trees.plans.commands.DropRowPolicyByRolesCommand; import org.apache.doris.nereids.trees.plans.commands.DropRowPolicyCommand; import org.apache.doris.nereids.trees.plans.commands.DropSqlBlockRuleCommand; import org.apache.doris.nereids.trees.plans.commands.DropStageCommand; @@ -1310,6 +1311,10 @@ default R visitDropRowPolicyCommand(DropRowPolicyCommand dropRowPolicyCommand, C return visitCommand(dropRowPolicyCommand, context); } + default R visitDropRowPolicyByRolesCommand(DropRowPolicyByRolesCommand dropRowPolicyByRolesCommand, C context) { + return visitCommand(dropRowPolicyByRolesCommand, context); + } + default R visitTransactionBeginCommand(TransactionBeginCommand transactionBeginCommand, C context) { return visitCommand(transactionBeginCommand, context); } diff --git a/fe/fe-core/src/main/java/org/apache/doris/policy/PolicyMgr.java b/fe/fe-core/src/main/java/org/apache/doris/policy/PolicyMgr.java index 0ecb66853d28e2..739e0478b2b1f7 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/policy/PolicyMgr.java +++ b/fe/fe-core/src/main/java/org/apache/doris/policy/PolicyMgr.java @@ -196,6 +196,22 @@ public void dropPolicy(DropPolicyLog dropPolicyLog, boolean ifExists) throws Ddl } } + public void dropRowPoliciesByRoles(List roleNames) throws DdlException { + writeLock(); + try { + for (String roleName : roleNames) { + DropPolicyLog dropPolicyLog = new DropPolicyLog( + null, null, null, PolicyTypeEnum.ROW, null, null, roleName); + if (existPolicy(dropPolicyLog)) { + unprotectedDrop(dropPolicyLog); + Env.getCurrentEnv().getEditLog().logDropPolicy(dropPolicyLog); + } + } + } finally { + writeUnlock(); + } + } + /** * Check whether the policy exist. * diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/DropRowPolicyByRolesCommandTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/DropRowPolicyByRolesCommandTest.java new file mode 100644 index 00000000000000..828a4288fd95b2 --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/DropRowPolicyByRolesCommandTest.java @@ -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. + +package org.apache.doris.nereids.trees.plans.commands; + +import org.apache.doris.catalog.Env; +import org.apache.doris.common.jmockit.Deencapsulation; +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.nereids.trees.plans.Plan; +import org.apache.doris.qe.ConnectContext; +import org.apache.doris.utframe.TestWithFeService; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.util.Arrays; + +public class DropRowPolicyByRolesCommandTest extends TestWithFeService { + private ConnectContext connectContext; + private Env env; + private AccessControllerManager accessControllerManager; + + private void runBefore() throws IOException { + connectContext = createDefaultCtx(); + env = Env.getCurrentEnv(); + accessControllerManager = env.getAccessManager(); + } + + @Test + public void testValidateNormal() throws Exception { + runBefore(); + AccessControllerManager spyAcm = Mockito.spy(accessControllerManager); + Mockito.doReturn(true).when(spyAcm).checkGlobalPriv( + Mockito.nullable(ConnectContext.class), Mockito.eq(PrivPredicate.GRANT)); + Deencapsulation.setField(env, "accessManager", spyAcm); + DropRowPolicyByRolesCommand command = new DropRowPolicyByRolesCommand( + Arrays.asList("role1", "role2")); + Assertions.assertDoesNotThrow(() -> command.validate(connectContext)); + } + + @Test + public void testValidateNoPrivilege() throws Exception { + runBefore(); + AccessControllerManager spyAcm = Mockito.spy(accessControllerManager); + Mockito.doReturn(false).when(spyAcm).checkGlobalPriv( + Mockito.nullable(ConnectContext.class), Mockito.eq(PrivPredicate.GRANT)); + Deencapsulation.setField(env, "accessManager", spyAcm); + DropRowPolicyByRolesCommand command = new DropRowPolicyByRolesCommand( + Arrays.asList("role1")); + Assertions.assertThrows(Exception.class, () -> command.validate(connectContext)); + } + + @Test + public void testValidateEmptyRoleName() throws Exception { + runBefore(); + AccessControllerManager spyAcm = Mockito.spy(accessControllerManager); + Mockito.doReturn(true).when(spyAcm).checkGlobalPriv( + Mockito.nullable(ConnectContext.class), Mockito.eq(PrivPredicate.GRANT)); + Deencapsulation.setField(env, "accessManager", spyAcm); + DropRowPolicyByRolesCommand command = new DropRowPolicyByRolesCommand( + Arrays.asList("")); + Assertions.assertThrows(Exception.class, () -> command.validate(connectContext)); + } + + @Test + public void testGetRoleNames() { + DropRowPolicyByRolesCommand command = new DropRowPolicyByRolesCommand( + Arrays.asList("role1", "role2", "role3")); + Assertions.assertEquals(Arrays.asList("role1", "role2", "role3"), command.getRoleNames()); + } + + @Test + public void testParseBacktickRoleName() { + NereidsParser parser = new NereidsParser(); + String[] sqls = { + "DROP ROW POLICY FOR ROLE role1", + "DROP ROW POLICY FOR ROLE role1, role2", + "DROP ROW POLICY FOR ROLE `test-role`", + "DROP ROW POLICY FOR ROLE `test-role`, role2", + }; + for (String sql : sqls) { + Assertions.assertDoesNotThrow(() -> { + Plan plan = parser.parseSingle(sql); + Assertions.assertInstanceOf(DropRowPolicyByRolesCommand.class, plan); + }, "Failed to parse: " + sql); + } + } +} diff --git a/regression-test/suites/account_p0/test_drop_row_policy_by_roles.groovy b/regression-test/suites/account_p0/test_drop_row_policy_by_roles.groovy new file mode 100644 index 00000000000000..bdcf939a21189d --- /dev/null +++ b/regression-test/suites/account_p0/test_drop_row_policy_by_roles.groovy @@ -0,0 +1,130 @@ +// 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_drop_row_policy_by_roles") { + def dbName = context.config.getDbNameByFile(context.file) + def tableName = "drop_row_policy_by_roles_tbl" + def user1 = "drop_rp_role_user1" + def user2 = "drop_rp_role_user2" + def role1 = "drop_rp_role1" + def role2 = "drop_rp_role2" + def tokens = context.config.getDbNameByFile(context.file) + def url = context.config.jdbcUrl + + sql "DROP TABLE IF EXISTS ${tableName}" + sql """ + CREATE TABLE ${tableName} ( + `k` INT, + `v` INT + ) DUPLICATE KEY (`k`) DISTRIBUTED BY HASH (`k`) BUCKETS 1 + PROPERTIES ('replication_num' = '1') + """ + sql "INSERT INTO ${tableName} VALUES (1,1), (2,2), (3,3)" + + sql "DROP USER IF EXISTS ${user1}" + sql "DROP USER IF EXISTS ${user2}" + sql "CREATE USER ${user1} IDENTIFIED BY '123abc!@#'" + sql "CREATE USER ${user2} IDENTIFIED BY '123abc!@#'" + sql "GRANT SELECT_PRIV ON internal.${dbName}.${tableName} TO ${user1}" + sql "GRANT SELECT_PRIV ON internal.${dbName}.${tableName} TO ${user2}" + + sql "DROP ROLE IF EXISTS ${role1}" + sql "DROP ROLE IF EXISTS ${role2}" + sql "CREATE ROLE ${role1}" + sql "CREATE ROLE ${role2}" + sql "GRANT ${role1} TO ${user1}" + sql "GRANT ${role2} TO ${user2}" + + sql "GRANT SELECT_PRIV ON internal.${dbName}.${tableName} TO ROLE ${role1}" + sql "GRANT SELECT_PRIV ON internal.${dbName}.${tableName} TO ROLE ${role2}" + + // clean up existing policies + sql "DROP ROW POLICY IF EXISTS rp1 ON ${dbName}.${tableName} FOR ROLE ${role1}" + sql "DROP ROW POLICY IF EXISTS rp2 ON ${dbName}.${tableName} FOR ROLE ${role1}" + sql "DROP ROW POLICY IF EXISTS rp3 ON ${dbName}.${tableName} FOR ROLE ${role2}" + sql "DROP ROW POLICY IF EXISTS rp4 ON ${dbName}.${tableName} FOR ROLE ${role2}" + + // create row policies for role1 and role2 + sql """ + CREATE ROW POLICY IF NOT EXISTS rp1 ON ${dbName}.${tableName} + AS RESTRICTIVE TO ROLE ${role1} USING (k = 1) + """ + sql """ + CREATE ROW POLICY IF NOT EXISTS rp2 ON ${dbName}.${tableName} + AS RESTRICTIVE TO ROLE ${role1} USING (v = 1) + """ + sql """ + CREATE ROW POLICY IF NOT EXISTS rp3 ON ${dbName}.${tableName} + AS RESTRICTIVE TO ROLE ${role2} USING (k = 2) + """ + sql """ + CREATE ROW POLICY IF NOT EXISTS rp4 ON ${dbName}.${tableName} + AS RESTRICTIVE TO ROLE ${role2} USING (v = 2) + """ + + sql 'sync' + + // verify policies exist + def showResult1 = sql "SHOW ROW POLICY FOR ROLE ${role1}" + assertEquals(2, showResult1.size()) + def showResult2 = sql "SHOW ROW POLICY FOR ROLE ${role2}" + assertEquals(2, showResult2.size()) + + // drop all row policies for role1 and role2 in one statement + sql "DROP ROW POLICY FOR ROLE ${role1}, ${role2}" + + sql 'sync' + + // verify all policies are gone + def showResultAfter1 = sql "SHOW ROW POLICY FOR ROLE ${role1}" + assertEquals(0, showResultAfter1.size()) + def showResultAfter2 = sql "SHOW ROW POLICY FOR ROLE ${role2}" + assertEquals(0, showResultAfter2.size()) + + // test: drop with non-existent role should not throw error + sql "DROP ROW POLICY FOR ROLE non_existent_role" + + // recreate policies for single role drop test + sql """ + CREATE ROW POLICY IF NOT EXISTS rp5 ON ${dbName}.${tableName} + AS RESTRICTIVE TO ROLE ${role1} USING (k = 1) + """ + sql """ + CREATE ROW POLICY IF NOT EXISTS rp6 ON ${dbName}.${tableName} + AS RESTRICTIVE TO ROLE ${role2} USING (k = 2) + """ + + sql 'sync' + + // drop only role1's policies + sql "DROP ROW POLICY FOR ROLE ${role1}" + + sql 'sync' + + // verify role1's policies are gone but role2's remain + def showResultRole1 = sql "SHOW ROW POLICY FOR ROLE ${role1}" + assertEquals(0, showResultRole1.size()) + def showResultRole2 = sql "SHOW ROW POLICY FOR ROLE ${role2}" + assertEquals(1, showResultRole2.size()) + + // cleanup + sql "DROP ROW POLICY FOR ROLE ${role2}" + sql "DROP USER IF EXISTS ${user1}" + sql "DROP USER IF EXISTS ${user2}" + sql "DROP ROLE IF EXISTS ${role1}" + sql "DROP ROLE IF EXISTS ${role2}" +}