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]