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

morningman 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 4291de9640e [Enhancement](auth) Improve password validation to align 
with MySQL STRONG policy (#60188)
4291de9640e is described below

commit 4291de9640e65f94ae91c2f4c1837ff3230749a0
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Wed Jan 28 15:30:53 2026 +0800

    [Enhancement](auth) Improve password validation to align with MySQL STRONG 
policy (#60188)
    
    ### What problem does this PR solve?
    
    Enhance the validatePlainPassword function in MysqlPassword.java to
    fully comply with MySQL's STRONG password validation policy.
    
    Changes:
    1. Require all 4 character types (digit, lowercase, uppercase, special
    character) instead of the previous "3 out of 4" requirement.
    
    2. Add dictionary word check to reject passwords containing common weak
    words.
    - Built-in dictionary includes common words like: password, admin, test,
    root, etc.
    - Support loading custom dictionary from external file via the new
    global variable `validate_password_dictionary_file`.
    
    3. Implement lazy loading for external dictionary file:
       - Dictionary is loaded on first password validation call.
       - Automatically reloads when the file path is changed.
       - Falls back to built-in dictionary if file loading fails.
    
    4. Improve error messages to clearly indicate which requirements are
    missing.
    
    5. Add comprehensive unit tests for all validation scenarios.
    
    Change the password dictionary file path resolution to use
    `Config.security_plugins_dir`
    as the base directory prefix. New
    `GlobalVariable.validatePasswordDictionaryFile` only
    needs to specify the filename, and the full path will be constructed as:
    `${security_plugins_dir}/<filename>`
---
 .../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 46da88b4dde..8c5e996a8b3 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
@@ -3428,17 +3428,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;
 
@@ -3661,6 +3661,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