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 f93a50ebb5b branch-4.0: [Enhancement](auth) Improve password 
validation to align with MySQL STRONG policy #60188 (#60299)
f93a50ebb5b is described below

commit f93a50ebb5bed84c56b08e6cc2ca91cd86c81d20
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Jan 30 16:15:07 2026 +0800

    branch-4.0: [Enhancement](auth) Improve password validation to align with 
MySQL STRONG policy #60188 (#60299)
    
    Cherry-picked from #60188
    
    Co-authored-by: Mingyu Chen (Rayner) <[email protected]>
---
 .../main/java/org/apache/doris/common/Config.java  |  14 +-
 .../java/org/apache/doris/mysql/MysqlPassword.java | 163 +++++++++-
 .../java/org/apache/doris/qe/GlobalVariable.java   |  21 +-
 .../org/apache/doris/mysql/MysqlPasswordTest.java  | 337 +++++++++++++++++++++
 .../suites/account_p0/test_alter_user.groovy       |   4 +-
 5 files changed, 515 insertions(+), 24 deletions(-)

diff --git a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java 
b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
index 1acbf5d5fab..424b03905c1 100644
--- a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
+++ b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
@@ -3393,17 +3393,17 @@ public class Config extends ConfigBase {
             options = {"without_warmup", "async_warmup", "sync_warmup", 
"peer_read_async_warmup"})
     public static String cloud_warm_up_for_rebalance_type = "async_warmup";
 
-    @ConfField(mutable = true, masterOnly = true, description = {"云上tablet均衡时,"
-            + "同一个host内预热批次的最大tablet个数,默认10", "The max number of tablets per 
host "
+    @ConfField(mutable = true, masterOnly = true, description = {"云上 tablet 
均衡时,"
+            + "同一个 host 内预热批次的最大 tablet 个数,默认 10", "The max number of tablets 
per host "
             + "when batching warm-up requests during cloud tablet rebalancing, 
default 10"})
     public static int cloud_warm_up_batch_size = 10;
 
-    @ConfField(mutable = true, masterOnly = true, description = {"云上tablet均衡时,"
-            + "预热批次最长等待时间,单位毫秒,默认50ms", "Maximum wait time in milliseconds 
before a "
+    @ConfField(mutable = true, masterOnly = true, description = {"云上 tablet 
均衡时,"
+            + "预热批次最长等待时间,单位毫秒,默认 50ms", "Maximum wait time in milliseconds 
before a "
             + "pending warm-up batch is flushed, default 50ms"})
     public static int cloud_warm_up_batch_flush_interval_ms = 50;
 
-    @ConfField(mutable = true, masterOnly = true, description = 
{"云上tablet均衡预热rpc异步线程池大小,默认4",
+    @ConfField(mutable = true, masterOnly = true, description = {"云上 tablet 
均衡预热 rpc 异步线程池大小,默认 4",
         "Thread pool size for asynchronous warm-up RPC dispatch during cloud 
tablet rebalancing, default 4"})
     public static int cloud_warm_up_rpc_async_pool_size = 4;
 
@@ -3626,6 +3626,10 @@ public class Config extends ConfigBase {
             "Authorization plugin directory"})
     public static String authorization_plugins_dir = EnvUtils.getDorisHome() + 
"/plugins/authorization";
 
+    @ConfField(description = {"安全相关插件目录",
+            "Security plugin directory"})
+    public static String security_plugins_dir = EnvUtils.getDorisHome() + 
"/plugins/security";
+
     @ConfField(description = {
             "鉴权插件配置文件路径,需在 DORIS_HOME 下,默认为 conf/authorization.conf",
             "Authorization plugin configuration file path, need to be in 
DORIS_HOME,"
diff --git a/fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlPassword.java 
b/fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlPassword.java
index 51a4b544305..14b07f1a531 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlPassword.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/MysqlPassword.java
@@ -18,18 +18,25 @@
 package org.apache.doris.mysql;
 
 import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.Config;
 import org.apache.doris.common.ErrorCode;
 import org.apache.doris.common.ErrorReport;
 import org.apache.doris.qe.GlobalVariable;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.nio.file.Paths;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
+import java.util.HashSet;
 import java.util.Random;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -88,10 +95,97 @@ public class MysqlPassword {
     private static final Set<Character> complexCharSet;
     public static final int MIN_PASSWORD_LEN = 8;
 
+    /**
+     * Built-in dictionary of common weak password words.
+     * Used as fallback when no external dictionary file is configured.
+     * Password containing any of these words (case-insensitive) will be 
rejected under STRONG policy.
+     */
+    private static final Set<String> BUILTIN_DICTIONARY_WORDS = 
ImmutableSet.of(
+            // Common password words
+            "password", "passwd", "pass", "pwd", "secret",
+            // User/role related
+            "admin", "administrator", "root", "user", "guest", "login", 
"master", "super",
+            // Test/demo related
+            "test", "testing", "demo", "sample", "example", "temp", 
"temporary",
+            // System/database related
+            "system", "server", "database", "mysql", "doris", "oracle", 
"postgres",
+            // Common weak patterns
+            "qwerty", "abc", "letmein", "welcome", "hello", "monkey", 
"dragon", "iloveyou",
+            "trustno", "sunshine", "princess", "football", "baseball", "soccer"
+    );
+
+    // Lazy-loaded dictionary from external file
+    private static volatile Set<String> loadedDictionaryWords = null;
+    // The file path that was used to load the dictionary (for detecting 
changes)
+    private static volatile String loadedDictionaryFilePath = null;
+    // Lock object for thread-safe lazy loading
+    private static final Object DICTIONARY_LOAD_LOCK = new Object();
+
     static {
         complexCharSet = "~!@#$%^&*()_+|<>,.?/:;'[]{}".chars().mapToObj(c -> 
(char) c).collect(Collectors.toSet());
     }
 
+    /**
+     * Get the dictionary words to use for password validation.
+     * If an external dictionary file is configured, load it lazily.
+     * Otherwise, use the built-in dictionary.
+     *
+     * @return the set of dictionary words (all in lowercase)
+     */
+    private static Set<String> getDictionaryWords() {
+        String configuredFileName = 
GlobalVariable.validatePasswordDictionaryFile;
+
+        // If no file is configured, use built-in dictionary
+        if (Strings.isNullOrEmpty(configuredFileName)) {
+            return BUILTIN_DICTIONARY_WORDS;
+        }
+
+        // Construct full path: security_plugins_dir/<configured_file_name> 
and normalize for safe comparison
+        String configuredFilePath = Paths.get(Config.security_plugins_dir, 
configuredFileName)
+                .normalize().toString();
+
+        // Check if we need to (re)load the dictionary
+        // Double-checked locking for thread safety
+        if (loadedDictionaryWords == null || 
!configuredFilePath.equals(loadedDictionaryFilePath)) {
+            synchronized (DICTIONARY_LOAD_LOCK) {
+                if (loadedDictionaryWords == null || 
!configuredFilePath.equals(loadedDictionaryFilePath)) {
+                    loadedDictionaryWords = 
loadDictionaryFromFile(configuredFilePath);
+                    loadedDictionaryFilePath = configuredFilePath;
+                }
+            }
+        }
+
+        return loadedDictionaryWords != null ? loadedDictionaryWords : 
BUILTIN_DICTIONARY_WORDS;
+    }
+
+    /**
+     * Load dictionary words from an external file.
+     * Each line in the file is treated as one dictionary word.
+     * Empty lines and lines starting with '#' are ignored.
+     *
+     * @param filePath path to the dictionary file
+     * @return set of dictionary words (all converted to lowercase), or null 
if loading fails
+     */
+    private static Set<String> loadDictionaryFromFile(String filePath) {
+        Set<String> words = new HashSet<>();
+        try (BufferedReader reader = new BufferedReader(new 
FileReader(filePath))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                line = line.trim();
+                // Skip empty lines and comments
+                if (!line.isEmpty() && !line.startsWith("#")) {
+                    words.add(line.toLowerCase());
+                }
+            }
+            LOG.info("Loaded {} words from password dictionary file: {}", 
words.size(), filePath);
+            return words;
+        } catch (IOException e) {
+            LOG.warn("Failed to load password dictionary file: {}. Using 
built-in dictionary. Error: {}",
+                    filePath, e.getMessage());
+            return null;
+        }
+    }
+
     public static byte[] createRandomString(int len) {
         byte[] bytes = new byte[len];
         random.nextBytes(bytes);
@@ -289,31 +383,74 @@ public class MysqlPassword {
         return passwd;
     }
 
+    /**
+     * Validate plain text password according to MySQL's validate_password 
policy.
+     * For STRONG policy, the password must meet all of the following 
requirements:
+     * 1. At least MIN_PASSWORD_LEN (8) characters long
+     * 2. Contains at least 1 digit
+     * 3. Contains at least 1 lowercase letter
+     * 4. Contains at least 1 uppercase letter
+     * 5. Contains at least 1 special character
+     * 6. Does not contain any dictionary words (case-insensitive)
+     */
     public static void validatePlainPassword(long validaPolicy, String text) 
throws AnalysisException {
         if (validaPolicy == GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG) {
             if (Strings.isNullOrEmpty(text) || text.length() < 
MIN_PASSWORD_LEN) {
                 throw new AnalysisException(
-                        "Violate password validation policy: STRONG. The 
password must be at least 8 characters");
+                        "Violate password validation policy: STRONG. "
+                                + "The password must be at least " + 
MIN_PASSWORD_LEN + " characters.");
             }
 
-            int i = 0;
-            if (text.chars().anyMatch(Character::isDigit)) {
-                i++;
+            StringBuilder missingTypes = new StringBuilder();
+
+            if (text.chars().noneMatch(Character::isDigit)) {
+                missingTypes.append("numeric, ");
             }
-            if (text.chars().anyMatch(Character::isLowerCase)) {
-                i++;
+            if (text.chars().noneMatch(Character::isLowerCase)) {
+                missingTypes.append("lowercase, ");
             }
-            if (text.chars().anyMatch(Character::isUpperCase)) {
-                i++;
+            if (text.chars().noneMatch(Character::isUpperCase)) {
+                missingTypes.append("uppercase, ");
             }
-            if (text.chars().anyMatch(c -> complexCharSet.contains((char) c))) 
{
-                i++;
+            if (text.chars().noneMatch(c -> complexCharSet.contains((char) 
c))) {
+                missingTypes.append("special character, ");
             }
-            if (i < 3) {
+
+            if (missingTypes.length() > 0) {
+                // Remove trailing ", "
+                missingTypes.setLength(missingTypes.length() - 2);
                 throw new AnalysisException(
-                        "Violate password validation policy: STRONG. The 
password must contain at least 3 types of "
-                                + "numbers, uppercase letters, lowercase 
letters and special characters.");
+                        "Violate password validation policy: STRONG. "
+                                + "The password must contain at least one 
character from each of the following types: "
+                                + "numeric, lowercase, uppercase, and special 
characters. "
+                                + "Missing: " + missingTypes + ".");
+            }
+
+            // Check for dictionary words (case-insensitive)
+            String foundWord = containsDictionaryWord(text);
+            if (foundWord != null) {
+                throw new AnalysisException(
+                        "Violate password validation policy: STRONG. "
+                                + "The password contains a common dictionary 
word '" + foundWord + "', "
+                                + "which makes it easy to guess. Please choose 
a more secure password.");
+            }
+        }
+    }
+
+    /**
+     * Check if the password contains any dictionary word (case-insensitive).
+     * Uses either the external dictionary file (if configured) or the 
built-in dictionary.
+     *
+     * @param password the password to check
+     * @return the found dictionary word, or null if none found
+     */
+    private static String containsDictionaryWord(String password) {
+        String lowerPassword = password.toLowerCase();
+        for (String word : getDictionaryWords()) {
+            if (lowerPassword.contains(word)) {
+                return word;
             }
         }
+        return null;
     }
 }
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/GlobalVariable.java 
b/fe/fe-core/src/main/java/org/apache/doris/qe/GlobalVariable.java
index ff2b423b8a1..d304c78ded4 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/GlobalVariable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/GlobalVariable.java
@@ -60,6 +60,8 @@ public final class GlobalVariable {
     public static final long VALIDATE_PASSWORD_POLICY_DISABLED = 0;
     public static final long VALIDATE_PASSWORD_POLICY_STRONG = 2;
 
+    public static final String VALIDATE_PASSWORD_DICTIONARY_FILE = 
"validate_password_dictionary_file";
+
     public static final String SQL_CONVERTER_SERVICE_URL = 
"sql_converter_service_url";
     public static final String ENABLE_AUDIT_PLUGIN = "enable_audit_plugin";
     public static final String AUDIT_PLUGIN_MAX_BATCH_BYTES = 
"audit_plugin_max_batch_bytes";
@@ -139,6 +141,17 @@ public final class GlobalVariable {
     @VariableMgr.VarAttr(name = VALIDATE_PASSWORD_POLICY, flag = 
VariableMgr.GLOBAL)
     public static long validatePasswordPolicy = 0;
 
+    @VariableMgr.VarAttr(name = VALIDATE_PASSWORD_DICTIONARY_FILE, flag = 
VariableMgr.GLOBAL,
+            description = {"密码验证字典文件路径。文件为纯文本格式,每行一个词。"
+                    + "当 validate_password_policy 为 STRONG(2) 
时,密码中不能包含字典中的任何词(不区分大小写)。"
+                    + "如果为空,则使用内置字典。",
+                    "Path to the password validation dictionary file. "
+                            + "The file should be plain text with one word per 
line. "
+                            + "When validate_password_policy is STRONG(2), "
+                            + "the password cannot contain any word from the 
dictionary "
+                            + "(case-insensitive). If empty, a built-in 
dictionary will be used."})
+    public static volatile String validatePasswordDictionaryFile = "";
+
     // If set to true, the db name of TABLE_SCHEMA column in tables in 
information_schema
     // database will be shown as `ctl.db`. Otherwise, show only `db`.
     // This is used to compatible with some MySQL tools.
@@ -184,12 +197,12 @@ public final class GlobalVariable {
     public static boolean enable_get_row_count_from_file_list = true;
 
     @VariableMgr.VarAttr(name = READ_ONLY, flag = VariableMgr.GLOBAL,
-            description = {"仅用于兼容MySQL生态,暂无实际意义",
+            description = {"仅用于兼容 MySQL 生态,暂无实际意义",
                     "Only for compatibility with MySQL ecosystem, no practical 
meaning"})
     public static boolean read_only = true;
 
     @VariableMgr.VarAttr(name = SUPER_READ_ONLY, flag = VariableMgr.GLOBAL,
-            description = {"仅用于兼容MySQL生态,暂无实际意义",
+            description = {"仅用于兼容 MySQL 生态,暂无实际意义",
                     "Only for compatibility with MySQL ecosystem, no practical 
meaning"})
     public static boolean super_read_only = true;
 
@@ -207,7 +220,7 @@ public final class GlobalVariable {
 
     @VariableMgr.VarAttr(name = ENABLE_FETCH_ICEBERG_STATS, flag = 
VariableMgr.GLOBAL,
             description = {
-                "当HMS catalog中的Iceberg表没有统计信息时,是否通过Iceberg Api获取统计信息",
+                "当 HMS catalog 中的 Iceberg 表没有统计信息时,是否通过 Iceberg Api 获取统计信息",
                 "Enable fetch stats for HMS Iceberg table when it's not 
analyzed."})
     public static boolean enableFetchIcebergStats = false;
 
@@ -228,7 +241,7 @@ public final class GlobalVariable {
                     "控制隐式类型转换的行为,当设置为 true 时,使用新的行为。新行为更为合理。类型优先级从高到低为时间相关类型 > 
"
                             + "数值类型 > 复杂类型 / JSON 类型 / IP 类型 > 字符串类型 > VARIANT 
类型。当两个或多个不同类型的表达式"
                             + "进行比较时,强制类型转换优先向高优先级类型转换。转换时尽可能保留精度,如:"
-                            + "当转换为时间相关类型时,当无法确定精度时,优先使用6位精度的 DATETIME 类型。"
+                            + "当转换为时间相关类型时,当无法确定精度时,优先使用 6 位精度的 DATETIME 类型。"
                             + "当转换为数值类型时,当无法确定精度时,优先使用 DECIMAL 类型。",
                     "Controls the behavior of implicit type conversion. When 
set to true, the new behavior is used,"
                             + " which is more reasonable. The type priority, 
from highest to lowest, is: time-related"
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlPasswordTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlPasswordTest.java
index 4cc76e146f9..f14bbf01763 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlPasswordTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/mysql/MysqlPasswordTest.java
@@ -18,13 +18,43 @@
 package org.apache.doris.mysql;
 
 import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.Config;
+import org.apache.doris.qe.GlobalVariable;
 
+import org.junit.After;
 import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 
 public class MysqlPasswordTest {
+
+    @Rule
+    public TemporaryFolder tempFolder = new TemporaryFolder();
+
+    private String originalDictionaryFile;
+    private String originalSecurityPluginsDir;
+
+    @Before
+    public void setUp() {
+        // Save original values
+        originalDictionaryFile = GlobalVariable.validatePasswordDictionaryFile;
+        originalSecurityPluginsDir = Config.security_plugins_dir;
+    }
+
+    @After
+    public void tearDown() {
+        // Restore original values
+        GlobalVariable.validatePasswordDictionaryFile = originalDictionaryFile;
+        Config.security_plugins_dir = originalSecurityPluginsDir;
+    }
+
     @Test
     public void testMakePassword() {
         Assert.assertEquals("*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4",
@@ -79,4 +109,311 @@ public class MysqlPasswordTest {
         Assert.fail("No exception throws");
     }
 
+    // ==================== validatePlainPassword Tests ====================
+
+    @Test
+    public void testValidatePasswordDisabledPolicy() throws AnalysisException {
+        // When policy is DISABLED, any password should pass
+        GlobalVariable.validatePasswordDictionaryFile = "";
+        
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_DISABLED,
 "weak");
+        
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_DISABLED,
 "");
+        
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_DISABLED,
 null);
+        
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_DISABLED,
 "test123");
+    }
+
+    @Test
+    public void testValidatePasswordStrongPolicyValid() throws 
AnalysisException {
+        // Valid password: 8+ chars, has digit, lowercase, uppercase, special 
char, no dictionary word
+        GlobalVariable.validatePasswordDictionaryFile = "";
+        
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "Xk9$mN2@pL");
+        
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "MyP@ss1!");
+        
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "Str0ng!Powd");
+    }
+
+    @Test
+    public void testValidatePasswordTooShort() {
+        GlobalVariable.validatePasswordDictionaryFile = "";
+        try {
+            
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "Aa1!abc");
+            Assert.fail("Expected AnalysisException for password too short");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("at least 8 
characters"));
+        }
+    }
+
+    @Test
+    public void testValidatePasswordNullOrEmpty() {
+        GlobalVariable.validatePasswordDictionaryFile = "";
+        try {
+            
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 null);
+            Assert.fail("Expected AnalysisException for null password");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("at least 8 
characters"));
+        }
+
+        try {
+            
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "");
+            Assert.fail("Expected AnalysisException for empty password");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("at least 8 
characters"));
+        }
+    }
+
+    @Test
+    public void testValidatePasswordMissingDigit() {
+        GlobalVariable.validatePasswordDictionaryFile = "";
+        try {
+            
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "Abcdefgh!");
+            Assert.fail("Expected AnalysisException for missing digit");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("Missing: numeric"));
+        }
+    }
+
+    @Test
+    public void testValidatePasswordMissingLowercase() {
+        GlobalVariable.validatePasswordDictionaryFile = "";
+        try {
+            
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "ABCDEFG1!");
+            Assert.fail("Expected AnalysisException for missing lowercase");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("Missing: lowercase"));
+        }
+    }
+
+    @Test
+    public void testValidatePasswordMissingUppercase() {
+        GlobalVariable.validatePasswordDictionaryFile = "";
+        try {
+            
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "abcdefg1!");
+            Assert.fail("Expected AnalysisException for missing uppercase");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("Missing: uppercase"));
+        }
+    }
+
+    @Test
+    public void testValidatePasswordMissingSpecialChar() {
+        GlobalVariable.validatePasswordDictionaryFile = "";
+        try {
+            
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "Abcdefg12");
+            Assert.fail("Expected AnalysisException for missing special 
character");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("Missing: special 
character"));
+        }
+    }
+
+    @Test
+    public void testValidatePasswordMissingMultipleTypes() {
+        GlobalVariable.validatePasswordDictionaryFile = "";
+        try {
+            // Missing digit, uppercase, special char
+            
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "abcdefghij");
+            Assert.fail("Expected AnalysisException for missing multiple 
types");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("numeric"));
+            Assert.assertTrue(e.getMessage().contains("uppercase"));
+            Assert.assertTrue(e.getMessage().contains("special character"));
+        }
+    }
+
+    @Test
+    public void testValidatePasswordBuiltinDictionaryWord() {
+        GlobalVariable.validatePasswordDictionaryFile = "";
+        // Test various built-in dictionary words
+        String[] dictionaryPasswords = {
+                "Test@123X",      // contains "test"
+                "Admin@123X",     // contains "admin"
+                "Password1!",     // contains "password"
+                "Root@1234X",     // contains "root"
+                "User@1234X",     // contains "user"
+                "Doris@123X",     // contains "doris"
+                "Qwerty@12X",     // contains "qwerty"
+                "Welcome1!X",     // contains "welcome"
+                "Hello@123X",     // contains "hello"
+        };
+
+        for (String password : dictionaryPasswords) {
+            try {
+                
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 password);
+                Assert.fail("Expected AnalysisException for dictionary word 
in: " + password);
+            } catch (AnalysisException e) {
+                Assert.assertTrue("Expected dictionary word error for: " + 
password,
+                        e.getMessage().contains("dictionary word"));
+            }
+        }
+    }
+
+    @Test
+    public void testValidatePasswordDictionaryWordCaseInsensitive() {
+        GlobalVariable.validatePasswordDictionaryFile = "";
+        // Dictionary check should be case-insensitive
+        String[] caseVariants = {
+                "TEST@123Xy",
+                "TeSt@123Xy",
+                "tEsT@123Xy",
+                "ADMIN@12Xy",
+                "AdMiN@12Xy",
+        };
+
+        for (String password : caseVariants) {
+            try {
+                
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 password);
+                Assert.fail("Expected AnalysisException for case-insensitive 
dictionary word in: " + password);
+            } catch (AnalysisException e) {
+                Assert.assertTrue(e.getMessage().contains("dictionary word"));
+            }
+        }
+    }
+
+    @Test
+    public void testValidatePasswordWithExternalDictionary() throws 
IOException, AnalysisException {
+        // Set security_plugins_dir to temp folder
+        Config.security_plugins_dir = tempFolder.getRoot().getAbsolutePath();
+
+        // Create a temporary dictionary file in the security_plugins_dir
+        File dictFile = tempFolder.newFile("test_dictionary.txt");
+        try (FileWriter writer = new FileWriter(dictFile)) {
+            writer.write("# This is a comment\n");
+            writer.write("customword\n");
+            writer.write("  secretkey  \n");  // with spaces
+            writer.write("\n");  // empty line
+            writer.write("forbidden\n");
+        }
+
+        // Use just the filename (not full path)
+        GlobalVariable.validatePasswordDictionaryFile = "test_dictionary.txt";
+
+        // Password containing custom dictionary word should fail
+        try {
+            
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "Customword1!");
+            Assert.fail("Expected AnalysisException for custom dictionary 
word");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("customword"));
+        }
+
+        try {
+            
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "Secretkey1!");
+            Assert.fail("Expected AnalysisException for custom dictionary 
word");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("secretkey"));
+        }
+
+        try {
+            
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "Forbidden1!");
+            Assert.fail("Expected AnalysisException for custom dictionary 
word");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("forbidden"));
+        }
+
+        // Password not containing custom dictionary words should pass
+        // Note: built-in words like "test" should NOT fail because we're 
using external dictionary
+        
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "Xk9$mN2@pL");
+    }
+
+    @Test
+    public void testValidatePasswordDictionaryFileNotFound() throws 
AnalysisException {
+        // Set security_plugins_dir to a valid path
+        Config.security_plugins_dir = tempFolder.getRoot().getAbsolutePath();
+
+        // When dictionary file doesn't exist, should fall back to built-in 
dictionary
+        GlobalVariable.validatePasswordDictionaryFile = 
"non_existent_dictionary.txt";
+
+        // Built-in dictionary word should still fail
+        try {
+            
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "Test@123Xy");
+            Assert.fail("Expected AnalysisException for built-in dictionary 
word");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("dictionary word"));
+        }
+
+        // Valid password should pass
+        
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "Xk9$mN2@pL");
+    }
+
+    @Test
+    public void testValidatePasswordDictionaryFileReload() throws IOException, 
AnalysisException {
+        // Set security_plugins_dir to temp folder
+        Config.security_plugins_dir = tempFolder.getRoot().getAbsolutePath();
+
+        // Create first dictionary file
+        File dictFile1 = tempFolder.newFile("dict1.txt");
+        try (FileWriter writer = new FileWriter(dictFile1)) {
+            writer.write("wordone\n");
+        }
+
+        // Use just the filename
+        GlobalVariable.validatePasswordDictionaryFile = "dict1.txt";
+
+        // Should fail for wordone
+        try {
+            
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "Wordone12!");
+            Assert.fail("Expected AnalysisException for wordone");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("wordone"));
+        }
+
+        // Create second dictionary file with different content
+        File dictFile2 = tempFolder.newFile("dict2.txt");
+        try (FileWriter writer = new FileWriter(dictFile2)) {
+            writer.write("wordtwo\n");
+        }
+
+        // Change to second dictionary file (just filename)
+        GlobalVariable.validatePasswordDictionaryFile = "dict2.txt";
+
+        // Should now pass for wordone (not in new dictionary)
+        
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "Wordone12!");
+
+        // Should fail for wordtwo
+        try {
+            
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "Wordtwo12!");
+            Assert.fail("Expected AnalysisException for wordtwo");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("wordtwo"));
+        }
+    }
+
+    @Test
+    public void testValidatePasswordEmptyDictionaryFile() throws IOException, 
AnalysisException {
+        // Set security_plugins_dir to temp folder
+        Config.security_plugins_dir = tempFolder.getRoot().getAbsolutePath();
+
+        // Use just the filename
+        GlobalVariable.validatePasswordDictionaryFile = "empty_dict.txt";
+
+        // With empty dictionary, only character requirements should be checked
+        try {
+            
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "Test@123X");
+            Assert.fail("Expected AnalysisException for test");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("test"));
+        }
+        try {
+            
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "Admin@12X");
+            Assert.fail("Expected AnalysisException for admin");
+        } catch (AnalysisException e) {
+            Assert.assertTrue(e.getMessage().contains("admin"));
+        }
+    }
+
+    @Test
+    public void testValidatePasswordDictionaryWithCommentsOnly() throws 
IOException, AnalysisException {
+        // Set security_plugins_dir to temp folder
+        Config.security_plugins_dir = tempFolder.getRoot().getAbsolutePath();
+
+        // Create a dictionary file with only comments
+        File dictFile = tempFolder.newFile("comments_dict.txt");
+        try (FileWriter writer = new FileWriter(dictFile)) {
+            writer.write("# comment 1\n");
+            writer.write("# comment 2\n");
+            writer.write("   # comment with leading spaces\n");
+        }
+
+        // Use just the filename
+        GlobalVariable.validatePasswordDictionaryFile = "comments_dict.txt";
+
+        // Should pass since dictionary effectively has no words
+        
MysqlPassword.validatePlainPassword(GlobalVariable.VALIDATE_PASSWORD_POLICY_STRONG,
 "Test@123X");
+    }
 }
diff --git a/regression-test/suites/account_p0/test_alter_user.groovy 
b/regression-test/suites/account_p0/test_alter_user.groovy
index fc4b8a12bf2..c0d0a26fb60 100644
--- a/regression-test/suites/account_p0/test_alter_user.groovy
+++ b/regression-test/suites/account_p0/test_alter_user.groovy
@@ -126,11 +126,11 @@ suite("test_alter_user", "account,nonConcurrent") {
     sql """set global validate_password_policy=STRONG"""
     test {
         sql """set password for 'test_auth_user3' = password("12345")"""
-        exception "Violate password validation policy: STRONG. The password 
must be at least 8 characters";
+        exception "Violate password validation policy: STRONG"
     }
     test {
         sql """set password for 'test_auth_user3' = password("12345678")"""
-        exception "Violate password validation policy: STRONG. The password 
must contain at least 3 types of numbers, uppercase letters, lowercase letters 
and special characters.";
+        exception "Violate password validation policy: STRONG"
     }
 
     sql """set password for 'test_auth_user3' = password('Ab1234567^')"""


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


Reply via email to