fix(mysql): support cleartext password auth option (#2499)
This commit is contained in:
parent
cabe89a19b
commit
21cec24e75
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
Loading…
Reference in New Issue