feat(kafka): improve topic management and Kerberos auth
This commit is contained in:
18 files changed
+1096
-81
No files matched your search
@@ -40,6 +40,7 @@ import { SQLITE_DATABASE_FILE_EXTENSIONS } from "@/lib/database/databaseFileDete
|
||||
import { connectionAttemptOriginalErrorMessage, connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs } from "@/lib/connection/connectionAttemptTimeout";
|
||||
import { appendConnectionErrorHints } from "@/lib/connection/connectionErrorHints";
|
||||
import { normalizeKafkaBootstrapServers } from "@/lib/connection/kafkaBootstrapServers";
|
||||
import { detectMqUiAuthKind, isMqAuthKindAllowedForSystem, type MqUiAuthKind } from "@/lib/connection/mqAuth";
|
||||
import { driverInstallProgressPercent, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
|
||||
import { ArrowLeft, ArrowDown, ArrowUp, CheckSquare, ChevronRight, CircleHelp, Copy, ExternalLink, FilePlus2, FolderOpen, GripVertical, Grid3X3, KeyRound, Link2, List, ListFilter, Loader2, Pencil, Pipette, Plus, Search, ShieldCheck, Square, Trash2 } from "@lucide/vue";
|
||||
import { buildDraftVisibleDatabasesConnectionId, connectionCanChooseVisibleDatabases, initialVisibleDatabaseSelection, visibleDatabaseSelectionIsStale } from "@/lib/connection/connectionVisibleDatabases";
|
||||
@@ -390,14 +391,17 @@ const dialogStep = ref<DialogStep>("select");
|
||||
const dbPickerView = ref<DbPickerView>("icon");
|
||||
const dbSearchQuery = ref("");
|
||||
const configTab = ref<ConfigTab>("connection");
|
||||
type MqAuthKind = MqAuth["kind"];
|
||||
const MQ_KAFKA_SECURITY_PROTOCOL_AUTO = "__auto";
|
||||
const mqAdminUrl = ref("http://127.0.0.1:8080");
|
||||
const mqSystemKind = ref<MqSystemKind>("pulsar");
|
||||
const mqKafkaBootstrapServers = ref("127.0.0.1:9092");
|
||||
const mqKafkaSecurityProtocol = ref(MQ_KAFKA_SECURITY_PROTOCOL_AUTO);
|
||||
const mqKafkaSaslMechanism = ref("PLAIN");
|
||||
const mqAuthKind = ref<MqAuthKind>("none");
|
||||
const mqKafkaKerberosPrincipal = ref("");
|
||||
const mqKafkaKerberosKeytabPath = ref("");
|
||||
const mqKafkaKerberosServiceName = ref("kafka");
|
||||
const mqKafkaKrb5ConfPath = ref("");
|
||||
const mqAuthKind = ref<MqUiAuthKind>("none");
|
||||
const mqToken = ref("");
|
||||
const mqBasicUsername = ref("");
|
||||
const mqBasicPassword = ref("");
|
||||
@@ -693,18 +697,50 @@ function mqExtraString(extra: Record<string, unknown>, key: string): string {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function mqExtraProperties(extra: Record<string, unknown>): Record<string, unknown> {
|
||||
const value = extra.properties;
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
||||
}
|
||||
|
||||
function mqExtraPropertyString(extra: Record<string, unknown>, key: string): string {
|
||||
const value = mqExtraProperties(extra)[key];
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function jaasStringValue(value: string): string {
|
||||
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
||||
}
|
||||
|
||||
function parseJaasStringProperty(value: string, key: string): string {
|
||||
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const match = value.match(new RegExp(`${escapedKey}\\s*=\\s*"((?:\\\\.|[^"\\\\])*)"`, "i"));
|
||||
if (!match) return "";
|
||||
return match[1].replace(/\\(["\\])/g, "$1");
|
||||
}
|
||||
|
||||
function resetMqFields(config?: Partial<MqAdminConfig>) {
|
||||
const systemKind = config?.systemKind === "kafka" ? "kafka" : "pulsar";
|
||||
const extra = mqExtraRecord(config);
|
||||
const properties = mqExtraProperties(extra);
|
||||
const jaasConfig = mqExtraPropertyString(extra, "sasl.jaas.config");
|
||||
mqSystemKind.value = systemKind;
|
||||
mqAdminUrl.value = config?.adminUrl?.trim() || (systemKind === "kafka" ? "" : "http://127.0.0.1:8080");
|
||||
mqKafkaBootstrapServers.value = mqExtraString(extra, "bootstrapServers") || "127.0.0.1:9092";
|
||||
mqKafkaSecurityProtocol.value = mqExtraString(extra, "securityProtocol") || MQ_KAFKA_SECURITY_PROTOCOL_AUTO;
|
||||
mqKafkaSaslMechanism.value = mqExtraString(extra, "saslMechanism") || "PLAIN";
|
||||
mqKafkaKerberosPrincipal.value = parseJaasStringProperty(jaasConfig, "principal");
|
||||
mqKafkaKerberosKeytabPath.value = parseJaasStringProperty(jaasConfig, "keyTab");
|
||||
mqKafkaKerberosServiceName.value = typeof properties["sasl.kerberos.service.name"] === "string" ? properties["sasl.kerberos.service.name"] : "kafka";
|
||||
mqKafkaKrb5ConfPath.value = typeof properties["java.security.krb5.conf"] === "string" ? properties["java.security.krb5.conf"] : "";
|
||||
mqTlsSkipVerify.value = !!config?.tlsSkipVerify;
|
||||
mqPinnedVersion.value = pinnedVersionToSelection(config?.pinnedVersion);
|
||||
const auth = (config?.auth || { kind: "none" }) as MqAuth;
|
||||
mqAuthKind.value = systemKind === "kafka" && auth.kind !== "basic" ? "none" : auth.kind || "none";
|
||||
mqAuthKind.value = detectMqUiAuthKind({
|
||||
systemKind,
|
||||
authKind: auth.kind,
|
||||
saslMechanism: mqKafkaSaslMechanism.value,
|
||||
jaasConfig,
|
||||
});
|
||||
mqToken.value = auth.token || "";
|
||||
mqBasicUsername.value = auth.username || "";
|
||||
mqBasicPassword.value = auth.password || "";
|
||||
@@ -741,12 +777,18 @@ function hydrateMqFields(value: unknown) {
|
||||
watch(mqSystemKind, (kind) => {
|
||||
if (kind === "kafka") {
|
||||
if (!mqKafkaBootstrapServers.value.trim()) mqKafkaBootstrapServers.value = "127.0.0.1:9092";
|
||||
if (!["none", "basic"].includes(mqAuthKind.value)) mqAuthKind.value = "none";
|
||||
if (!isMqAuthKindAllowedForSystem(kind, mqAuthKind.value)) mqAuthKind.value = "none";
|
||||
return;
|
||||
}
|
||||
if (!mqAdminUrl.value.trim()) mqAdminUrl.value = "http://127.0.0.1:8080";
|
||||
});
|
||||
|
||||
watch(mqAuthKind, (kind) => {
|
||||
if (mqSystemKind.value === "kafka" && kind === "basic" && mqKafkaSaslMechanism.value.toUpperCase() === "GSSAPI") {
|
||||
mqKafkaSaslMechanism.value = "PLAIN";
|
||||
}
|
||||
});
|
||||
|
||||
function resetNacosFields(config?: Partial<NacosAdminConfig>) {
|
||||
nacosServerAddr.value = config?.serverAddr?.trim() || NACOS_DEFAULT_CONSOLE_URL;
|
||||
nacosNamespace.value = config?.namespace || "";
|
||||
@@ -845,6 +887,12 @@ function buildMqAuth(): MqAuth {
|
||||
}
|
||||
}
|
||||
|
||||
function buildKafkaKerberosJaasConfig(): string {
|
||||
const principal = requireMqField(mqKafkaKerberosPrincipal.value, t("connection.kafkaKerberosPrincipalRequired"));
|
||||
const keytab = requireMqField(mqKafkaKerberosKeytabPath.value, t("connection.kafkaKerberosKeytabRequired"));
|
||||
return `com.sun.security.auth.module.Krb5LoginModule required useKeyTab=true storeKey=true keyTab="${jaasStringValue(keytab)}" principal="${jaasStringValue(principal)}";`;
|
||||
}
|
||||
|
||||
function buildMqTokenSigning() {
|
||||
if (mqTokenSigningMode.value === "none") return undefined;
|
||||
return {
|
||||
@@ -857,11 +905,21 @@ function buildMqAdminConfig(): MqAdminConfig {
|
||||
const systemKind = mqSystemKind.value;
|
||||
if (systemKind === "kafka") {
|
||||
const bootstrapServers = normalizeKafkaBootstrapServers(mqKafkaBootstrapServers.value);
|
||||
const extra: Record<string, string> = { bootstrapServers };
|
||||
const extra: Record<string, unknown> = { bootstrapServers };
|
||||
const securityProtocol = mqKafkaSecurityProtocol.value === MQ_KAFKA_SECURITY_PROTOCOL_AUTO ? "" : mqKafkaSecurityProtocol.value.trim();
|
||||
const saslMechanism = mqKafkaSaslMechanism.value.trim();
|
||||
const saslMechanism = mqAuthKind.value === "kerberos" ? "GSSAPI" : mqKafkaSaslMechanism.value.trim();
|
||||
const properties: Record<string, string> = {};
|
||||
if (securityProtocol) extra.securityProtocol = securityProtocol;
|
||||
if (mqAuthKind.value === "basic" && saslMechanism) extra.saslMechanism = saslMechanism;
|
||||
if (mqAuthKind.value === "kerberos") {
|
||||
extra.saslMechanism = "GSSAPI";
|
||||
properties["sasl.jaas.config"] = buildKafkaKerberosJaasConfig();
|
||||
properties["sasl.kerberos.service.name"] = mqKafkaKerberosServiceName.value.trim() || "kafka";
|
||||
if (mqKafkaKrb5ConfPath.value.trim()) {
|
||||
properties["java.security.krb5.conf"] = mqKafkaKrb5ConfPath.value.trim();
|
||||
}
|
||||
}
|
||||
if (Object.keys(properties).length) extra.properties = properties;
|
||||
return {
|
||||
systemKind: mqSystemKind.value,
|
||||
adminUrl: "",
|
||||
@@ -3908,6 +3966,7 @@ function openExternalUrl(url: string) {
|
||||
<Button size="sm" :variant="mqAuthKind === 'none' ? 'default' : 'outline'" @click="mqAuthKind = 'none'">None</Button>
|
||||
<Button v-if="mqSystemKind !== 'kafka'" size="sm" :variant="mqAuthKind === 'token' ? 'default' : 'outline'" @click="mqAuthKind = 'token'">Token</Button>
|
||||
<Button size="sm" :variant="mqAuthKind === 'basic' ? 'default' : 'outline'" @click="mqAuthKind = 'basic'">Basic</Button>
|
||||
<Button v-if="mqSystemKind === 'kafka'" size="sm" :variant="mqAuthKind === 'kerberos' ? 'default' : 'outline'" @click="mqAuthKind = 'kerberos'">Kerberos</Button>
|
||||
<Button v-if="mqSystemKind !== 'kafka'" size="sm" :variant="mqAuthKind === 'apiKey' ? 'default' : 'outline'" @click="mqAuthKind = 'apiKey'">API Key</Button>
|
||||
<Button v-if="mqSystemKind !== 'kafka'" size="sm" :variant="mqAuthKind === 'oauth2' ? 'default' : 'outline'" @click="mqAuthKind = 'oauth2'">OAuth2</Button>
|
||||
</div>
|
||||
@@ -3941,6 +4000,31 @@ function openExternalUrl(url: string) {
|
||||
</Select>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="mqSystemKind === 'kafka' && mqAuthKind === 'kerberos'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.kafkaKerberosPrincipal") }}</Label>
|
||||
<Input v-model="mqKafkaKerberosPrincipal" class="col-span-3" placeholder="user@EXAMPLE.COM" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.kafkaKerberosKeytab") }}</Label>
|
||||
<Input v-model="mqKafkaKerberosKeytabPath" class="col-span-3" :placeholder="t('connection.kafkaKerberosKeytabPlaceholder')" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.kafkaKerberosServiceName") }}</Label>
|
||||
<Input v-model="mqKafkaKerberosServiceName" class="col-span-3" placeholder="kafka" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.kafkaKerberosKrb5Conf") }}</Label>
|
||||
<Input v-model="mqKafkaKrb5ConfPath" class="col-span-3" :placeholder="t('connection.kafkaKerberosKrb5ConfPlaceholder')" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<div></div>
|
||||
<div class="col-span-3 space-y-1 text-xs leading-5 text-muted-foreground">
|
||||
<p>{{ t("connection.kafkaKerberosPathHint") }}</p>
|
||||
<p>{{ t("connection.kafkaKerberosAuthHint") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="mqAuthKind === 'apiKey'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">Header</Label>
|
||||
|
||||
Reference in new issue
Block a user