This is an automated email from the ASF dual-hosted git repository.

dataroaring 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 0a7e8673cf6 [fix](nereids) Fix incorrect isDomain parsing in SET 
PASSWORD statement (#60565)
0a7e8673cf6 is described below

commit 0a7e8673cf65ad3cf1a2e097e240ed66105c49cc
Author: bobhan1 <[email protected]>
AuthorDate: Wed Feb 11 08:52:17 2026 +0800

    [fix](nereids) Fix incorrect isDomain parsing in SET PASSWORD statement 
(#60565)
    
    ### What problem does this PR solve?
    
    Issue Number: close #xxx
    
    Problem Summary:
    ```
    MySQL [email protected]:(none)> create user t1 identified by '12345';
    Query OK, 0 rows affected
    Time: 0.008s
    MySQL [email protected]:(none)> show all grants;
    
+--------------+---------+----------+----------+----------------------+--------------+-----------------------------------------------------------------------+------------+----------+---------------+-------------------+-----------------+-------------------+--------------------+-------------------+
    | UserIdentity | Comment | Password | Roles    | GlobalPrivs          | 
CatalogPrivs | DatabasePrivs                                                    
     | TablePrivs | ColPrivs | ResourcePrivs | CloudClusterPrivs | 
CloudStagePrivs | StorageVaultPrivs | WorkloadGroupPrivs | ComputeGroupPrivs |
    
+--------------+---------+----------+----------+----------------------+--------------+-----------------------------------------------------------------------+------------+----------+---------------+-------------------+-----------------+-------------------+--------------------+-------------------+
    | 'admin'@'%'  | ADMIN   | No       | admin    | Admin_priv           | 
<null>       | internal.information_schema: Select_priv; internal.mysql: 
Select_priv | <null>     | <null>   | <null>        | <null>            | 
<null>          | <null>            | normal: Usage_priv | <null>            |
    | 'root'@'%'   | ROOT    | No       | operator | Node_priv,Admin_priv | 
<null>       | internal.information_schema: Select_priv; internal.mysql: 
Select_priv | <null>     | <null>   | <null>        | <null>            | 
<null>          | <null>            | normal: Usage_priv | <null>            |
    | 't1'@'%'     |         | Yes      |          | <null>               | 
<null>       | internal.information_schema: Select_priv; internal.mysql: 
Select_priv | <null>     | <null>   | <null>        | <null>            | 
<null>          | <null>            | normal: Usage_priv | <null>            |
    
+--------------+---------+----------+----------+----------------------+--------------+-----------------------------------------------------------------------+------------+----------+---------------+-------------------+-----------------+-------------------+--------------------+-------------------+
    3 rows in set
    Time: 0.019s
    MySQL [email protected]:(none)> set password for 't1'@'%' = password("123");
    (1105, "errCode = 2, detailMessage = user 't1'@['%'] does not exist")
    ```
    
    Fix incorrect `isDomain` flag parsing in `SET PASSWORD` statement.
    
    When parsing `SET PASSWORD FOR 'user'@'host'` statement, the `isDomain`
    flag was incorrectly set to `true` because the previous code used
    `ctx.userIdentify().ATSIGN() != null` to determine if a user is a domain
    user. Since the `@` symbol always exists in `'user'@'host'` format,
    `isDomain` was always `true`, which is wrong.
    
    The correct behavior:
    - `'user'@'host'` (single quotes) → `isDomain = false` (regular user)
    - `'user'@("domain")` (parentheses around host) → `isDomain = true`
    (domain user)
    
    This PR fixes the issue by reusing the existing `visitUserIdentify()`
    method which correctly handles the `isDomain` flag.
    
    ### Release note
    
    Fix SET PASSWORD statement incorrectly marking regular users as domain
    users.
    
    ### Check List (For Author)
    
    - Test
        - [x] Regression test
        - [x] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    
    - Behavior changed:
    - [x] Yes. Previously `SET PASSWORD FOR 'user'@'%'` would fail because
    the user was incorrectly treated as a domain user. Now it works
    correctly.
    
    - Does this need documentation?
        - [x] No.
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label
---
 .../doris/nereids/parser/LogicalPlanBuilder.java   |   8 +-
 .../trees/plans/commands/SetOptionsCommand.java    |   4 +
 .../trees/plans/commands/info/SetPassVarOp.java    |   4 +
 .../doris/nereids/parser/SetPasswordParseTest.java | 128 +++++++++++++++++++++
 .../suites/account_p0/test_set_password.groovy     |  69 +++++++++++
 5 files changed, 206 insertions(+), 7 deletions(-)

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 ef1808ec2e7..3291f25651f 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
@@ -5550,16 +5550,10 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
 
     @Override
     public SetVarOp visitSetPassword(SetPasswordContext ctx) {
-        String user;
-        String host;
-        boolean isDomain;
         String passwordText;
         UserIdentity userIdentity = null;
         if (ctx.userIdentify() != null) {
-            user = stripQuotes(ctx.userIdentify().user.getText());
-            host = ctx.userIdentify().host != null ? 
stripQuotes(ctx.userIdentify().host.getText()) : "%";
-            isDomain = ctx.userIdentify().ATSIGN() != null;
-            userIdentity = new UserIdentity(user, host, isDomain);
+            userIdentity = visitUserIdentify(ctx.userIdentify());
         }
         passwordText = stripQuotes(ctx.STRING_LITERAL().getText());
         return new SetPassVarOp(userIdentity, new PassVar(passwordText, 
ctx.isPlain != null));
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/SetOptionsCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/SetOptionsCommand.java
index f62ff61ccf8..6ab3fb6732b 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/SetOptionsCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/SetOptionsCommand.java
@@ -87,4 +87,8 @@ public class SetOptionsCommand extends Command implements 
Forward, NeedAuditEncr
             varOp.afterForwardToMaster(ctx);
         }
     }
+
+    public List<SetVarOp> getSetVarOps() {
+        return setVarOpList;
+    }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/SetPassVarOp.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/SetPassVarOp.java
index 718c9acd4b0..f142b9679a2 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/SetPassVarOp.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/SetPassVarOp.java
@@ -98,4 +98,8 @@ public class SetPassVarOp extends SetVarOp {
     public boolean needAuditEncryption() {
         return true;
     }
+
+    public UserIdentity getUserIdent() {
+        return userIdent;
+    }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/SetPasswordParseTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/SetPasswordParseTest.java
new file mode 100644
index 00000000000..789cf84680b
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/SetPasswordParseTest.java
@@ -0,0 +1,128 @@
+// 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.parser;
+
+import org.apache.doris.analysis.UserIdentity;
+import org.apache.doris.common.AnalysisException;
+import org.apache.doris.nereids.trees.plans.commands.SetOptionsCommand;
+import org.apache.doris.nereids.trees.plans.commands.info.SetPassVarOp;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit test for SET PASSWORD statement parsing.
+ * This test verifies that UserIdentity is correctly parsed with proper 
isDomain flag.
+ */
+public class SetPasswordParseTest {
+
+    /**
+     * Test SET PASSWORD FOR 'user'@'%' - should NOT be a domain user.
+     * Bug fix: Previously, ATSIGN() check incorrectly set isDomain=true.
+     */
+    @Test
+    public void testSetPasswordNonDomainUser() throws AnalysisException {
+        String sql = "SET PASSWORD FOR 't1'@'%' = PASSWORD('123')";
+        NereidsParser parser = new NereidsParser();
+        LogicalPlan plan = parser.parseSingle(sql);
+        SetOptionsCommand command = (SetOptionsCommand) plan;
+        SetPassVarOp setPassOp = (SetPassVarOp) command.getSetVarOps().get(0);
+
+        UserIdentity userIdent = setPassOp.getUserIdent();
+        userIdent.analyze(); // Need to analyze before calling 
getQualifiedUser()
+        Assertions.assertEquals("t1", userIdent.getQualifiedUser());
+        Assertions.assertEquals("%", userIdent.getHost());
+        Assertions.assertFalse(userIdent.isDomain(),
+                "User 't1'@'%' should NOT be a domain user (isDomain should be 
false)");
+    }
+
+    /**
+     * Test SET PASSWORD FOR 'user'@'192.168.1.1' - single quotes, non-domain 
user.
+     */
+    @Test
+    public void testSetPasswordWithSingleQuotes() throws AnalysisException {
+        String sql = "SET PASSWORD FOR 'testuser'@'192.168.1.1' = 
PASSWORD('pass')";
+        NereidsParser parser = new NereidsParser();
+        LogicalPlan plan = parser.parseSingle(sql);
+        SetOptionsCommand command = (SetOptionsCommand) plan;
+        SetPassVarOp setPassOp = (SetPassVarOp) command.getSetVarOps().get(0);
+
+        UserIdentity userIdent = setPassOp.getUserIdent();
+        userIdent.analyze();
+        Assertions.assertEquals("testuser", userIdent.getQualifiedUser());
+        Assertions.assertEquals("192.168.1.1", userIdent.getHost());
+        Assertions.assertFalse(userIdent.isDomain(),
+                "User with single quotes should NOT be a domain user");
+    }
+
+    /**
+     * Test SET PASSWORD FOR 'user'@("domain") - SHOULD be a domain user.
+     * Domain users are identified by parentheses around host.
+     */
+    @Test
+    public void testSetPasswordDomainUser() throws AnalysisException {
+        String sql = "SET PASSWORD FOR 'domainuser'@(\"example.com\") = 
PASSWORD('pass')";
+        NereidsParser parser = new NereidsParser();
+        LogicalPlan plan = parser.parseSingle(sql);
+        SetOptionsCommand command = (SetOptionsCommand) plan;
+        SetPassVarOp setPassOp = (SetPassVarOp) command.getSetVarOps().get(0);
+
+        UserIdentity userIdent = setPassOp.getUserIdent();
+        userIdent.analyze();
+        Assertions.assertEquals("domainuser", userIdent.getQualifiedUser());
+        Assertions.assertEquals("example.com", userIdent.getHost());
+        Assertions.assertTrue(userIdent.isDomain(),
+                "User with parentheses SHOULD be a domain user (isDomain 
should be true)");
+    }
+
+    /**
+     * Test SET PASSWORD without FOR clause - should use current user.
+     */
+    @Test
+    public void testSetPasswordNoForClause() {
+        String sql = "SET PASSWORD = PASSWORD('newpass')";
+        NereidsParser parser = new NereidsParser();
+        LogicalPlan plan = parser.parseSingle(sql);
+        SetOptionsCommand command = (SetOptionsCommand) plan;
+        SetPassVarOp setPassOp = (SetPassVarOp) command.getSetVarOps().get(0);
+
+        // Without FOR clause, userIdent should be null (will be set to 
current user later)
+        Assertions.assertNull(setPassOp.getUserIdent(),
+                "SET PASSWORD without FOR clause should have null userIdent");
+    }
+
+    /**
+     * Test SET PASSWORD FOR 'user'@'%%' - verify isDomain is false even with 
%%.
+     */
+    @Test
+    public void testSetPasswordDoublePercent() throws AnalysisException {
+        String sql = "SET PASSWORD FOR 'testuser'@'%%' = PASSWORD('pass')";
+        NereidsParser parser = new NereidsParser();
+        LogicalPlan plan = parser.parseSingle(sql);
+        SetOptionsCommand command = (SetOptionsCommand) plan;
+        SetPassVarOp setPassOp = (SetPassVarOp) command.getSetVarOps().get(0);
+
+        UserIdentity userIdent = setPassOp.getUserIdent();
+        userIdent.analyze();
+        Assertions.assertEquals("testuser", userIdent.getQualifiedUser());
+        Assertions.assertEquals("%%", userIdent.getHost());
+        Assertions.assertFalse(userIdent.isDomain(),
+                "User 'testuser'@'%%' with single quotes should NOT be a 
domain user");
+    }
+}
diff --git a/regression-test/suites/account_p0/test_set_password.groovy 
b/regression-test/suites/account_p0/test_set_password.groovy
new file mode 100644
index 00000000000..65f08d5f40e
--- /dev/null
+++ b/regression-test/suites/account_p0/test_set_password.groovy
@@ -0,0 +1,69 @@
+// 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_set_password", "account") {
+    def user = "test_set_password_user"
+    def password1 = "Password_123"
+    def password2 = "Password_456"
+
+    // cleanup
+    try_sql "DROP USER IF EXISTS ${user}"
+
+    // create user with initial password
+    sql "CREATE USER '${user}'@'%' IDENTIFIED BY '${password1}'"
+
+    // grant cluster usage for 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 
'${user}'@'%'"""
+    }
+
+    // verify login with initial password
+    def tokens = context.config.jdbcUrl.split('/')
+    def url = tokens[0] + "//" + tokens[2] + "/" + "information_schema" + "?"
+
+    connect(user, password1, url) {
+        def result = sql "SELECT 1"
+        assertEquals(1, result[0][0])
+    }
+
+    // test SET PASSWORD FOR 'user'@'%' - this was the bug scenario
+    // Previously isDomain was incorrectly set to true for 'user'@'%' format
+    sql "SET PASSWORD FOR '${user}'@'%' = PASSWORD('${password2}')"
+
+    // verify login with new password
+    connect(user, password2, url) {
+        def result = sql "SELECT 1"
+        assertEquals(1, result[0][0])
+    }
+
+    // verify old password no longer works
+    try {
+        connect(user, password1, url) {
+            sql "SELECT 1"
+        }
+        assertTrue(false, "Old password should not work after SET PASSWORD")
+    } catch (Exception e) {
+        logger.info("Expected error with old password: " + e.getMessage())
+        assertTrue(e.getMessage().contains("Access denied") || 
e.getMessage().contains("authentication failed"))
+    }
+
+    // cleanup
+    sql "DROP USER IF EXISTS '${user}'@'%'"
+}


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

Reply via email to