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 9d499a9400f [fix](auth) Reject privilege object names with more than 
three parts in GRANT/REVOKE (#68660)
9d499a9400f is described below

commit 9d499a9400f2bd245eec3951b7cb38ac7e92c84f
Author: Calvin Kirs <[email protected]>
AuthorDate: Wed Sep 30 18:24:47 2026 +0800

    [fix](auth) Reject privilege object names with more than three parts in 
GRANT/REVOKE (#68660)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary:
    
    The grammar of table privilege statements accepts an object name with
    any number of dot-separated parts (`multipartIdentifierOrAsterisk`), but
    `LogicalPlanBuilder` only turned 1, 2 or 3 parts into a `TablePattern`
    (db, db.tbl, ctl.db.tbl) and left it null for anything else. The null
    pattern then reached the commands and surfaced as an internal error:
    
    ```sql
    GRANT SELECT_PRIV ON a.b.c.d TO 'u'@'%';
    -- errCode = 2, detailMessage = tablePattern is null
    REVOKE SELECT_PRIV ON a.b.c.d FROM 'u'@'%';
    -- errCode = 2, detailMessage = Cannot invoke 
"org.apache.doris.analysis.TablePattern.analyze()" because "this.tablePattern" 
is null
    ```
    
    GRANT failed on the `Objects.requireNonNull` in the
    `GrantTablePrivilegeCommand` constructor; REVOKE had no such check and
    hit a NullPointerException in `validate()`. For comparison, MySQL
    rejects the same statements as a syntax error (`ERROR 1064 ... near
    '.c.d TO ...'`).
    
    Fix:
    
    - Build the `TablePattern` of both statements in one helper,
    `parsePrivilegeTablePattern`, which throws a `ParseException` naming the
    accepted shapes for any other part count:
    
      ```
    Privilege object name should be db, db.tbl or ctl.db.tbl, but got:
    a.b.c.d(line 1, pos 21)
    
      == SQL ==
      GRANT SELECT_PRIV ON a.b.c.d TO 'u'@'%'
      ---------------------^^^
      ```
    
    - `RevokeTablePrivilegeCommand` now rejects null arguments in its
    constructor, the same as `GrantTablePrivilegeCommand`. The `tablePattern
    != null` checks in `RevokeTablePrivilegeCommand.validate()` and
    `Auth.revokeTablePrivilegeCommand()` are removed since the pattern can
    no longer be null; the one in `Auth` would have skipped the revoke
    silently instead of failing.
    
    ### Release note
    
    GRANT/REVOKE with a table privilege object name of more than three parts
    (for example `a.b.c.d`) now fails with the parse error "Privilege object
    name should be db, db.tbl or ctl.db.tbl" instead of an internal null
    pointer error.
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [x] Regression test
        - [x] Unit Test
        - [x] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - FE UT: `GrantTablePrivilegeCommandTest` and
    `RevokeTablePrivilegeCommandTest` gain `testObjectName` (1/2/3-part
    mapping) and `testObjectNameWithTooManyParts` (4/5 parts and `*.*.*.*`
    rejected); the rejection cases fail without the fix. 14 privilege and
    parser test classes (63 tests) pass.
    - Regression: new `account_p0/test_grant_revoke_object_name`, run on a
    local cluster with the FE built from this branch.
    - Manual: the statements above, plus `a.b.c.d.e`, `` `a`.`b`.`c`.`d` ``
    and `GRANT ... TO ROLE`, all return the parse error; `a.b.c` still goes
    through the normal path.
---
 .../org/apache/doris/mysql/privilege/Auth.java     | 10 ++--
 .../doris/nereids/parser/LogicalPlanBuilder.java   | 68 +++++++---------------
 .../commands/RevokeTablePrivilegeCommand.java      | 13 ++---
 .../commands/GrantTablePrivilegeCommandTest.java   | 29 +++++++++
 .../commands/RevokeTablePrivilegeCommandTest.java  | 29 +++++++++
 .../test_grant_revoke_object_name.groovy           | 51 ++++++++++++++++
 6 files changed, 139 insertions(+), 61 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/Auth.java 
b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/Auth.java
index e25720f63e7..69f2c33e3fd 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/Auth.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/Auth.java
@@ -926,12 +926,10 @@ public class Auth implements Writable {
 
     // revoke table
     public void revokeTablePrivilegeCommand(RevokeTablePrivilegeCommand 
command) throws DdlException {
-        if (command.getTablePattern() != null) {
-            PrivBitSet privs = PrivBitSet.of(command.getPrivileges());
-            revokeInternal(command.getUserIdentity().orElse(null), 
command.getRole().orElse(null),
-                    command.getTablePattern(), privs, 
command.getColPrivileges(),
-                    true /* err on non exist */, false /* is replay */);
-        }
+        PrivBitSet privs = PrivBitSet.of(command.getPrivileges());
+        revokeInternal(command.getUserIdentity().orElse(null), 
command.getRole().orElse(null),
+                command.getTablePattern(), privs, command.getColPrivileges(),
+                true /* err on non exist */, false /* is replay */);
     }
 
     public void replayRevoke(PrivInfo info) {
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 443e0b50a3a..a7c520d2f5d 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
@@ -9182,35 +9182,28 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
         return new TransactionRollbackCommand();
     }
 
+    /**
+     * The object of a table privilege is db, db.tbl or ctl.db.tbl; 
TablePattern.analyze() checks its wildcards.
+     */
+    private TablePattern 
parsePrivilegeTablePattern(DorisParser.MultipartIdentifierOrAsteriskContext 
ctx) {
+        List<String> parts = visitMultipartIdentifierOrAsterisk(ctx);
+        switch (parts.size()) {
+            case 1:
+                return new TablePattern(parts.get(0), "");
+            case 2:
+                return new TablePattern(parts.get(0), parts.get(1));
+            case 3:
+                return new TablePattern(parts.get(0), parts.get(1), 
parts.get(2));
+            default:
+                throw new ParseException("Privilege object name should be db, 
db.tbl or ctl.db.tbl, but got: "
+                        + String.join(".", parts), ctx);
+        }
+    }
+
     @Override
     public LogicalPlan 
visitGrantTablePrivilege(DorisParser.GrantTablePrivilegeContext ctx) {
         List<AccessPrivilegeWithCols> accessPrivilegeWithCols = 
visitPrivilegeList(ctx.privilegeList());
-
-        List<String> parts = 
visitMultipartIdentifierOrAsterisk(ctx.multipartIdentifierOrAsterisk());
-        int size = parts.size();
-
-        if (size < 1) {
-            throw new AnalysisException("grant table privilege statement 
missing parameters");
-        }
-
-        TablePattern tablePattern = null;
-        if (size == 1) {
-            String db = parts.get(size - 1);
-            tablePattern = new TablePattern(db, "");
-        }
-
-        if (size == 2) {
-            String db = parts.get(size - 2);
-            String tbl = parts.get(size - 1);
-            tablePattern = new TablePattern(db, tbl);
-        }
-
-        if (size == 3) {
-            String ctl = parts.get(size - 3);
-            String db = parts.get(size - 2);
-            String tbl = parts.get(size - 1);
-            tablePattern = new TablePattern(ctl, db, tbl);
-        }
+        TablePattern tablePattern = 
parsePrivilegeTablePattern(ctx.multipartIdentifierOrAsterisk());
 
         Optional<UserIdentity> userIdentity = Optional.empty();
         Optional<String> role = Optional.empty();
@@ -9356,28 +9349,7 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
     @Override
     public LogicalPlan 
visitRevokeTablePrivilege(DorisParser.RevokeTablePrivilegeContext ctx) {
         List<AccessPrivilegeWithCols> accessPrivilegeWithCols = 
visitPrivilegeList(ctx.privilegeList());
-
-        List<String> parts = 
visitMultipartIdentifierOrAsterisk(ctx.multipartIdentifierOrAsterisk());
-        int size = parts.size();
-
-        TablePattern tablePattern = null;
-        if (size == 1) {
-            String db = parts.get(size - 1);
-            tablePattern = new TablePattern(db, "");
-        }
-
-        if (size == 2) {
-            String db = parts.get(size - 2);
-            String tbl = parts.get(size - 1);
-            tablePattern = new TablePattern(db, tbl);
-        }
-
-        if (size == 3) {
-            String ctl = parts.get(size - 3);
-            String db = parts.get(size - 2);
-            String tbl = parts.get(size - 1);
-            tablePattern = new TablePattern(ctl, db, tbl);
-        }
+        TablePattern tablePattern = 
parsePrivilegeTablePattern(ctx.multipartIdentifierOrAsterisk());
 
         Optional<UserIdentity> userIdentity = Optional.empty();
         Optional<String> role = Optional.empty();
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/RevokeTablePrivilegeCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/RevokeTablePrivilegeCommand.java
index 1748d6bd57b..5213df7cf01 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/RevokeTablePrivilegeCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/RevokeTablePrivilegeCommand.java
@@ -38,6 +38,7 @@ import org.apache.commons.collections4.MapUtils;
 
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 
@@ -59,10 +60,10 @@ public class RevokeTablePrivilegeCommand extends Command 
implements ForwardWithS
     public RevokeTablePrivilegeCommand(List<AccessPrivilegeWithCols> 
accessPrivileges, TablePattern tablePattern,
             Optional<UserIdentity> userIdentity, Optional<String> role) {
         super(PlanType.REVOKE_TABLE_PRIVILEGE_COMMAND);
-        this.accessPrivileges = accessPrivileges;
-        this.tablePattern = tablePattern;
-        this.userIdentity = userIdentity;
-        this.role = role;
+        this.accessPrivileges = Objects.requireNonNull(accessPrivileges, 
"accessPrivileges is null");
+        this.tablePattern = Objects.requireNonNull(tablePattern, "tablePattern 
is null");
+        this.userIdentity = Objects.requireNonNull(userIdentity, "userIdentity 
is null");
+        this.role = Objects.requireNonNull(role, "role is null");
     }
 
     @Override
@@ -100,9 +101,7 @@ public class RevokeTablePrivilegeCommand extends Command 
implements ForwardWithS
         }
 
         // Revoke operation obey the same rule as Grant operation. reuse the 
same method
-        if (tablePattern != null) {
-            GrantTablePrivilegeCommand.checkTablePrivileges(privileges, 
tablePattern, colPrivileges);
-        }
+        GrantTablePrivilegeCommand.checkTablePrivileges(privileges, 
tablePattern, colPrivileges);
     }
 
     @Override
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 65f80e75732..9683ed5b161 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
@@ -23,6 +23,7 @@ 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.exceptions.ParseException;
 import org.apache.doris.nereids.parser.NereidsParser;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
 import org.apache.doris.utframe.TestWithFeService;
@@ -191,4 +192,32 @@ public class GrantTablePrivilegeCommandTest extends 
TestWithFeService {
         LogicalPlan plan = new NereidsParser().parseSingle(sql);
         ((Command) plan).run(connectContext, null);
     }
+
+    @Test
+    public void testObjectName() {
+        NereidsParser nereidsParser = new NereidsParser();
+        String[][] cases = {
+                {"GRANT SELECT_PRIV ON test TO 'jack'", "test.*"},
+                {"GRANT SELECT_PRIV ON test.test_table TO 'jack'", 
"test.test_table"},
+                {"GRANT SELECT_PRIV ON internal.test.test_table TO 'jack'", 
"internal.test.test_table"},
+        };
+        for (String[] c : cases) {
+            LogicalPlan plan = nereidsParser.parseSingle(c[0]);
+            Assertions.assertTrue(plan instanceof GrantTablePrivilegeCommand, 
c[0]);
+            Assertions.assertEquals(c[1], ((GrantTablePrivilegeCommand) 
plan).getTablePattern().toString(), c[0]);
+        }
+    }
+
+    @Test
+    public void testObjectNameWithTooManyParts() {
+        NereidsParser nereidsParser = new NereidsParser();
+        for (String name : new String[] {"a.b.c.d", "*.*.*.*", "a.b.c.d.e"}) {
+            String sql = "GRANT SELECT_PRIV ON " + name + " TO 'jack'";
+            ParseException exception = 
Assertions.assertThrows(ParseException.class,
+                    () -> nereidsParser.parseSingle(sql), sql);
+            Assertions.assertTrue(exception.getMessage().contains(
+                    "Privilege object name should be db, db.tbl or ctl.db.tbl, 
but got: " + name),
+                    exception.getMessage());
+        }
+    }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/RevokeTablePrivilegeCommandTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/RevokeTablePrivilegeCommandTest.java
index 9c04d120eb1..06738995913 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/RevokeTablePrivilegeCommandTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/RevokeTablePrivilegeCommandTest.java
@@ -20,6 +20,7 @@ package org.apache.doris.nereids.trees.plans.commands;
 import org.apache.doris.analysis.TablePattern;
 import org.apache.doris.catalog.AccessPrivilege;
 import org.apache.doris.catalog.AccessPrivilegeWithCols;
+import org.apache.doris.nereids.exceptions.ParseException;
 import org.apache.doris.nereids.parser.NereidsParser;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
 import org.apache.doris.utframe.TestWithFeService;
@@ -95,4 +96,32 @@ public class RevokeTablePrivilegeCommandTest extends 
TestWithFeService {
         Assertions.assertTrue(revokeplan2 instanceof 
RevokeTablePrivilegeCommand);
         Assertions.assertDoesNotThrow(() -> ((RevokeTablePrivilegeCommand) 
revokeplan2).run(connectContext, null));
     }
+
+    @Test
+    public void testObjectName() {
+        NereidsParser nereidsParser = new NereidsParser();
+        String[][] cases = {
+                {"REVOKE SELECT_PRIV ON test FROM 'jack'", "test.*"},
+                {"REVOKE SELECT_PRIV ON test.test_table FROM 'jack'", 
"test.test_table"},
+                {"REVOKE SELECT_PRIV ON internal.test.test_table FROM 'jack'", 
"internal.test.test_table"},
+        };
+        for (String[] c : cases) {
+            LogicalPlan plan = nereidsParser.parseSingle(c[0]);
+            Assertions.assertTrue(plan instanceof RevokeTablePrivilegeCommand, 
c[0]);
+            Assertions.assertEquals(c[1], ((RevokeTablePrivilegeCommand) 
plan).getTablePattern().toString(), c[0]);
+        }
+    }
+
+    @Test
+    public void testObjectNameWithTooManyParts() {
+        NereidsParser nereidsParser = new NereidsParser();
+        for (String name : new String[] {"a.b.c.d", "*.*.*.*", "a.b.c.d.e"}) {
+            String sql = "REVOKE SELECT_PRIV ON " + name + " FROM 'jack'";
+            ParseException exception = 
Assertions.assertThrows(ParseException.class,
+                    () -> nereidsParser.parseSingle(sql), sql);
+            Assertions.assertTrue(exception.getMessage().contains(
+                    "Privilege object name should be db, db.tbl or ctl.db.tbl, 
but got: " + name),
+                    exception.getMessage());
+        }
+    }
 }
diff --git 
a/regression-test/suites/account_p0/test_grant_revoke_object_name.groovy 
b/regression-test/suites/account_p0/test_grant_revoke_object_name.groovy
new file mode 100644
index 00000000000..6801a383dd0
--- /dev/null
+++ b/regression-test/suites/account_p0/test_grant_revoke_object_name.groovy
@@ -0,0 +1,51 @@
+// 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_revoke_object_name") {
+    sql """DROP USER IF EXISTS 'test_grant_revoke_object_name_user'"""
+    sql """DROP ROLE IF EXISTS test_grant_revoke_object_name_role"""
+    sql """DROP DATABASE IF EXISTS test_grant_revoke_object_name_db"""
+    sql """CREATE DATABASE test_grant_revoke_object_name_db"""
+    sql """CREATE USER 'test_grant_revoke_object_name_user'"""
+    sql """CREATE ROLE test_grant_revoke_object_name_role"""
+
+    // db, db.tbl and ctl.db.tbl are the only object name shapes a table 
privilege takes
+    sql """GRANT SELECT_PRIV ON test_grant_revoke_object_name_db TO 
'test_grant_revoke_object_name_user'"""
+    sql """REVOKE SELECT_PRIV ON test_grant_revoke_object_name_db FROM 
'test_grant_revoke_object_name_user'"""
+    sql """GRANT SELECT_PRIV ON test_grant_revoke_object_name_db.* TO 
'test_grant_revoke_object_name_user'"""
+    sql """REVOKE SELECT_PRIV ON test_grant_revoke_object_name_db.* FROM 
'test_grant_revoke_object_name_user'"""
+    sql """GRANT SELECT_PRIV ON internal.test_grant_revoke_object_name_db.* TO 
'test_grant_revoke_object_name_user'"""
+    sql """REVOKE SELECT_PRIV ON internal.test_grant_revoke_object_name_db.* 
FROM 'test_grant_revoke_object_name_user'"""
+
+    // four or more parts are rejected while parsing, for both a user and a 
role
+    test {
+        sql """GRANT SELECT_PRIV ON a.b.c.d TO 
'test_grant_revoke_object_name_user'"""
+        exception "Privilege object name should be db, db.tbl or ctl.db.tbl, 
but got: a.b.c.d"
+    }
+    test {
+        sql """REVOKE SELECT_PRIV ON a.b.c.d FROM 
'test_grant_revoke_object_name_user'"""
+        exception "Privilege object name should be db, db.tbl or ctl.db.tbl, 
but got: a.b.c.d"
+    }
+    test {
+        sql """GRANT SELECT_PRIV ON 
internal.test_grant_revoke_object_name_db.t.c TO ROLE 
test_grant_revoke_object_name_role"""
+        exception "Privilege object name should be db, db.tbl or ctl.db.tbl, 
but got: internal.test_grant_revoke_object_name_db.t.c"
+    }
+    test {
+        sql """REVOKE SELECT_PRIV ON *.*.*.* FROM ROLE 
test_grant_revoke_object_name_role"""
+        exception "Privilege object name should be db, db.tbl or ctl.db.tbl, 
but got: *.*.*.*"
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to