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

yiguolei pushed a commit to branch branch-4.0
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.0 by this push:
     new fb93bd37daf branch-4.0: [fix](nereids) Fix incorrect isDomain parsing 
in SET PASSWORD statement #60565 (#60657)
fb93bd37daf is described below

commit fb93bd37daf6cce59fcd48143a070cfe117023e1
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Feb 12 23:36:48 2026 +0800

    branch-4.0: [fix](nereids) Fix incorrect isDomain parsing in SET PASSWORD 
statement #60565 (#60657)
    
    Cherry-picked from #60565
    
    Co-authored-by: bobhan1 <[email protected]>
---
 .../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 055482cadbd..a2b5053785a 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
@@ -5355,16 +5355,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