This is an automated email from the ASF dual-hosted git repository.
CalvinKirs pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 66d1bf5de44 [fix](auth) Check column privileges held by grantor in
GRANT/REVOKE (#68555)
66d1bf5de44 is described below
commit 66d1bf5de4480b2d04f4e1775574bbd9a53de05e
Author: Calvin Kirs <[email protected]>
AuthorDate: Wed Sep 30 09:29:05 2026 +0800
[fix](auth) Check column privileges held by grantor in GRANT/REVOKE (#68555)
Rule 4 in `GrantTablePrivilegeCommand.checkTablePrivileges` (#32825)
only checks table-level privileges, so a column-level GRANT/REVOKE such
as `SELECT_PRIV(c1)` was checked for GRANT_PRIV alone. This PR also
requires the grantor to hold that privilege on the table or on each
listed column, the same as for table-level privileges.
---
.../plans/commands/GrantTablePrivilegeCommand.java | 25 ++++-
.../commands/GrantTablePrivilegeCommandTest.java | 87 ++++++++++++++++
.../data/account_p0/test_grant_col_priv.out | 7 ++
.../suites/account_p0/test_grant_col_priv.groovy | 109 +++++++++++++++++++++
4 files changed, 226 insertions(+), 2 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/GrantTablePrivilegeCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/GrantTablePrivilegeCommand.java
index 16169ef8326..0672cf57be5 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/GrantTablePrivilegeCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/GrantTablePrivilegeCommand.java
@@ -27,6 +27,7 @@ import org.apache.doris.common.Config;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.FeNameFormat;
+import org.apache.doris.common.UserException;
import org.apache.doris.mysql.privilege.AccessControllerManager;
import org.apache.doris.mysql.privilege.Auth;
import org.apache.doris.mysql.privilege.ColPrivilegeKey;
@@ -133,6 +134,7 @@ public class GrantTablePrivilegeCommand extends Command
implements ForwardWithSy
* 3. Only the user with NODE_PRIV can grant NODE_PRIV to other user
* 4. Check that the current user has both grant_priv and the permissions
to be assigned to others
* 5. col priv must assign to specific table
+ * 6. Check that the current user has the col privs to be assigned to
others, on the table or on each col
*/
public static void checkTablePrivileges(Collection<Privilege> privileges,
TablePattern tblPattern,
Map<ColPrivilegeKey, Set<String>> colPrivileges) throws
AnalysisException {
@@ -153,8 +155,8 @@ public class GrantTablePrivilegeCommand extends Command
implements ForwardWithSy
// Rule 4
PrivPredicate predicate = getPrivPredicate(privileges);
AccessControllerManager accessManager =
Env.getCurrentEnv().getAccessManager();
- if (!accessManager.checkGlobalPriv(ConnectContext.get(),
PrivPredicate.ADMIN)
- && !checkTablePriv(ConnectContext.get(), predicate,
tblPattern)) {
+ boolean isAdmin = accessManager.checkGlobalPriv(ConnectContext.get(),
PrivPredicate.ADMIN);
+ if (!isAdmin && !checkTablePriv(ConnectContext.get(), predicate,
tblPattern)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ALL_ACCESS_DENIED_ERROR,
predicate.getPrivs().toPrivilegeList());
}
@@ -163,6 +165,25 @@ public class GrantTablePrivilegeCommand extends Command
implements ForwardWithSy
if (!MapUtils.isEmpty(colPrivileges) &&
"*".equals(tblPattern.getTbl())) {
throw new AnalysisException("Col auth must specify specific
table");
}
+
+ // Rule 6
+ if (!isAdmin) {
+ checkColPrivs(ConnectContext.get(), colPrivileges);
+ }
+ }
+
+ private static void checkColPrivs(ConnectContext ctx, Map<ColPrivilegeKey,
Set<String>> colPrivileges)
+ throws AnalysisException {
+ AccessControllerManager accessManager =
Env.getCurrentEnv().getAccessManager();
+ for (Map.Entry<ColPrivilegeKey, Set<String>> entry :
colPrivileges.entrySet()) {
+ ColPrivilegeKey key = entry.getKey();
+ PrivPredicate wanted =
PrivPredicate.of(PrivBitSet.of(key.getPrivilege()),
CompoundPredicate.Operator.OR);
+ try {
+ accessManager.checkColumnsPriv(ctx, key.getCtl(), key.getDb(),
key.getTbl(), entry.getValue(), wanted);
+ } catch (UserException e) {
+ throw new AnalysisException(e.getMessage(), e);
+ }
+ }
}
private static PrivPredicate getPrivPredicate(Collection<Privilege>
privileges) {
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/GrantTablePrivilegeCommandTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/GrantTablePrivilegeCommandTest.java
index bd022f4e2af..65f80e75732 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/GrantTablePrivilegeCommandTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/GrantTablePrivilegeCommandTest.java
@@ -18,8 +18,10 @@
package org.apache.doris.nereids.trees.plans.commands;
import org.apache.doris.analysis.TablePattern;
+import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.AccessPrivilege;
import org.apache.doris.catalog.AccessPrivilegeWithCols;
+import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.DdlException;
import org.apache.doris.nereids.parser.NereidsParser;
import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
@@ -104,4 +106,89 @@ public class GrantTablePrivilegeCommandTest extends
TestWithFeService {
Assertions.assertTrue(plan instanceof GrantTablePrivilegeCommand);
Assertions.assertThrows(DdlException.class, () ->
((GrantTablePrivilegeCommand) plan).run(connectContext, null));
}
+
+ @Test
+ public void testGrantColPrivWithOnlyGrantPriv() throws Exception {
+ addUser("col_grantor1", true);
+ addUser("col_target1", true);
+ grantPriv("GRANT GRANT_PRIV ON test.test_table TO 'col_grantor1'");
+ try {
+ useUser("col_grantor1");
+ Assertions.assertThrows(AnalysisException.class,
+ () -> runCommand("GRANT SELECT_PRIV(k1) ON test.test_table
TO 'col_target1'"));
+ Assertions.assertThrows(AnalysisException.class,
+ () -> runCommand("GRANT SELECT_PRIV(k1, k2) ON
test.test_table TO 'col_grantor1'"));
+ Assertions.assertThrows(AnalysisException.class,
+ () -> runCommand("REVOKE SELECT_PRIV(k1) ON
test.test_table FROM 'col_target1'"));
+ } finally {
+ connectContext.setCurrentUserIdentity(UserIdentity.ROOT);
+ }
+ }
+
+ @Test
+ public void testGrantColPrivWithColSelectPriv() throws Exception {
+ addUser("col_grantor2", true);
+ addUser("col_target2", true);
+ grantPriv("GRANT GRANT_PRIV, SELECT_PRIV(k1) ON test.test_table TO
'col_grantor2'");
+ try {
+ useUser("col_grantor2");
+ Assertions.assertDoesNotThrow(
+ () -> runCommand("GRANT SELECT_PRIV(k1) ON test.test_table
TO 'col_target2'"));
+ AnalysisException e =
Assertions.assertThrows(AnalysisException.class,
+ () -> runCommand("GRANT SELECT_PRIV(k2) ON test.test_table
TO 'col_target2'"));
+ Assertions.assertTrue(e.getMessage().contains("k2"),
e.getMessage());
+ Assertions.assertThrows(AnalysisException.class,
+ () -> runCommand("GRANT SELECT_PRIV(k1, k2) ON
test.test_table TO 'col_target2'"));
+ Assertions.assertThrows(AnalysisException.class,
+ () -> runCommand("REVOKE SELECT_PRIV(k2) ON
test.test_table FROM 'col_target2'"));
+ Assertions.assertDoesNotThrow(
+ () -> runCommand("REVOKE SELECT_PRIV(k1) ON
test.test_table FROM 'col_target2'"));
+ } finally {
+ connectContext.setCurrentUserIdentity(UserIdentity.ROOT);
+ }
+ }
+
+ @Test
+ public void testGrantColPrivWithTableSelectPriv() throws Exception {
+ addUser("col_grantor3", true);
+ addUser("col_target3", true);
+ grantPriv("GRANT GRANT_PRIV, SELECT_PRIV ON test.test_table TO
'col_grantor3'");
+ try {
+ useUser("col_grantor3");
+ Assertions.assertDoesNotThrow(
+ () -> runCommand("GRANT SELECT_PRIV(k1, k2) ON
test.test_table TO 'col_target3'"));
+ Assertions.assertDoesNotThrow(
+ () -> runCommand("REVOKE SELECT_PRIV(k2) ON
test.test_table FROM 'col_target3'"));
+ } finally {
+ connectContext.setCurrentUserIdentity(UserIdentity.ROOT);
+ }
+ }
+
+ @Test
+ public void testGrantColPrivWithDbGrantPriv() throws Exception {
+ addUser("col_grantor4", true);
+ addUser("col_target4", true);
+ grantPriv("GRANT GRANT_PRIV ON test.* TO 'col_grantor4'");
+ try {
+ useUser("col_grantor4");
+ Assertions.assertThrows(AnalysisException.class,
+ () -> runCommand("GRANT SELECT_PRIV(k1) ON test.test_table
TO 'col_target4'"));
+ } finally {
+ connectContext.setCurrentUserIdentity(UserIdentity.ROOT);
+ }
+ }
+
+ @Test
+ public void testGrantColPrivByAdmin() throws Exception {
+ addUser("col_target5", true);
+ Assertions.assertDoesNotThrow(
+ () -> runCommand("GRANT SELECT_PRIV(k1) ON test.test_table TO
'col_target5'"));
+ Assertions.assertDoesNotThrow(
+ () -> runCommand("REVOKE SELECT_PRIV(k1) ON test.test_table
FROM 'col_target5'"));
+ }
+
+ private void runCommand(String sql) throws Exception {
+ LogicalPlan plan = new NereidsParser().parseSingle(sql);
+ ((Command) plan).run(connectContext, null);
+ }
}
diff --git a/regression-test/data/account_p0/test_grant_col_priv.out
b/regression-test/data/account_p0/test_grant_col_priv.out
new file mode 100644
index 00000000000..e26ff33084a
--- /dev/null
+++ b/regression-test/data/account_p0/test_grant_col_priv.out
@@ -0,0 +1,7 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !select_c1 --
+1
+
+-- !select_c1_c2 --
+1 2
+
diff --git a/regression-test/suites/account_p0/test_grant_col_priv.groovy
b/regression-test/suites/account_p0/test_grant_col_priv.groovy
new file mode 100644
index 00000000000..fb19a7a7ca8
--- /dev/null
+++ b/regression-test/suites/account_p0/test_grant_col_priv.groovy
@@ -0,0 +1,109 @@
+// 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_grant_col_priv") {
+ String pwd = 'C123_567p'
+
+ try_sql("drop user test_grant_col_priv_grantor")
+ try_sql("drop user test_grant_col_priv_target")
+ sql """drop database if exists test_grant_col_priv_db"""
+
+ sql """create database test_grant_col_priv_db"""
+ sql """
+ create table test_grant_col_priv_db.test_grant_col_priv_tbl (c1 int,
c2 int)
+ duplicate key(c1)
+ distributed by hash(c1) buckets 1
+ properties ("replication_num" = "1")
+ """
+ sql """insert into test_grant_col_priv_db.test_grant_col_priv_tbl values
(1, 2)"""
+
+ sql """create user 'test_grant_col_priv_grantor' identified by '${pwd}'"""
+ sql """create user 'test_grant_col_priv_target' identified by '${pwd}'"""
+ //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
test_grant_col_priv_grantor"""
+ sql """GRANT USAGE_PRIV ON CLUSTER `${validCluster}` TO
test_grant_col_priv_target"""
+ }
+ // for login
+ sql """grant select_priv on regression_test to
test_grant_col_priv_grantor"""
+ sql """grant select_priv on regression_test to
test_grant_col_priv_target"""
+
+ // have grant_priv only, can not grant col select_priv
+ sql """grant grant_priv on test_grant_col_priv_db.test_grant_col_priv_tbl
to test_grant_col_priv_grantor"""
+ connect('test_grant_col_priv_grantor', "${pwd}", context.config.jdbcUrl) {
+ test {
+ sql """grant select_priv(c1) on
test_grant_col_priv_db.test_grant_col_priv_tbl to test_grant_col_priv_target"""
+ exception "denied"
+ }
+ test {
+ sql """grant select_priv(c1, c2) on
test_grant_col_priv_db.test_grant_col_priv_tbl to test_grant_col_priv_grantor"""
+ exception "denied"
+ }
+ }
+ connect('test_grant_col_priv_target', "${pwd}", context.config.jdbcUrl) {
+ test {
+ sql """select c1 from
test_grant_col_priv_db.test_grant_col_priv_tbl"""
+ exception "denied"
+ }
+ }
+
+ // have grant_priv and select_priv on c1, can grant/revoke select_priv on
c1 but not on c2
+ sql """grant select_priv(c1) on
test_grant_col_priv_db.test_grant_col_priv_tbl to test_grant_col_priv_grantor"""
+ connect('test_grant_col_priv_grantor', "${pwd}", context.config.jdbcUrl) {
+ sql """grant select_priv(c1) on
test_grant_col_priv_db.test_grant_col_priv_tbl to test_grant_col_priv_target"""
+ test {
+ sql """grant select_priv(c2) on
test_grant_col_priv_db.test_grant_col_priv_tbl to test_grant_col_priv_target"""
+ exception "denied"
+ }
+ test {
+ sql """grant select_priv(c1, c2) on
test_grant_col_priv_db.test_grant_col_priv_tbl to test_grant_col_priv_target"""
+ exception "denied"
+ }
+ test {
+ sql """revoke select_priv(c2) on
test_grant_col_priv_db.test_grant_col_priv_tbl from
test_grant_col_priv_target"""
+ exception "denied"
+ }
+ }
+ connect('test_grant_col_priv_target', "${pwd}", context.config.jdbcUrl) {
+ order_qt_select_c1 """select c1 from
test_grant_col_priv_db.test_grant_col_priv_tbl"""
+ test {
+ sql """select c2 from
test_grant_col_priv_db.test_grant_col_priv_tbl"""
+ exception "denied"
+ }
+ }
+ connect('test_grant_col_priv_grantor', "${pwd}", context.config.jdbcUrl) {
+ sql """revoke select_priv(c1) on
test_grant_col_priv_db.test_grant_col_priv_tbl from
test_grant_col_priv_target"""
+ }
+ connect('test_grant_col_priv_target', "${pwd}", context.config.jdbcUrl) {
+ test {
+ sql """select c1 from
test_grant_col_priv_db.test_grant_col_priv_tbl"""
+ exception "denied"
+ }
+ }
+
+ // have grant_priv and select_priv on the table, can grant select_priv on
any col
+ sql """grant select_priv on test_grant_col_priv_db.test_grant_col_priv_tbl
to test_grant_col_priv_grantor"""
+ connect('test_grant_col_priv_grantor', "${pwd}", context.config.jdbcUrl) {
+ sql """grant select_priv(c1, c2) on
test_grant_col_priv_db.test_grant_col_priv_tbl to test_grant_col_priv_target"""
+ }
+ connect('test_grant_col_priv_target', "${pwd}", context.config.jdbcUrl) {
+ order_qt_select_c1_c2 """select c1, c2 from
test_grant_col_priv_db.test_grant_col_priv_tbl"""
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]