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]