mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(hive): add Kerberos connection options
This commit is contained in:
@@ -169,8 +169,30 @@ public final class HiveAgent extends AbstractJdbcAgent {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String buildUrl(ConnectParams params) {
|
||||
return "jdbc:hive2://" + params.getHost() + ":" + params.getPort() + "/" + params.getDatabase();
|
||||
static String buildUrl(ConnectParams params) {
|
||||
String connectionString = trimToEmpty(params.getConnection_string());
|
||||
if (!connectionString.isEmpty()) {
|
||||
return connectionString;
|
||||
}
|
||||
String url = "jdbc:hive2://" + params.getHost() + ":" + params.getPort() + "/" + params.getDatabase();
|
||||
return appendHiveUrlParams(url, params.getUrl_params());
|
||||
}
|
||||
|
||||
private static String appendHiveUrlParams(String url, String urlParams) {
|
||||
String params = trimHiveUrlParams(urlParams);
|
||||
if (params.isEmpty()) {
|
||||
return url;
|
||||
}
|
||||
// Hive JDBC authentication options use semicolon-separated URL properties.
|
||||
return url + (url.endsWith(";") ? "" : ";") + params;
|
||||
}
|
||||
|
||||
private static String trimHiveUrlParams(String urlParams) {
|
||||
String value = trimToEmpty(urlParams);
|
||||
while (value.startsWith("?") || value.startsWith("&") || value.startsWith(";")) {
|
||||
value = value.substring(1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String trimToNull(String value) {
|
||||
|
||||
@@ -1,11 +1,42 @@
|
||||
package com.dbx.agent.hive;
|
||||
|
||||
import com.dbx.agent.ConnectParams;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class HiveAgentTest {
|
||||
@Test
|
||||
void hiveJdbcStandaloneRuntimeClassesAreAvailable() throws ClassNotFoundException {
|
||||
Class.forName("org.apache.hive.jdbc.HiveDriver");
|
||||
Class.forName("org.apache.hive.org.apache.thrift.protocol.TProtocol");
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildUrlAppendsKerberosUrlParams() {
|
||||
ConnectParams params = new ConnectParams();
|
||||
params.setHost("hive.example.com");
|
||||
params.setPort(10000);
|
||||
params.setDatabase("default");
|
||||
params.setUrl_params(";principal=hive/hive.example.com@EXAMPLE.COM;auth=kerberos");
|
||||
|
||||
assertEquals(
|
||||
"jdbc:hive2://hive.example.com:10000/default;principal=hive/hive.example.com@EXAMPLE.COM;auth=kerberos",
|
||||
HiveAgent.buildUrl(params)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildUrlUsesCustomJdbcUrlAsIs() {
|
||||
ConnectParams params = new ConnectParams();
|
||||
params.setConnection_string(
|
||||
"jdbc:hive2://zk1.example.com,zk2.example.com/default;serviceDiscoveryMode=zooKeeper;zooKeeperNamespace=hiveserver2;principal=hive/_HOST@EXAMPLE.COM"
|
||||
);
|
||||
params.setUrl_params("principal=hive/ignored@EXAMPLE.COM");
|
||||
|
||||
assertEquals(
|
||||
"jdbc:hive2://zk1.example.com,zk2.example.com/default;serviceDiscoveryMode=zooKeeper;zooKeeperNamespace=hiveserver2;principal=hive/_HOST@EXAMPLE.COM",
|
||||
HiveAgent.buildUrl(params)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -581,6 +581,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 3306,
|
||||
username: "user".to_string(),
|
||||
|
||||
@@ -578,6 +578,16 @@ impl AgentManager {
|
||||
state: &AgentState,
|
||||
driver_key: &str,
|
||||
jre_key: &str,
|
||||
) -> Result<AgentLaunchSpec, String> {
|
||||
self.resolve_agent_launch_spec_with_extra_args(state, driver_key, jre_key, &[])
|
||||
}
|
||||
|
||||
pub fn resolve_agent_launch_spec_with_extra_args(
|
||||
&self,
|
||||
state: &AgentState,
|
||||
driver_key: &str,
|
||||
jre_key: &str,
|
||||
extra_java_args: &[String],
|
||||
) -> Result<AgentLaunchSpec, String> {
|
||||
let driver_dir = self.driver_dir(driver_key);
|
||||
let config_path = self.driver_launch_config_path(driver_key);
|
||||
@@ -598,7 +608,7 @@ impl AgentManager {
|
||||
"{driver_key} driver jar is invalid or corrupt. Please reinstall it from the Driver Manager."
|
||||
));
|
||||
}
|
||||
return Ok(AgentLaunchSpec::java_jar(java, jar_path));
|
||||
return Ok(AgentLaunchSpec::java_jar_with_extra_args(java, jar_path, extra_java_args));
|
||||
}
|
||||
|
||||
Err(format!("{driver_key} driver is not installed. Please install it from the Driver Manager."))
|
||||
@@ -766,7 +776,16 @@ impl AgentManager {
|
||||
db_type: &DatabaseType,
|
||||
driver_profile: Option<&str>,
|
||||
) -> Result<AgentDriverClient, String> {
|
||||
crate::agent_runtime::spawn_connection_client(self, db_type, driver_profile).await
|
||||
self.spawn_with_extra_java_args(db_type, driver_profile, &[]).await
|
||||
}
|
||||
|
||||
pub async fn spawn_with_extra_java_args(
|
||||
&self,
|
||||
db_type: &DatabaseType,
|
||||
driver_profile: Option<&str>,
|
||||
extra_java_args: &[String],
|
||||
) -> Result<AgentDriverClient, String> {
|
||||
crate::agent_runtime::spawn_connection_client(self, db_type, driver_profile, extra_java_args).await
|
||||
}
|
||||
|
||||
pub async fn call_daemon<T: serde::de::DeserializeOwned + Send + 'static>(
|
||||
|
||||
@@ -24,7 +24,7 @@ pub async fn stop_daemon_by_key(manager: &AgentManager, agent_key: &str) {
|
||||
|
||||
pub async fn restart_daemon_by_key(manager: &AgentManager, agent_key: &str) -> Result<(), String> {
|
||||
manager.daemons.lock().await.remove(agent_key);
|
||||
let client = spawn_client_for_key(manager, agent_key).await?;
|
||||
let client = spawn_client_for_key(manager, agent_key, &[]).await?;
|
||||
manager.daemons.lock().await.insert(agent_key.to_string(), client);
|
||||
Ok(())
|
||||
}
|
||||
@@ -33,10 +33,11 @@ pub async fn spawn_connection_client(
|
||||
manager: &AgentManager,
|
||||
db_type: &DatabaseType,
|
||||
driver_profile: Option<&str>,
|
||||
extra_java_args: &[String],
|
||||
) -> Result<AgentDriverClient, String> {
|
||||
let keys = runtime_agent_key_candidates(db_type, driver_profile)
|
||||
.ok_or_else(|| format!("{:?} is not an agent-driven database type", db_type))?;
|
||||
spawn_first_available_client(manager, &keys).await
|
||||
spawn_first_available_client(manager, &keys, extra_java_args).await
|
||||
}
|
||||
|
||||
pub async fn call_daemon<T: DeserializeOwned + Send + 'static>(
|
||||
@@ -53,7 +54,7 @@ pub async fn call_daemon<T: DeserializeOwned + Send + 'static>(
|
||||
let mut daemons = manager.daemons.lock().await;
|
||||
|
||||
if !daemons.contains_key(&key) {
|
||||
let client = spawn_client_for_key(manager, &key).await?;
|
||||
let client = spawn_client_for_key(manager, &key, &[]).await?;
|
||||
daemons.insert(key.clone(), client);
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ pub async fn call_daemon<T: DeserializeOwned + Send + 'static>(
|
||||
Err(err) => {
|
||||
log::warn!("[agent] daemon call failed, respawning: {err}");
|
||||
daemons.remove(&key);
|
||||
let mut new_client = spawn_client_for_key(manager, &key).await?;
|
||||
let mut new_client = spawn_client_for_key(manager, &key, &[]).await?;
|
||||
let result = new_client.call::<T>(method, params).await?;
|
||||
daemons.insert(key, new_client);
|
||||
Ok(result)
|
||||
@@ -86,7 +87,7 @@ pub async fn call_daemon_with_timeout<T: DeserializeOwned + Send + 'static>(
|
||||
let mut daemons = manager.daemons.lock().await;
|
||||
|
||||
if !daemons.contains_key(&key) {
|
||||
let client = spawn_client_for_key(manager, &key).await?;
|
||||
let client = spawn_client_for_key(manager, &key, &[]).await?;
|
||||
daemons.insert(key.clone(), client);
|
||||
}
|
||||
|
||||
@@ -96,7 +97,7 @@ pub async fn call_daemon_with_timeout<T: DeserializeOwned + Send + 'static>(
|
||||
Err(err) => {
|
||||
log::warn!("[agent] daemon call failed, respawning: {err}");
|
||||
daemons.remove(&key);
|
||||
let mut new_client = spawn_client_for_key(manager, &key).await?;
|
||||
let mut new_client = spawn_client_for_key(manager, &key, &[]).await?;
|
||||
let result = new_client.call_with_timeout::<T>(method, params, timeout_duration).await?;
|
||||
daemons.insert(key, new_client);
|
||||
Ok(result)
|
||||
@@ -137,10 +138,11 @@ fn first_installed_agent_key<'a>(manager: &AgentManager, keys: &'a [&'static str
|
||||
async fn spawn_first_available_client(
|
||||
manager: &AgentManager,
|
||||
keys: &[&'static str],
|
||||
extra_java_args: &[String],
|
||||
) -> Result<AgentDriverClient, String> {
|
||||
let mut last_error = None;
|
||||
for key in keys {
|
||||
match spawn_client_for_key(manager, key).await {
|
||||
match spawn_client_for_key(manager, key, extra_java_args).await {
|
||||
Ok(client) => return Ok(client),
|
||||
Err(err) => last_error = Some(err),
|
||||
}
|
||||
@@ -148,11 +150,15 @@ async fn spawn_first_available_client(
|
||||
Err(last_error.unwrap_or_else(|| "No agent driver candidates available".to_string()))
|
||||
}
|
||||
|
||||
async fn spawn_client_for_key(manager: &AgentManager, key: &str) -> Result<AgentDriverClient, String> {
|
||||
async fn spawn_client_for_key(
|
||||
manager: &AgentManager,
|
||||
key: &str,
|
||||
extra_java_args: &[String],
|
||||
) -> Result<AgentDriverClient, String> {
|
||||
let state = manager.load_state();
|
||||
let jre_key = state.installed_drivers.get(key).map(|driver| driver.jre.as_str()).unwrap_or(DEFAULT_JRE_KEY);
|
||||
|
||||
let launch = manager.resolve_agent_launch_spec(&state, key, jre_key)?;
|
||||
let launch = manager.resolve_agent_launch_spec_with_extra_args(&state, key, jre_key, extra_java_args)?;
|
||||
let mut client = AgentDriverClient::spawn(launch).await?;
|
||||
client.try_optional_handshake(manager.agent_app_version()).await;
|
||||
Ok(client)
|
||||
|
||||
@@ -752,6 +752,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 5432,
|
||||
username: "app".to_string(),
|
||||
@@ -801,6 +802,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 8848,
|
||||
username: "nacos".to_string(),
|
||||
@@ -887,6 +889,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "localhost".to_string(),
|
||||
port: 5432,
|
||||
username: "user".to_string(),
|
||||
|
||||
@@ -1187,8 +1187,15 @@ impl AppState {
|
||||
agent_connection_pool_database_type!() => {
|
||||
let connect_params =
|
||||
agent_connect_params(&db_config, &host, port, db_config.effective_database().unwrap_or(""));
|
||||
let mut client =
|
||||
self.agent_manager.spawn(&db_config.db_type, db_config.driver_profile.as_deref()).await?;
|
||||
// Kerberos JVM properties are connection-scoped; shared agent daemons must not inherit them.
|
||||
let mut client = self
|
||||
.agent_manager
|
||||
.spawn_with_extra_java_args(
|
||||
&db_config.db_type,
|
||||
db_config.driver_profile.as_deref(),
|
||||
&db_config.agent_java_options,
|
||||
)
|
||||
.await?;
|
||||
let connect_result = client
|
||||
.call_method_with_timeout::<serde_json::Value>(
|
||||
AgentMethod::Connect,
|
||||
@@ -3005,6 +3012,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 3306,
|
||||
username: "root".to_string(),
|
||||
|
||||
@@ -724,6 +724,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "localhost".to_string(),
|
||||
port: 5432,
|
||||
username: "postgres".to_string(),
|
||||
|
||||
@@ -15,6 +15,7 @@ const RPC_TIMEOUT_SECS: u64 = 30;
|
||||
const STARTUP_TIMEOUT_SECS: u64 = 15;
|
||||
const STDERR_TAIL_LINES: usize = 20;
|
||||
const AGENT_EXIT_DIAGNOSTIC_WAIT_MS: u64 = 200;
|
||||
const AGENT_JAVA_OPTS_ENV: &str = "DBX_AGENT_JAVA_OPTS";
|
||||
const AGENT_JAVA_TOO_OLD_MESSAGE: &str =
|
||||
"Agent requires Java 21, but DBX started it with an older Java runtime. Use DBX managed JRE 21 or select a Java 21 executable in Driver Manager.";
|
||||
#[cfg(windows)]
|
||||
@@ -42,10 +43,18 @@ impl AgentLaunchSpec {
|
||||
}
|
||||
|
||||
pub fn java_jar(java_path: impl Into<PathBuf>, jar_path: impl AsRef<Path>) -> Self {
|
||||
Self::java_jar_with_extra_args(java_path, jar_path, &[])
|
||||
}
|
||||
|
||||
pub fn java_jar_with_extra_args(
|
||||
java_path: impl Into<PathBuf>,
|
||||
jar_path: impl AsRef<Path>,
|
||||
extra_java_args: &[String],
|
||||
) -> Self {
|
||||
let jar_path = jar_path.as_ref();
|
||||
Self {
|
||||
program: java_path.into(),
|
||||
args: agent_java_args(&jar_path.to_string_lossy()),
|
||||
args: agent_java_args_with_extra_args(&jar_path.to_string_lossy(), extra_java_args),
|
||||
working_dir: jar_path.parent().map(Path::to_path_buf),
|
||||
}
|
||||
}
|
||||
@@ -1172,7 +1181,25 @@ pub fn mongo_document_id_params(database: &str, collection: &str, id: &str) -> V
|
||||
serde_json::json!({ "database": database, "collection": collection, "id": id })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn agent_java_args(jar_path: &str) -> Vec<String> {
|
||||
agent_java_args_with_extra(jar_path, std::env::var(AGENT_JAVA_OPTS_ENV).ok().as_deref())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn agent_java_args_with_extra(jar_path: &str, extra_opts: Option<&str>) -> Vec<String> {
|
||||
agent_java_args_with_extra_opts(jar_path, extra_opts, &[])
|
||||
}
|
||||
|
||||
fn agent_java_args_with_extra_args(jar_path: &str, extra_java_args: &[String]) -> Vec<String> {
|
||||
agent_java_args_with_extra_opts(jar_path, std::env::var(AGENT_JAVA_OPTS_ENV).ok().as_deref(), extra_java_args)
|
||||
}
|
||||
|
||||
fn agent_java_args_with_extra_opts(
|
||||
jar_path: &str,
|
||||
extra_opts: Option<&str>,
|
||||
extra_java_args: &[String],
|
||||
) -> Vec<String> {
|
||||
let mut args = vec![
|
||||
"-Dfile.encoding=UTF-8",
|
||||
"-Dsun.stdout.encoding=UTF-8",
|
||||
@@ -1192,6 +1219,13 @@ fn agent_java_args(jar_path: &str) -> Vec<String> {
|
||||
args.push("-Djava.net.preferIPv4Stack=true".to_string());
|
||||
}
|
||||
|
||||
// Hive/Kerberos JDBC drivers read JAAS and krb5 settings during JVM startup,
|
||||
// so users need a process-level escape hatch before the agent jar is loaded.
|
||||
if let Some(extra) = extra_opts {
|
||||
args.extend(parse_agent_java_opts(extra));
|
||||
}
|
||||
args.extend(extra_java_args.iter().map(|arg| arg.trim()).filter(|arg| !arg.is_empty()).map(str::to_string));
|
||||
|
||||
args.push("--add-opens=java.sql/java.sql=ALL-UNNAMED".to_string());
|
||||
|
||||
args.extend(["-XX:TieredStopAtLevel=1", "-XX:+UseSerialGC", "-jar", jar_path].into_iter().map(str::to_string));
|
||||
@@ -1199,6 +1233,43 @@ fn agent_java_args(jar_path: &str) -> Vec<String> {
|
||||
args
|
||||
}
|
||||
|
||||
fn parse_agent_java_opts(opts: &str) -> Vec<String> {
|
||||
let mut args = Vec::new();
|
||||
let mut current = String::new();
|
||||
let mut quote = None;
|
||||
let mut chars = opts.chars().peekable();
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match (quote, ch) {
|
||||
(None, ch) if ch.is_whitespace() => {
|
||||
if !current.is_empty() {
|
||||
args.push(std::mem::take(&mut current));
|
||||
}
|
||||
}
|
||||
(None, '\'' | '"') => quote = Some(ch),
|
||||
(Some(q), ch) if ch == q => quote = None,
|
||||
(Some('"'), '\\') => {
|
||||
if let Some(&next) = chars.peek() {
|
||||
if next == '"' || next == '\\' {
|
||||
current.push(chars.next().unwrap());
|
||||
} else {
|
||||
current.push(ch);
|
||||
}
|
||||
} else {
|
||||
current.push(ch);
|
||||
}
|
||||
}
|
||||
_ => current.push(ch),
|
||||
}
|
||||
}
|
||||
|
||||
if !current.is_empty() {
|
||||
args.push(current);
|
||||
}
|
||||
|
||||
args
|
||||
}
|
||||
|
||||
fn agent_jar_path_matches_key(jar_path: &str, key: &str) -> bool {
|
||||
Path::new(jar_path).components().any(|component| component.as_os_str().to_string_lossy() == key)
|
||||
}
|
||||
@@ -1345,13 +1416,13 @@ impl Drop for AgentDriverClient {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
agent_close_query_session_params, agent_handshake_params, agent_java_args, agent_object_source_params,
|
||||
agent_proxy_env_vars, agent_schema_params, agent_schema_table_params, agent_supports_capability,
|
||||
agent_transaction_params, format_agent_process_error, format_agent_startup_error,
|
||||
is_unsupported_handshake_error, mongo_collection_params, mongo_database_params, mongo_document_id_params,
|
||||
read_agent_line, start_stderr_collector, AgentCapability, AgentDriverClient, AgentHandshake, AgentKvMethod,
|
||||
AgentMethod, AgentTableReadCloseParams, AgentTableReadPageParams, AgentTableReadStartParams, MongoAgentMethod,
|
||||
StderrTail, AGENT_PROTOCOL_VERSION,
|
||||
agent_close_query_session_params, agent_handshake_params, agent_java_args, agent_java_args_with_extra,
|
||||
agent_java_args_with_extra_opts, agent_object_source_params, agent_proxy_env_vars, agent_schema_params,
|
||||
agent_schema_table_params, agent_supports_capability, agent_transaction_params, format_agent_process_error,
|
||||
format_agent_startup_error, is_unsupported_handshake_error, mongo_collection_params, mongo_database_params,
|
||||
mongo_document_id_params, parse_agent_java_opts, read_agent_line, start_stderr_collector, AgentCapability,
|
||||
AgentDriverClient, AgentHandshake, AgentKvMethod, AgentMethod, AgentTableReadCloseParams,
|
||||
AgentTableReadPageParams, AgentTableReadStartParams, MongoAgentMethod, StderrTail, AGENT_PROTOCOL_VERSION,
|
||||
};
|
||||
use std::io::Cursor;
|
||||
use std::process::{Command, Stdio};
|
||||
@@ -1403,6 +1474,60 @@ mod tests {
|
||||
assert!(!args.iter().any(|arg| arg == "-Djava.net.preferIPv4Stack=true"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_java_args_include_custom_jvm_options_before_jar() {
|
||||
let args = agent_java_args_with_extra(
|
||||
"/tmp/dbx/drivers/hive/agent.jar",
|
||||
Some("-Djava.security.auth.login.config=C:\\jaas.conf -Djavax.security.auth.useSubjectCredsOnly=false"),
|
||||
);
|
||||
|
||||
let login_config = args
|
||||
.iter()
|
||||
.position(|arg| arg == "-Djava.security.auth.login.config=C:\\jaas.conf")
|
||||
.expect("custom JAAS option should be present");
|
||||
let jar = args.iter().position(|arg| arg == "-jar").expect("agent jar marker should be present");
|
||||
|
||||
assert!(login_config < jar);
|
||||
assert!(args.iter().any(|arg| arg == "-Djavax.security.auth.useSubjectCredsOnly=false"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_java_args_include_connection_jvm_options_after_env_options() {
|
||||
let args = agent_java_args_with_extra_opts(
|
||||
"/tmp/dbx/drivers/hive/agent.jar",
|
||||
Some("-Djava.security.krb5.conf=/etc/global-krb5.conf"),
|
||||
&["-Djava.security.krb5.conf=/etc/connection-krb5.conf".to_string()],
|
||||
);
|
||||
|
||||
let global = args
|
||||
.iter()
|
||||
.position(|arg| arg == "-Djava.security.krb5.conf=/etc/global-krb5.conf")
|
||||
.expect("global krb5 option should be present");
|
||||
let connection = args
|
||||
.iter()
|
||||
.position(|arg| arg == "-Djava.security.krb5.conf=/etc/connection-krb5.conf")
|
||||
.expect("connection krb5 option should be present");
|
||||
let jar = args.iter().position(|arg| arg == "-jar").expect("agent jar marker should be present");
|
||||
|
||||
assert!(global < connection);
|
||||
assert!(connection < jar);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_java_opts_parser_preserves_quoted_windows_paths() {
|
||||
let args = parse_agent_java_opts(
|
||||
r#"-Djava.security.krb5.conf="C:\Program Files\MIT\Kerberos5\krb5.ini" -Dsun.security.krb5.debug=true"#,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
"-Djava.security.krb5.conf=C:\\Program Files\\MIT\\Kerberos5\\krb5.ini",
|
||||
"-Dsun.security.krb5.debug=true"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_process_environment_removes_common_proxy_variables() {
|
||||
let proxy_env_vars = agent_proxy_env_vars();
|
||||
|
||||
@@ -3029,6 +3029,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "cache.example.com".to_string(),
|
||||
port: 6379,
|
||||
username: String::new(),
|
||||
|
||||
@@ -14,6 +14,8 @@ pub struct ConnectionConfig {
|
||||
pub driver_label: Option<String>,
|
||||
#[serde(default)]
|
||||
pub url_params: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub agent_java_options: Vec<String>,
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub username: String,
|
||||
@@ -382,6 +384,8 @@ struct ConnectionConfigData {
|
||||
pub driver_label: Option<String>,
|
||||
#[serde(default)]
|
||||
pub url_params: Option<String>,
|
||||
#[serde(default)]
|
||||
pub agent_java_options: Vec<String>,
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub username: String,
|
||||
@@ -464,6 +468,7 @@ impl From<ConnectionConfigData> for ConnectionConfig {
|
||||
driver_profile: data.driver_profile,
|
||||
driver_label: data.driver_label,
|
||||
url_params: data.url_params,
|
||||
agent_java_options: data.agent_java_options,
|
||||
host: data.host,
|
||||
port: data.port,
|
||||
username: data.username,
|
||||
@@ -1790,6 +1795,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "10.1.2.3".to_string(),
|
||||
port: 2883,
|
||||
username: username.to_string(),
|
||||
@@ -1844,6 +1850,23 @@ mod tests {
|
||||
assert_eq!(serde_json::from_str::<DatabaseType>("\"zookeeper\"").unwrap(), DatabaseType::ZooKeeper);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connection_config_defaults_missing_agent_java_options_to_empty() {
|
||||
let config: ConnectionConfig = serde_json::from_value(serde_json::json!({
|
||||
"id": "id",
|
||||
"name": "Hive",
|
||||
"db_type": "hive",
|
||||
"host": "hive.local",
|
||||
"port": 10000,
|
||||
"username": "",
|
||||
"password": "",
|
||||
"database": "default"
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
assert!(config.agent_java_options.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zookeeper_connection_url_uses_zookeeper_scheme() {
|
||||
let mut config = mysql_config("", "", None);
|
||||
|
||||
@@ -105,6 +105,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: String::new(),
|
||||
port: 0,
|
||||
username: String::new(),
|
||||
|
||||
@@ -553,6 +553,7 @@ mod tests {
|
||||
driver_profile: Some("pulsar".to_string()),
|
||||
driver_label: Some("Apache Pulsar".to_string()),
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 8080,
|
||||
username: String::new(),
|
||||
|
||||
@@ -130,6 +130,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 8848,
|
||||
username: String::new(),
|
||||
|
||||
@@ -174,6 +174,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 8848,
|
||||
username: String::new(),
|
||||
@@ -238,6 +239,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 8848,
|
||||
username: String::new(),
|
||||
|
||||
@@ -2890,6 +2890,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "localhost".to_string(),
|
||||
port: 0,
|
||||
username: String::new(),
|
||||
@@ -3519,6 +3520,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "localhost".to_string(),
|
||||
port: 0,
|
||||
username: String::new(),
|
||||
|
||||
@@ -2091,6 +2091,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 5432,
|
||||
username: "user".to_string(),
|
||||
|
||||
@@ -151,6 +151,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 5432,
|
||||
username: "user".to_string(),
|
||||
|
||||
@@ -2415,6 +2415,7 @@ mod tests {
|
||||
driver_profile: Some("pulsar".to_string()),
|
||||
driver_label: Some("Apache Pulsar".to_string()),
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 8080,
|
||||
username: String::new(),
|
||||
@@ -2471,6 +2472,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 8848,
|
||||
username: "nacos".to_string(),
|
||||
|
||||
@@ -4298,6 +4298,7 @@ mod tests {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: ":memory:".to_string(),
|
||||
port: 0,
|
||||
username: String::new(),
|
||||
|
||||
@@ -22,6 +22,7 @@ fn live_postgres_config(
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: host.to_string(),
|
||||
port,
|
||||
username: user.to_string(),
|
||||
|
||||
@@ -16,6 +16,7 @@ fn postgres_test_config(id: &str, database: &str) -> ConnectionConfig {
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 5432,
|
||||
username: "postgres".to_string(),
|
||||
|
||||
@@ -16,6 +16,7 @@ fn live_sqlserver_config(id: &str, database: &str) -> dbx_core::models::connecti
|
||||
driver_profile: None,
|
||||
driver_label: None,
|
||||
url_params: None,
|
||||
agent_java_options: Vec::new(),
|
||||
host: std::env::var("DBX_LIVE_SQLSERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()),
|
||||
port: std::env::var("DBX_LIVE_SQLSERVER_PORT").ok().and_then(|value| value.parse().ok()).unwrap_or(1433),
|
||||
username: std::env::var("DBX_LIVE_SQLSERVER_USER").unwrap_or_else(|_| "sa".to_string()),
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { test } from "vitest";
|
||||
import assert from "node:assert/strict";
|
||||
import { applyHiveKerberosSubmitConfig, hiveKerberosFormConfig } from "../../apps/desktop/src/lib/database/hiveKerberosOptions.ts";
|
||||
|
||||
test("adds Hive Kerberos URL and JVM options while preserving unrelated params", () => {
|
||||
const result = applyHiveKerberosSubmitConfig({
|
||||
authMode: "kerberos",
|
||||
principal: "hive/_HOST@EXAMPLE.COM",
|
||||
krb5ConfPath: "C:\\ProgramData\\MIT\\Kerberos5\\krb5.ini",
|
||||
jaasConfigPath: "C:\\dbx\\hive-jaas.conf",
|
||||
useSubjectCredsOnlyFalse: true,
|
||||
extraJavaOptions: "-Dsun.security.krb5.debug=true",
|
||||
urlParams: ";serviceDiscoveryMode=zooKeeper;principal=hive/old@EXAMPLE.COM",
|
||||
});
|
||||
|
||||
assert.equal(result.urlParams, "serviceDiscoveryMode=zooKeeper;principal=hive/_HOST@EXAMPLE.COM");
|
||||
assert.deepEqual(result.agentJavaOptions, ["-Djava.security.krb5.conf=C:\\ProgramData\\MIT\\Kerberos5\\krb5.ini", "-Djava.security.auth.login.config=C:\\dbx\\hive-jaas.conf", "-Djavax.security.auth.useSubjectCredsOnly=false", "-Dsun.security.krb5.debug=true"]);
|
||||
});
|
||||
|
||||
test("disabling Hive Kerberos removes generated URL params but keeps extra JVM options", () => {
|
||||
const result = applyHiveKerberosSubmitConfig({
|
||||
authMode: "none",
|
||||
principal: "hive/_HOST@EXAMPLE.COM",
|
||||
krb5ConfPath: "/etc/krb5.conf",
|
||||
jaasConfigPath: "/etc/hive-jaas.conf",
|
||||
useSubjectCredsOnlyFalse: true,
|
||||
extraJavaOptions: "-Dcustom.flag=true",
|
||||
urlParams: "auth=kerberos;principal=hive/_HOST@EXAMPLE.COM;transportMode=http",
|
||||
});
|
||||
|
||||
assert.equal(result.urlParams, "transportMode=http");
|
||||
assert.deepEqual(result.agentJavaOptions, ["-Dcustom.flag=true"]);
|
||||
});
|
||||
|
||||
test("preserves non Kerberos Hive auth params when Kerberos is disabled", () => {
|
||||
const result = applyHiveKerberosSubmitConfig({
|
||||
authMode: "none",
|
||||
principal: "",
|
||||
krb5ConfPath: "",
|
||||
jaasConfigPath: "",
|
||||
useSubjectCredsOnlyFalse: false,
|
||||
extraJavaOptions: "",
|
||||
urlParams: "auth=noSasl;transportMode=http",
|
||||
});
|
||||
|
||||
assert.equal(result.urlParams, "auth=noSasl;transportMode=http");
|
||||
});
|
||||
|
||||
test("hydrates Hive Kerberos form values from saved URL and JVM options", () => {
|
||||
const form = hiveKerberosFormConfig("principal=hive/_HOST@EXAMPLE.COM", ["-Djava.security.krb5.conf=/etc/krb5.conf", "-Djava.security.auth.login.config=/etc/hive-jaas.conf", "-Djavax.security.auth.useSubjectCredsOnly=false", "-Dsun.security.krb5.debug=true"]);
|
||||
|
||||
assert.deepEqual(form, {
|
||||
authMode: "kerberos",
|
||||
principal: "hive/_HOST@EXAMPLE.COM",
|
||||
krb5ConfPath: "/etc/krb5.conf",
|
||||
jaasConfigPath: "/etc/hive-jaas.conf",
|
||||
useSubjectCredsOnlyFalse: true,
|
||||
extraJavaOptions: "-Dsun.security.krb5.debug=true",
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user