feat(hive): add Kerberos connection options

This commit is contained in:
t8y2 committed 2026-07-08 13:23:21 +08:00
1 parent 5b70626cf7
commit 2929b1c035
30 files changed
+639 -23

No files matched your search

@@ -48,6 +48,7 @@ import { isSchemaAware } from "@/lib/database/databaseFeatureSupport";
import VisibleSchemasDialog from "@/components/sidebar/VisibleSchemasDialog.vue";
import { oceanbaseModeConnectionPatch, oceanbaseSubModeFromConfig } from "@/lib/database/oceanbaseConnectionMode";
import { translateBackendError } from "@/i18n/backend-errors";
import { applyHiveKerberosSubmitConfig, hiveKerberosFormConfig, type HiveKerberosAuthMode } from "@/lib/database/hiveKerberosOptions";
type DbOption = { value: string; label: string };
type DbCategory = { key: string; title: string; options: DbOption[] };
@@ -151,6 +152,7 @@ const defaultForm = (): ConnectionForm => ({
driver_profile: "mysql",
driver_label: "MySQL",
url_params: "",
agent_java_options: [],
host: "127.0.0.1",
port: 3306,
username: "root",
@@ -378,6 +380,12 @@ const dremioConnectionUrls = ref<Record<DremioConnectionMode, string>>({
"arrow-flight-sql": DREMIO_ARROW_FLIGHT_SQL_JDBC_URL,
legacy: DREMIO_LEGACY_JDBC_URL,
});
const hiveAuthMode = ref<HiveKerberosAuthMode>("none");
const hivePrincipal = ref("");
const hiveKrb5ConfPath = ref("");
const hiveJaasConfigPath = ref("");
const hiveUseSubjectCredsOnlyFalse = ref(false);
const hiveExtraJavaOptions = ref("");
const dialogStep = ref<DialogStep>("select");
const dbPickerView = ref<DbPickerView>("icon");
const dbSearchQuery = ref("");
@@ -775,6 +783,16 @@ function hydrateInfluxDbFields(value: unknown) {
resetInfluxDbFields(value as Partial<InfluxDbExternalConfig>);
}
function resetHiveKerberosFields(config?: Pick<ConnectionConfig, "url_params" | "agent_java_options">) {
const kerberos = hiveKerberosFormConfig(config?.url_params, config?.agent_java_options);
hiveAuthMode.value = kerberos.authMode;
hivePrincipal.value = kerberos.principal;
hiveKrb5ConfPath.value = kerberos.krb5ConfPath;
hiveJaasConfigPath.value = kerberos.jaasConfigPath;
hiveUseSubjectCredsOnlyFalse.value = kerberos.useSubjectCredsOnlyFalse;
hiveExtraJavaOptions.value = kerberos.extraJavaOptions;
}
function buildInfluxDbExternalConfig(): InfluxDbExternalConfig {
if (influxDbVersion.value !== "2") return { version: "1" };
const org = influxDbOrg.value.trim();
@@ -1202,6 +1220,7 @@ function applyProfile(val: string, preserveConnectionFields = false) {
form.value.port = profile.port;
form.value.username = profile.user;
form.value.url_params = profile.urlParams || "";
form.value.agent_java_options = [];
if (profile.host) {
form.value.host = profile.host;
}
@@ -1258,6 +1277,7 @@ function applyProfile(val: string, preserveConnectionFields = false) {
form.value.password = "";
form.value.connection_string = undefined;
}
resetHiveKerberosFields(profile.type === "hive" ? form.value : undefined);
}
}
@@ -1294,6 +1314,7 @@ watch(
driver_profile: oceanbasePatch?.driver_profile || profile,
driver_label: config.driver_label || oceanbasePatch?.driver_label || driverProfiles[profile]?.label || config.db_type,
url_params: config.url_params || "",
agent_java_options: config.agent_java_options || [],
host: config.db_type === "h2" && h2FilePathFromJdbcUrl(config.connection_string) ? h2FilePathFromJdbcUrl(config.connection_string) : config.host,
port: profile === "tdengine" && (config.port === 0 || config.port === 6030) ? 6041 : config.port,
username: config.username,
@@ -1346,6 +1367,7 @@ watch(
} else {
resetInfluxDbFields();
}
resetHiveKerberosFields(config.db_type === "hive" ? config : undefined);
h2ConnectionMode.value = h2ConnectionModeForConfig(config);
customColorInput.value = config.color || "";
selectedTransportLayerId.value = form.value.transport_layers?.[0]?.id || null;
@@ -1373,6 +1395,7 @@ watch(
resetMqFields();
resetNacosFields();
resetInfluxDbFields();
resetHiveKerberosFields();
oceanbaseSubMode.value = "mysql";
h2ConnectionMode.value = "file";
dremioConnectionMode.value = "legacy";
@@ -2128,6 +2151,24 @@ function connectionConfigForSubmit(id: string): ConnectionConfig {
if (config.db_type === "manticoresearch") {
config.url_params = "";
}
if (config.db_type === "hive") {
if (hiveAuthMode.value === "kerberos" && !hivePrincipal.value.trim()) {
throw new Error(t("connection.hiveKerberosPrincipalRequired"));
}
const hiveKerberos = applyHiveKerberosSubmitConfig({
authMode: hiveAuthMode.value,
principal: hivePrincipal.value,
krb5ConfPath: hiveKrb5ConfPath.value,
jaasConfigPath: hiveJaasConfigPath.value,
useSubjectCredsOnlyFalse: hiveUseSubjectCredsOnlyFalse.value,
extraJavaOptions: hiveExtraJavaOptions.value,
urlParams: config.url_params,
});
config.url_params = hiveKerberos.urlParams;
config.agent_java_options = hiveKerberos.agentJavaOptions;
} else {
config.agent_java_options = undefined;
}
if (config.db_type === "informix" && config.informix_server) {
// Strip INFORMIXSERVER from url_params to avoid duplicate when dedicated field is used
config.url_params = (config.url_params || "")
@@ -2339,6 +2380,7 @@ function connectionConfigForSubmit(id: string): ConnectionConfig {
config.visible_databases = Array.isArray(config.visible_databases) && config.visible_databases.length > 0 ? config.visible_databases : undefined;
}
if (config.visible_schemas && Object.keys(config.visible_schemas).length === 0) config.visible_schemas = undefined;
if (config.agent_java_options && config.agent_java_options.length === 0) config.agent_java_options = undefined;
return config as ConnectionConfig;
}
@@ -3239,6 +3281,24 @@ async function browseEtcdTlsFile(target: "ca" | "cert" | "key") {
}
}
async function browseHiveKerberosFile(target: "krb5" | "jaas") {
if (isTauriRuntime()) {
const { open } = await import("@tauri-apps/plugin-dialog");
const selected = await open({
title: target === "krb5" ? t("connection.hiveKrb5ConfBrowse") : t("connection.hiveJaasConfigBrowse"),
multiple: false,
filters: [{ name: "Config", extensions: ["conf", "ini", "properties", "*"] }],
});
if (selected && typeof selected === "string") {
if (target === "krb5") {
hiveKrb5ConfPath.value = selected;
} else {
hiveJaasConfigPath.value = selected;
}
}
}
}
async function browseDbFilePath() {
if (isTauriRuntime()) {
const { open } = await import("@tauri-apps/plugin-dialog");
@@ -4387,6 +4447,76 @@ function openExternalUrl(url: string) {
<Input v-model="form.database" class="col-span-3" :placeholder="databasePlaceholder" />
</div>
<template v-if="form.db_type === 'hive'">
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ t("connection.hiveAuthMode") }}</Label>
<div class="col-span-3 grid h-8 grid-cols-2 overflow-hidden rounded-md border border-input bg-muted/30 p-0.5">
<button type="button" class="h-7 rounded-sm px-3 text-sm transition-colors" :class="hiveAuthMode === 'none' ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'" :aria-pressed="hiveAuthMode === 'none'" @click="hiveAuthMode = 'none'">
{{ t("connection.hiveAuthNone") }}
</button>
<button
type="button"
class="h-7 rounded-sm px-3 text-sm transition-colors"
:class="hiveAuthMode === 'kerberos' ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
:aria-pressed="hiveAuthMode === 'kerberos'"
@click="hiveAuthMode = 'kerberos'"
>
Kerberos
</button>
</div>
</div>
<template v-if="hiveAuthMode === 'kerberos'">
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.hivePrincipal") }}</Label>
<Input v-model="hivePrincipal" class="col-span-3" placeholder="hive/_HOST@EXAMPLE.COM" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">krb5.conf</Label>
<div class="col-span-3 flex items-center gap-1">
<Input v-model="hiveKrb5ConfPath" class="flex-1" placeholder="/etc/krb5.conf" />
<Tooltip v-if="isDesktop">
<TooltipTrigger as-child>
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="browseHiveKerberosFile('krb5')">
<FolderOpen class="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("connection.hiveKrb5ConfBrowse") }}</TooltipContent>
</Tooltip>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">JAAS</Label>
<div class="col-span-3 flex items-center gap-1">
<Input v-model="hiveJaasConfigPath" class="flex-1" placeholder="/etc/hive-jaas.conf" />
<Tooltip v-if="isDesktop">
<TooltipTrigger as-child>
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="browseHiveKerberosFile('jaas')">
<FolderOpen class="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("connection.hiveJaasConfigBrowse") }}</TooltipContent>
</Tooltip>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.hiveTicketCache") }}</Label>
<label class="col-span-3 flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="hiveUseSubjectCredsOnlyFalse" class="mr-0" />
<span class="text-xs text-muted-foreground">{{ t("connection.hiveTicketCacheFallback") }}</span>
</label>
</div>
<div class="grid grid-cols-4 items-start gap-4">
<Label :class="connectionLabelTopClass">{{ t("connection.hiveJvmOptions") }}</Label>
<textarea
v-model="hiveExtraJavaOptions"
class="col-span-3 min-h-16 rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
:placeholder="t('connection.hiveJvmOptionsPlaceholder')"
/>
</div>
</template>
</template>
<div v-if="form.db_type === 'oracle'" class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.mode") }}</Label>
<div class="col-span-3 grid h-8 grid-cols-2 overflow-hidden rounded-md border border-input bg-muted/30 p-0.5">
+10
View File
@@ -199,6 +199,16 @@ export default {
driverName: "Driver Name",
driverNamePlaceholder: "Vendor or environment name",
urlParams: "URL Params",
hiveAuthMode: "Auth",
hiveAuthNone: "None",
hivePrincipal: "Principal",
hiveKerberosPrincipalRequired: "Hive Kerberos principal is required",
hiveKrb5ConfBrowse: "Choose krb5.conf",
hiveJaasConfigBrowse: "Choose JAAS config",
hiveTicketCache: "Ticket Cache",
hiveTicketCacheFallback: "Set useSubjectCredsOnly=false",
hiveJvmOptions: "JVM Options",
hiveJvmOptionsPlaceholder: "-Dsun.security.krb5.debug=true",
gbaseServer: "GBASEDBTSERVER",
informixServer: "INFORMIXSERVER",
sslEnable: "Enable encrypted connection",
+10
View File
@@ -201,6 +201,16 @@ export default withEnglishFallback({
driverName: "驱动名称",
driverNamePlaceholder: "厂商或环境名称",
urlParams: "URL 参数",
hiveAuthMode: "认证",
hiveAuthNone: "无",
hivePrincipal: "Principal",
hiveKerberosPrincipalRequired: "Hive Kerberos principal 不能为空",
hiveKrb5ConfBrowse: "选择 krb5.conf",
hiveJaasConfigBrowse: "选择 JAAS 配置",
hiveTicketCache: "票据缓存",
hiveTicketCacheFallback: "设置 useSubjectCredsOnly=false",
hiveJvmOptions: "JVM 参数",
hiveJvmOptionsPlaceholder: "-Dsun.security.krb5.debug=true",
gbaseServer: "GBASEDBTSERVER",
informixServer: "INFORMIXSERVER",
sslEnable: "启用加密连接",
@@ -0,0 +1,149 @@
export type HiveKerberosAuthMode = "none" | "kerberos";
export interface HiveKerberosFormConfig {
authMode: HiveKerberosAuthMode;
principal: string;
krb5ConfPath: string;
jaasConfigPath: string;
useSubjectCredsOnlyFalse: boolean;
extraJavaOptions: string;
}
export interface HiveKerberosSubmitConfig extends HiveKerberosFormConfig {
urlParams?: string;
}
const KRB5_CONF_PREFIX = "-Djava.security.krb5.conf=";
const JAAS_CONFIG_PREFIX = "-Djava.security.auth.login.config=";
const SUBJECT_CREDS_PREFIX = "-Djavax.security.auth.useSubjectCredsOnly=";
export function hiveKerberosFormConfig(urlParams?: string, agentJavaOptions?: string[]): HiveKerberosFormConfig {
const params = parseHiveUrlParams(urlParams);
const auth = getHiveUrlParam(params, "auth").toLowerCase();
const principal = getHiveUrlParam(params, "principal");
const java = parseHiveKerberosJavaOptions(agentJavaOptions || []);
return {
authMode: auth === "kerberos" || principal ? "kerberos" : "none",
principal,
...java,
};
}
export function applyHiveKerberosSubmitConfig(config: HiveKerberosSubmitConfig): {
urlParams: string;
agentJavaOptions: string[];
} {
const enabled = config.authMode === "kerberos";
return {
urlParams: applyHiveKerberosUrlParams(config.urlParams, enabled, config.principal),
agentJavaOptions: buildHiveKerberosJavaOptions(enabled, config),
};
}
export function applyHiveKerberosUrlParams(urlParams: string | undefined, enabled: boolean, principal: string): string {
const params = parseHiveUrlParams(urlParams).filter((param) => shouldKeepHiveUrlParam(param, enabled));
if (enabled) {
const normalizedPrincipal = principal.trim();
if (normalizedPrincipal) {
params.push({ key: "principal", value: normalizedPrincipal });
}
}
return serializeHiveUrlParams(params);
}
export function buildHiveKerberosJavaOptions(enabled: boolean, config: HiveKerberosFormConfig): string[] {
const options: string[] = [];
if (enabled) {
const krb5ConfPath = config.krb5ConfPath.trim();
const jaasConfigPath = config.jaasConfigPath.trim();
if (krb5ConfPath) options.push(`${KRB5_CONF_PREFIX}${krb5ConfPath}`);
if (jaasConfigPath) options.push(`${JAAS_CONFIG_PREFIX}${jaasConfigPath}`);
if (config.useSubjectCredsOnlyFalse) options.push(`${SUBJECT_CREDS_PREFIX}false`);
}
options.push(...splitExtraJavaOptions(config.extraJavaOptions));
return options;
}
interface HiveUrlParam {
key: string;
value: string;
}
function parseHiveUrlParams(urlParams?: string): HiveUrlParam[] {
return trimHiveUrlParams(urlParams)
.split(/[;&]/)
.map((part) => part.trim())
.filter(Boolean)
.map((part) => {
const equals = part.indexOf("=");
if (equals < 0) return { key: part, value: "" };
return { key: part.slice(0, equals).trim(), value: part.slice(equals + 1).trim() };
})
.filter((param) => !!param.key);
}
function serializeHiveUrlParams(params: HiveUrlParam[]): string {
return params.map((param) => (param.value ? `${param.key}=${param.value}` : param.key)).join(";");
}
function trimHiveUrlParams(urlParams?: string): string {
return (urlParams || "")
.trim()
.replace(/^[?&;]+/, "")
.replace(/[?&;]+$/, "");
}
function getHiveUrlParam(params: HiveUrlParam[], key: string): string {
const lowerKey = key.toLowerCase();
return params.find((param) => param.key.toLowerCase() === lowerKey)?.value || "";
}
function shouldKeepHiveUrlParam(param: HiveUrlParam, kerberosEnabled: boolean): boolean {
const key = param.key.toLowerCase();
if (key === "principal") return false;
if (key !== "auth") return true;
const auth = param.value.toLowerCase();
if (kerberosEnabled) {
// `auth=noSasl` and token auth conflict with Kerberos. Hive's JDBC driver selects
// regular Kerberos from `principal`; `auth=kerberos` is only needed for explicit advanced cases.
return auth === "kerberos";
}
return auth !== "kerberos";
}
function parseHiveKerberosJavaOptions(options: string[]): Omit<HiveKerberosFormConfig, "authMode" | "principal"> {
let krb5ConfPath = "";
let jaasConfigPath = "";
let useSubjectCredsOnlyFalse = false;
const extra: string[] = [];
for (const option of options) {
const trimmed = option.trim();
if (!trimmed) continue;
if (trimmed.startsWith(KRB5_CONF_PREFIX)) {
krb5ConfPath = trimmed.slice(KRB5_CONF_PREFIX.length);
} else if (trimmed.startsWith(JAAS_CONFIG_PREFIX)) {
jaasConfigPath = trimmed.slice(JAAS_CONFIG_PREFIX.length);
} else if (trimmed.startsWith(SUBJECT_CREDS_PREFIX)) {
useSubjectCredsOnlyFalse = trimmed.slice(SUBJECT_CREDS_PREFIX.length).toLowerCase() === "false";
} else {
extra.push(trimmed);
}
}
return {
krb5ConfPath,
jaasConfigPath,
useSubjectCredsOnlyFalse,
extraJavaOptions: extra.join("\n"),
};
}
function splitExtraJavaOptions(value: string): string[] {
return value
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
}
@@ -825,6 +825,7 @@ export const useConnectionStore = defineStore("connection", () => {
driver_profile: profile,
driver_label: config.driver_label || labelMap[profile] || config.db_type,
url_params: config.url_params || "",
agent_java_options: Array.isArray(config.agent_java_options) ? config.agent_java_options : [],
attached_databases: Array.isArray(config.attached_databases) ? config.attached_databases.filter((database) => database.name?.trim() && database.path?.trim()) : [],
transport_layers: Array.isArray(config.transport_layers) ? config.transport_layers : [],
connect_timeout_secs: config.connect_timeout_secs || 10,
+1
View File
@@ -118,6 +118,7 @@ export interface ConnectionConfig {
driver_profile?: string;
driver_label?: string;
url_params?: string;
agent_java_options?: string[];
host: string;
port: number;
username: string;