You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@doris.apache.org by yi...@apache.org on 2026/10/08 03:23:48 UTC

(doris) 17/35: branch-4.1: [fix](auth) Check column privileges held by grantor in GRANT/REVOKE (#68629)

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 2f9f1e253fa924815caf2642a372c66dfdd2ab7e
Author: Calvin Kirs <gu...@selectdb.com>
AuthorDate: Tue Sep 29 23:35:34 2026 +0800

    branch-4.1: [fix](auth) Check column privileges held by grantor in GRANT/REVOKE (#68629)
    
    https://github.com/apache/doris/pull/68555
---
 .../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: commits-unsubscribe@doris.apache.org
For additional commands, e-mail: commits-help@doris.apache.org