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]

Reply via email to