fix(mysql): support cleartext password auth option (#2499)

This commit is contained in:
LRcoding 2026-07-03 13:47:47 +08:00 committed by GitHub
parent cabe89a19b
commit 21cec24e75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 187 additions and 1 deletions

View File

@ -31,6 +31,7 @@ import { firstZooKeeperEndpoint, normalizeZooKeeperConnectString } from "@/lib/z
import { isLocalFileTypeDb } from "@/lib/connectionFile";
import { MQ_PINNED_VERSION_OPTIONS, pinnedVersionToSelection, selectionToPinnedVersion } from "@/lib/mqPinnedVersionOptions";
import { mongodbAuthFailureHint, mongoUrlParam, setMongoUrlParam } from "@/lib/mongoConnectionOptions";
import { mysqlCleartextPasswordAuthEnabled, setMysqlCleartextPasswordAuthEnabled } from "@/lib/mysqlConnectionOptions";
import { copyToClipboard } from "@/lib/clipboard";
import { agentDriverInstallKey, appendAgentDriverUpdateHint, hasAgentDriverUpdate, showAgentDriverInstallHint, type AgentDriverInstallState } from "@/lib/agentDriverInstallHint";
import { prestoSqlBuiltinDriverPaths } from "@/lib/prestoSqlBuiltinDriver";
@ -1593,6 +1594,13 @@ const supportsCaCertificatePath = computed(() => form.value.db_type === "clickho
const supportsGenericUrlParams = computed(() => form.value.db_type !== "manticoresearch");
const bareMysqlProfiles = new Set(["doris", "selectdb", "oceanbase"]);
const supportsMysqlTlsOptions = computed(() => form.value.db_type === "starrocks" || (form.value.db_type === "mysql" && !bareMysqlProfiles.has(selectedType.value)));
const supportsMysqlCleartextPasswordAuth = computed(() => form.value.db_type === "mysql" && !bareMysqlProfiles.has(selectedType.value));
const mysqlCleartextPasswordAuth = computed({
get: () => mysqlCleartextPasswordAuthEnabled(form.value.url_params),
set: (value: boolean) => {
form.value.url_params = setMysqlCleartextPasswordAuthEnabled(form.value.url_params, value);
},
});
const mysqlTlsMode = computed({
get: () => mysqlTlsModeFromParams(form.value.url_params, form.value.ssl),
set: (value: string) => {
@ -4204,7 +4212,7 @@ function openExternalUrl(url: string) {
</TabsContent>
<TabsContent v-if="supportsTlsToggle" value="tls" class="m-0">
<div class="connection-form-body grid gap-4 py-4 pr-2 max-h-[65vh] overflow-y-auto">
<div class="connection-form-body grid gap-4 py-4 pr-2 max-h-[65vh] overflow-y-auto overflow-x-hidden">
<div v-if="!supportsPostgresTlsOptions && !supportsMysqlTlsOptions" class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">SSL/TLS</Label>
<label class="col-span-3 flex items-center gap-2 cursor-pointer">
@ -4284,6 +4292,16 @@ function openExternalUrl(url: string) {
</template>
<template v-if="supportsMysqlTlsOptions">
<div v-if="supportsMysqlCleartextPasswordAuth" class="grid grid-cols-4 items-start gap-4">
<Label :class="[connectionLabelSmallPaddedClass, 'min-w-0 break-words']">{{ t("connection.mysqlCleartextPasswordAuth") }}</Label>
<div class="col-span-3 flex min-w-0 items-start justify-between gap-4">
<p class="min-w-0 text-[11px] leading-4 text-muted-foreground break-words">
{{ t("connection.mysqlCleartextPasswordAuthHint") }}
</p>
<Switch v-model="mysqlCleartextPasswordAuth" class="mt-0.5 shrink-0" />
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.mysqlTlsMode") }}</Label>
<Select v-model="mysqlTlsMode">

View File

@ -203,6 +203,8 @@ export default {
mysqlTlsModeRequired: "Required",
mysqlTlsModeVerifyCa: "Verify CA",
mysqlTlsModeVerifyIdentity: "Verify Identity",
mysqlCleartextPasswordAuth: "Cleartext Password Auth",
mysqlCleartextPasswordAuthHint: "Enable only when the MySQL server requires the cleartext password authentication plugin.",
mysqlCaCertHint: "Required for Verify CA and Verify Identity when the server certificate uses a private CA.",
mysqlClientCert: "Client Auth",
mysqlClientCertPlaceholder: "/path/to/client.crt",

View File

@ -205,6 +205,8 @@ export default withEnglishFallback({
mysqlTlsModeRequired: "Requerido",
mysqlTlsModeVerifyCa: "Verificar CA",
mysqlTlsModeVerifyIdentity: "Verificar identidad",
mysqlCleartextPasswordAuth: "Autenticación de contraseña en claro",
mysqlCleartextPasswordAuthHint: "Actívala solo cuando el servidor MySQL requiera el plugin de autenticación de contraseña en claro.",
mysqlCaCertHint: "Requerido para Verificar CA e Identidad cuando el certificado usa una CA privada.",
mysqlClientCert: "Autenticación cliente",
mysqlClientCertPlaceholder: "/ruta/a/client.crt",

View File

@ -204,6 +204,8 @@ export default withEnglishFallback({
mysqlTlsModeRequired: "Richiesta",
mysqlTlsModeVerifyCa: "Verifica CA",
mysqlTlsModeVerifyIdentity: "Verifica Identità",
mysqlCleartextPasswordAuth: "Autenticazione password in chiaro",
mysqlCleartextPasswordAuthHint: "Attiva solo quando il server MySQL richiede il plugin di autenticazione password in chiaro.",
mysqlCaCertHint: "Richiesto per Verifica CA e Verifica Identità quando il certificato del server usa una CA privata.",
mysqlClientCert: "Autenticazione Client",
mysqlClientCertPlaceholder: "/percorso/per/client.crt",

View File

@ -204,6 +204,8 @@ export default withEnglishFallback({
mysqlTlsModeRequired: "必須",
mysqlTlsModeVerifyCa: "CA検証",
mysqlTlsModeVerifyIdentity: "ID検証",
mysqlCleartextPasswordAuth: "平文パスワード認証",
mysqlCleartextPasswordAuthHint: "MySQLサーバーがcleartext password認証プラグインを要求する場合のみ有効にしてください。",
mysqlCaCertHint: "サーバー証明書がプライベートCAで署名されている場合、CA検証とID検証に必要です。",
mysqlClientCert: "クライアント認証",
mysqlClientCertPlaceholder: "/path/to/client.crt",

View File

@ -205,6 +205,8 @@ export default withEnglishFallback({
mysqlTlsModeRequired: "Obrigatório",
mysqlTlsModeVerifyCa: "Verificar CA",
mysqlTlsModeVerifyIdentity: "Verificar Identidade",
mysqlCleartextPasswordAuth: "Autenticação de senha em texto claro",
mysqlCleartextPasswordAuthHint: "Ative somente quando o servidor MySQL exigir o plugin de autenticação de senha em texto claro.",
mysqlCaCertHint: "Necessário para Verificar CA e Verificar Identidade quando o certificado do servidor usa uma CA privada.",
mysqlClientCert: "Autenticação do Cliente",
mysqlClientCertPlaceholder: "/caminho/para/client.crt",

View File

@ -205,6 +205,8 @@ export default withEnglishFallback({
mysqlTlsModeRequired: "必须使用",
mysqlTlsModeVerifyCa: "验证 CA",
mysqlTlsModeVerifyIdentity: "验证身份",
mysqlCleartextPasswordAuth: "明文密码认证",
mysqlCleartextPasswordAuthHint: "仅在 MySQL 服务端要求明文密码认证插件时开启。",
mysqlCaCertHint: "服务端证书由私有 CA 签发时Verify CA / Verify Identity 模式需要填写。",
mysqlClientCert: "客户端认证",
mysqlClientCertPlaceholder: "/path/to/client.crt",

View File

@ -205,6 +205,8 @@ export default withEnglishFallback({
mysqlTlsModeRequired: "必須使用",
mysqlTlsModeVerifyCa: "驗證 CA",
mysqlTlsModeVerifyIdentity: "驗證身份",
mysqlCleartextPasswordAuth: "明文密碼認證",
mysqlCleartextPasswordAuthHint: "僅在 MySQL 伺服器要求明文密碼認證外掛時啟用。",
mysqlCaCertHint: "伺服器端憑證由私有 CA 簽發時Verify CA / Verify Identity 模式需要填寫。",
mysqlClientCert: "用戶端認證",
mysqlClientCertPlaceholder: "/path/to/client.crt",

View File

@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { mysqlCleartextPasswordAuthEnabled, setMysqlCleartextPasswordAuthEnabled } from "../mysqlConnectionOptions";
describe("mysqlConnectionOptions", () => {
it("detects cleartext password auth aliases", () => {
expect(mysqlCleartextPasswordAuthEnabled("allowCleartextPasswords=true")).toBe(true);
expect(mysqlCleartextPasswordAuthEnabled("enable_cleartext_plugin=true")).toBe(true);
expect(mysqlCleartextPasswordAuthEnabled("?AllowCleartextPasswords=TRUE")).toBe(true);
expect(mysqlCleartextPasswordAuthEnabled("allowCleartextPasswords=false")).toBe(false);
});
it("enables the canonical cleartext password auth param and removes aliases", () => {
expect(setMysqlCleartextPasswordAuthEnabled("allowCleartextPasswords=true&charset=utf8mb4", true)).toBe("charset=utf8mb4&enable_cleartext_plugin=true");
});
it("disables cleartext password auth params while preserving unrelated params", () => {
expect(setMysqlCleartextPasswordAuthEnabled("allowCleartextPasswords=true&enable_cleartext_plugin=true&charset=utf8mb4&connect_timeout=10", false)).toBe("charset=utf8mb4&connect_timeout=10");
});
});

View File

@ -0,0 +1,31 @@
const MYSQL_CLEAR_TEXT_PARAM_ALIASES = new Set(["allowcleartextpasswords", "enable_cleartext_plugin"]);
const MYSQL_CLEAR_TEXT_PARAM = "enable_cleartext_plugin";
function normalizeKey(key: string): string {
return key.trim().toLowerCase();
}
function isTruthyUrlParam(value: string): boolean {
return ["true", "1", "yes", "on"].includes(value.trim().toLowerCase());
}
function isMysqlCleartextPasswordParam(key: string): boolean {
return MYSQL_CLEAR_TEXT_PARAM_ALIASES.has(normalizeKey(key));
}
export function mysqlCleartextPasswordAuthEnabled(params: string | undefined): boolean {
const parsed = new URLSearchParams((params || "").trim().replace(/^\?/, ""));
for (const [key, value] of parsed.entries()) {
if (isMysqlCleartextPasswordParam(key) && isTruthyUrlParam(value)) return true;
}
return false;
}
export function setMysqlCleartextPasswordAuthEnabled(params: string | undefined, enabled: boolean): string {
const parsed = new URLSearchParams((params || "").trim().replace(/^\?/, ""));
for (const key of [...parsed.keys()]) {
if (isMysqlCleartextPasswordParam(key)) parsed.delete(key);
}
if (enabled) parsed.set(MYSQL_CLEAR_TEXT_PARAM, "true");
return parsed.toString();
}

View File

@ -952,6 +952,14 @@ fn is_dbx_handled_mysql_url_param(key: &str) -> bool {
)
}
fn is_mysql_cleartext_password_param(key: &str) -> bool {
matches!(key.to_ascii_lowercase().as_str(), "allowcleartextpasswords" | "enable_cleartext_plugin")
}
fn mysql_url_param_value_is_true(value: &str) -> bool {
matches!(value.trim().to_ascii_lowercase().as_str(), "true" | "1" | "yes" | "on")
}
/// Strips the database path from a `mysql://[user[:pass]@]host[:port][/path]`
/// URL, returning only the scheme and authority. Used so mysql_async does not
/// send the database as the schema during the MySQL handshake (StarRocks would
@ -975,6 +983,7 @@ fn mysql_async_url(url: &str) -> Cow<'_, str> {
let mut filtered: Vec<String> = Vec::new();
let mut changed = false;
let mut has_catalog = false;
let mut enable_cleartext_plugin = false;
for segment in query.split('&') {
let segment = segment.trim();
if segment.is_empty() {
@ -989,6 +998,11 @@ fn mysql_async_url(url: &str) -> Cow<'_, str> {
if key.eq_ignore_ascii_case("catalog") {
has_catalog = true;
}
if is_mysql_cleartext_password_param(key) {
changed = true;
enable_cleartext_plugin |= mysql_url_param_value_is_true(value);
continue;
}
if is_dbx_handled_mysql_url_param(key) {
changed = true;
continue;
@ -1017,6 +1031,9 @@ fn mysql_async_url(url: &str) -> Cow<'_, str> {
}
filtered.push(segment.to_string());
}
if enable_cleartext_plugin {
filtered.push("enable_cleartext_plugin=true".to_string());
}
// When a catalog is configured, the database in the URL path must not be
// sent as the schema during the MySQL handshake. Strip the path so mysql_async
@ -3710,6 +3727,24 @@ UNIQUE KEY(`tenant_id`, `name``part`)
assert_eq!(mysql_async_url(url).as_ref(), "mysql://host:3306/db?require_ssl=true");
}
#[test]
fn mysql_async_url_normalizes_cleartext_password_auth_alias() {
let url = "mysql://host:3306/db?allowCleartextPasswords=true&charset=utf8mb4";
assert_eq!(mysql_async_url(url).as_ref(), "mysql://host:3306/db?enable_cleartext_plugin=true");
}
#[test]
fn mysql_async_url_deduplicates_cleartext_password_auth_params() {
let url = "mysql://host:3306/db?allowCleartextPasswords=true&enable_cleartext_plugin=true&require_ssl=true";
assert_eq!(mysql_async_url(url).as_ref(), "mysql://host:3306/db?require_ssl=true&enable_cleartext_plugin=true");
}
#[test]
fn mysql_async_url_omits_disabled_cleartext_password_auth_params() {
let url = "mysql://host:3306/db?allowCleartextPasswords=false&enable_cleartext_plugin=&require_ssl=true";
assert_eq!(mysql_async_url(url).as_ref(), "mysql://host:3306/db?require_ssl=true");
}
#[test]
fn mysql_async_url_strips_go_and_timezone_compat_params() {
let url = "mysql://host:3306/db?charset=utf8mb4&parseTime=True&loc=Local&connectionTimeZone=Asia%2FShanghai&forceConnectionTimeZoneToSession=true&require_ssl=true";

View File

@ -1216,9 +1216,30 @@ fn normalize_bare_mysql_url_params(value: &str) -> String {
.join("&")
}
fn is_mysql_cleartext_password_param(key: &str) -> bool {
matches!(key.to_ascii_lowercase().as_str(), "allowcleartextpasswords" | "enable_cleartext_plugin")
}
fn mysql_url_param_value_is_true(value: &str) -> bool {
matches!(value.trim().to_ascii_lowercase().as_str(), "true" | "1" | "yes" | "on")
}
fn normalize_mysql_url_params(value: &str, force_tls: bool, accept_invalid_certs: bool) -> String {
let value = value.trim_start_matches('?');
let mut parts: Vec<String> = value.split('&').filter(|part| !part.is_empty()).map(str::to_string).collect();
let enable_cleartext_plugin = parts.iter().any(|part| {
let Some((key, value)) = part.split_once('=') else {
return false;
};
is_mysql_cleartext_password_param(key.trim()) && mysql_url_param_value_is_true(value)
});
parts.retain(|part| {
let Some((key, _)) = part.split_once('=') else {
return true;
};
!is_mysql_cleartext_password_param(key.trim())
});
if force_tls {
parts.retain(|part| {
@ -1242,6 +1263,9 @@ fn normalize_mysql_url_params(value: &str, force_tls: bool, accept_invalid_certs
if !parts.iter().any(|part| url_param_key_is(part, "charset")) {
parts.push("charset=utf8mb4".to_string());
}
if enable_cleartext_plugin {
parts.push("enable_cleartext_plugin=true".to_string());
}
parts.join("&")
}
@ -2086,6 +2110,51 @@ mod tests {
);
}
#[test]
fn mysql_cleartext_password_auth_alias_normalizes_to_driver_param() {
let mut config = mysql_config("root", "secret", Some("test"));
config.url_params = Some("allowCleartextPasswords=true".to_string());
assert_eq!(
config.connection_url(),
"mysql://root:secret@10.1.2.3:2883/test?ssl-mode=preferred&charset=utf8mb4&enable_cleartext_plugin=true"
);
}
#[test]
fn mysql_cleartext_password_auth_keeps_canonical_driver_param() {
let mut config = mysql_config("root", "secret", Some("test"));
config.url_params = Some("enable_cleartext_plugin=true".to_string());
assert_eq!(
config.connection_url(),
"mysql://root:secret@10.1.2.3:2883/test?ssl-mode=preferred&charset=utf8mb4&enable_cleartext_plugin=true"
);
}
#[test]
fn mysql_cleartext_password_auth_deduplicates_aliases() {
let mut config = mysql_config("root", "secret", Some("test"));
config.url_params =
Some("allowCleartextPasswords=true&enable_cleartext_plugin=true&charset=utf8mb4".to_string());
assert_eq!(
config.connection_url(),
"mysql://root:secret@10.1.2.3:2883/test?ssl-mode=preferred&charset=utf8mb4&enable_cleartext_plugin=true"
);
}
#[test]
fn mysql_cleartext_password_auth_omits_disabled_values() {
let mut config = mysql_config("root", "secret", Some("test"));
config.url_params = Some("allowCleartextPasswords=false&enable_cleartext_plugin=&charset=utf8mb4".to_string());
assert_eq!(
config.connection_url(),
"mysql://root:secret@10.1.2.3:2883/test?ssl-mode=preferred&charset=utf8mb4"
);
}
#[test]
fn mysql_tls_switch_requires_ssl_without_strict_certificate_checks_by_default() {
let mut config = mysql_config("root", "secret", Some("test"));