feat(starrocks): enable user and privilege management
This commit is contained in:
1 parent
640e82cdc6
commit
41e0989f96
7 files changed
+248
-60
No files matched your search
@@ -14,7 +14,7 @@ import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
|
|||||||
import type { ConnectionConfig } from "@/types/database";
|
import type { ConnectionConfig } from "@/types/database";
|
||||||
import * as api from "@/lib/backend/api";
|
import * as api from "@/lib/backend/api";
|
||||||
import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
|
import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
|
||||||
import { grantsFromQueryResult, getDatabaseUserAdminProvider, supportsDatabaseUserAdmin, type DatabaseUserIdentity, type PrivilegeScope } from "@/lib/database/databaseUserAdmin";
|
import { grantsFromQueryResult, resolveDatabaseUserAdminProviderForConnection, type DatabaseUserIdentity, type PrivilegeScope } from "@/lib/database/databaseUserAdmin";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
connection: ConnectionConfig;
|
connection: ConnectionConfig;
|
||||||
@@ -54,9 +54,16 @@ const grantOption = ref(false);
|
|||||||
const selectedPrivileges = ref<string[]>(["SELECT"]);
|
const selectedPrivileges = ref<string[]>(["SELECT"]);
|
||||||
const createCanLogin = ref(true);
|
const createCanLogin = ref(true);
|
||||||
|
|
||||||
const supported = computed(() => supportsDatabaseUserAdmin(props.connection.db_type));
|
const provider = computed(() => resolveDatabaseUserAdminProviderForConnection(props.connection));
|
||||||
const provider = computed(() => getDatabaseUserAdminProvider(props.connection.db_type));
|
const supported = computed(() => provider.value !== null);
|
||||||
const isPostgres = computed(() => provider.value?.dialect === "postgres");
|
const isPostgres = computed(() => provider.value?.dialect === "postgres");
|
||||||
|
const canCreateUser = computed(() => !!provider.value?.createUserSql);
|
||||||
|
const canAlterPassword = computed(() => !!provider.value?.alterPasswordSql);
|
||||||
|
const canAlterLogin = computed(() => !!provider.value?.alterLoginSql);
|
||||||
|
const canDropUser = computed(() => !!provider.value?.dropUserSql);
|
||||||
|
const canGrantPrivileges = computed(() => !!provider.value?.grantPrivilegesSql);
|
||||||
|
const canRevokePrivileges = computed(() => !!provider.value?.revokePrivilegesSql);
|
||||||
|
const canEditPrivileges = computed(() => canGrantPrivileges.value || canRevokePrivileges.value);
|
||||||
const selectedUser = computed(() => users.value.find((user) => userKey(user) === selectedUserKey.value));
|
const selectedUser = computed(() => users.value.find((user) => userKey(user) === selectedUserKey.value));
|
||||||
const filteredUsers = computed(() => {
|
const filteredUsers = computed(() => {
|
||||||
const query = search.value.trim().toLowerCase();
|
const query = search.value.trim().toLowerCase();
|
||||||
@@ -64,7 +71,7 @@ const filteredUsers = computed(() => {
|
|||||||
return users.value.filter((user) => userLabel(user).toLowerCase().includes(query));
|
return users.value.filter((user) => userLabel(user).toLowerCase().includes(query));
|
||||||
});
|
});
|
||||||
const selectedPrivilegeSet = computed(() => new Set(selectedPrivileges.value));
|
const selectedPrivilegeSet = computed(() => new Set(selectedPrivileges.value));
|
||||||
const availablePrivileges = computed(() => provider.value?.privilegesForScope(privilegeScope.value) ?? []);
|
const availablePrivileges = computed(() => provider.value?.privilegesForScope?.(privilegeScope.value) ?? []);
|
||||||
const hasPrivilegePicker = computed(() => privilegeScope.value !== "role");
|
const hasPrivilegePicker = computed(() => privilegeScope.value !== "role");
|
||||||
const loginDisableLabel = computed(() => (isPostgres.value ? t("userAdmin.disableLogin") : t("userAdmin.lock")));
|
const loginDisableLabel = computed(() => (isPostgres.value ? t("userAdmin.disableLogin") : t("userAdmin.lock")));
|
||||||
const loginEnableLabel = computed(() => (isPostgres.value ? t("userAdmin.enableLogin") : t("userAdmin.unlock")));
|
const loginEnableLabel = computed(() => (isPostgres.value ? t("userAdmin.enableLogin") : t("userAdmin.unlock")));
|
||||||
@@ -135,7 +142,7 @@ async function loadGrants() {
|
|||||||
const result = await api.executeQuery(props.connection.id, "", userProvider.showGrantsSql(user), undefined, undefined, {
|
const result = await api.executeQuery(props.connection.id, "", userProvider.showGrantsSql(user), undefined, undefined, {
|
||||||
maxRows: 1000,
|
maxRows: 1000,
|
||||||
});
|
});
|
||||||
grants.value = grantsFromQueryResult(result);
|
grants.value = (userProvider.parseGrants ?? grantsFromQueryResult)(result);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
grantError.value = error?.message || String(error);
|
grantError.value = error?.message || String(error);
|
||||||
grants.value = [];
|
grants.value = [];
|
||||||
@@ -188,10 +195,11 @@ async function applyPendingSql() {
|
|||||||
|
|
||||||
function previewCreateUser() {
|
function previewCreateUser() {
|
||||||
const userProvider = provider.value;
|
const userProvider = provider.value;
|
||||||
if (!userProvider) return;
|
const createUserSql = userProvider?.createUserSql;
|
||||||
|
if (!createUserSql) return;
|
||||||
if (!createUser.value.trim() || !createPassword.value) return;
|
if (!createUser.value.trim() || !createPassword.value) return;
|
||||||
previewSql(
|
previewSql(
|
||||||
userProvider.createUserSql({
|
createUserSql({
|
||||||
user: createUser.value.trim(),
|
user: createUser.value.trim(),
|
||||||
host: createHost.value.trim() || "%",
|
host: createHost.value.trim() || "%",
|
||||||
password: createPassword.value,
|
password: createPassword.value,
|
||||||
@@ -209,8 +217,9 @@ function previewCreateUser() {
|
|||||||
function previewPasswordChange() {
|
function previewPasswordChange() {
|
||||||
const user = selectedUser.value;
|
const user = selectedUser.value;
|
||||||
const userProvider = provider.value;
|
const userProvider = provider.value;
|
||||||
if (!user || !userProvider || !newPassword.value) return;
|
const alterPasswordSql = userProvider?.alterPasswordSql;
|
||||||
previewSql(userProvider.alterPasswordSql(user, newPassword.value), {
|
if (!user || !alterPasswordSql || !newPassword.value) return;
|
||||||
|
previewSql(alterPasswordSql(user, newPassword.value), {
|
||||||
danger: true,
|
danger: true,
|
||||||
afterApply: async () => {
|
afterApply: async () => {
|
||||||
passwordDialogOpen.value = false;
|
passwordDialogOpen.value = false;
|
||||||
@@ -222,23 +231,26 @@ function previewPasswordChange() {
|
|||||||
function previewDropUser() {
|
function previewDropUser() {
|
||||||
const user = selectedUser.value;
|
const user = selectedUser.value;
|
||||||
const userProvider = provider.value;
|
const userProvider = provider.value;
|
||||||
if (!user || !userProvider) return;
|
const dropUserSql = userProvider?.dropUserSql;
|
||||||
previewSql(userProvider.dropUserSql(user), { danger: true });
|
if (!user || !dropUserSql) return;
|
||||||
|
previewSql(dropUserSql(user), { danger: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function previewLoginChange(enabled: boolean) {
|
function previewLoginChange(enabled: boolean) {
|
||||||
const user = selectedUser.value;
|
const user = selectedUser.value;
|
||||||
const userProvider = provider.value;
|
const userProvider = provider.value;
|
||||||
if (!user || !userProvider) return;
|
const alterLoginSql = userProvider?.alterLoginSql;
|
||||||
previewSql(userProvider.alterLoginSql(user, enabled), { danger: true });
|
if (!user || !alterLoginSql) return;
|
||||||
|
previewSql(alterLoginSql(user, enabled), { danger: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function previewGrant() {
|
function previewGrant() {
|
||||||
const user = selectedUser.value;
|
const user = selectedUser.value;
|
||||||
const userProvider = provider.value;
|
const userProvider = provider.value;
|
||||||
if (!user || !userProvider || (privilegeScope.value === "role" && !privilegeRole.value.trim())) return;
|
const grantPrivilegesSql = userProvider?.grantPrivilegesSql;
|
||||||
|
if (!user || !grantPrivilegesSql || (privilegeScope.value === "role" && !privilegeRole.value.trim())) return;
|
||||||
previewSql(
|
previewSql(
|
||||||
userProvider.grantPrivilegesSql({
|
grantPrivilegesSql({
|
||||||
user,
|
user,
|
||||||
privileges: selectedPrivileges.value,
|
privileges: selectedPrivileges.value,
|
||||||
database: privilegeDatabase.value,
|
database: privilegeDatabase.value,
|
||||||
@@ -253,9 +265,10 @@ function previewGrant() {
|
|||||||
function previewRevoke() {
|
function previewRevoke() {
|
||||||
const user = selectedUser.value;
|
const user = selectedUser.value;
|
||||||
const userProvider = provider.value;
|
const userProvider = provider.value;
|
||||||
if (!user || !userProvider || (privilegeScope.value === "role" && !privilegeRole.value.trim())) return;
|
const revokePrivilegesSql = userProvider?.revokePrivilegesSql;
|
||||||
|
if (!user || !revokePrivilegesSql || (privilegeScope.value === "role" && !privilegeRole.value.trim())) return;
|
||||||
previewSql(
|
previewSql(
|
||||||
userProvider.revokePrivilegesSql({
|
revokePrivilegesSql({
|
||||||
user,
|
user,
|
||||||
privileges: selectedPrivileges.value,
|
privileges: selectedPrivileges.value,
|
||||||
database: privilegeDatabase.value,
|
database: privilegeDatabase.value,
|
||||||
@@ -270,7 +283,7 @@ function previewRevoke() {
|
|||||||
function resetPrivilegeDefaults(scope: PrivilegeScope) {
|
function resetPrivilegeDefaults(scope: PrivilegeScope) {
|
||||||
const userProvider = provider.value;
|
const userProvider = provider.value;
|
||||||
if (!userProvider) return;
|
if (!userProvider) return;
|
||||||
selectedPrivileges.value = userProvider.defaultPrivilegesForScope(scope);
|
selectedPrivileges.value = userProvider.defaultPrivilegesForScope?.(scope) ?? [];
|
||||||
if (userProvider.dialect === "postgres") {
|
if (userProvider.dialect === "postgres") {
|
||||||
if (scope === "database") privilegeDatabase.value = props.connection.database || "postgres";
|
if (scope === "database") privilegeDatabase.value = props.connection.database || "postgres";
|
||||||
if (scope === "schema" || scope === "table") privilegeDatabase.value = "public";
|
if (scope === "schema" || scope === "table") privilegeDatabase.value = "public";
|
||||||
@@ -296,7 +309,7 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => provider.value?.dialect,
|
() => provider.value,
|
||||||
() => {
|
() => {
|
||||||
privilegeScope.value = provider.value?.defaultScope ?? "mysql";
|
privilegeScope.value = provider.value?.defaultScope ?? "mysql";
|
||||||
resetPrivilegeDefaults(privilegeScope.value);
|
resetPrivilegeDefaults(privilegeScope.value);
|
||||||
@@ -326,7 +339,7 @@ onMounted(loadUsers);
|
|||||||
<RefreshCcw v-else class="h-3.5 w-3.5" />
|
<RefreshCcw v-else class="h-3.5 w-3.5" />
|
||||||
{{ t("grid.refresh") }}
|
{{ t("grid.refresh") }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" class="h-7 gap-1.5 px-2 text-xs" :disabled="!supported" @click="createDialogOpen = true">
|
<Button v-if="canCreateUser" size="sm" class="h-7 gap-1.5 px-2 text-xs" @click="createDialogOpen = true">
|
||||||
<Plus class="h-3.5 w-3.5" />
|
<Plus class="h-3.5 w-3.5" />
|
||||||
{{ t("userAdmin.newUser") }}
|
{{ t("userAdmin.newUser") }}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -385,27 +398,27 @@ onMounted(loadUsers);
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-auto flex items-center gap-1.5">
|
<div class="ml-auto flex items-center gap-1.5">
|
||||||
<Button variant="outline" size="sm" class="h-7 gap-1.5 px-2 text-xs" @click="passwordDialogOpen = true">
|
<Button v-if="canAlterPassword" variant="outline" size="sm" class="h-7 gap-1.5 px-2 text-xs" @click="passwordDialogOpen = true">
|
||||||
<KeyRound class="h-3.5 w-3.5" />
|
<KeyRound class="h-3.5 w-3.5" />
|
||||||
{{ t("userAdmin.changePassword") }}
|
{{ t("userAdmin.changePassword") }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm" class="h-7 gap-1.5 px-2 text-xs" @click="previewLoginChange(false)">
|
<Button v-if="canAlterLogin" variant="outline" size="sm" class="h-7 gap-1.5 px-2 text-xs" @click="previewLoginChange(false)">
|
||||||
<Lock class="h-3.5 w-3.5" />
|
<Lock class="h-3.5 w-3.5" />
|
||||||
{{ loginDisableLabel }}
|
{{ loginDisableLabel }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm" class="h-7 gap-1.5 px-2 text-xs" @click="previewLoginChange(true)">
|
<Button v-if="canAlterLogin" variant="outline" size="sm" class="h-7 gap-1.5 px-2 text-xs" @click="previewLoginChange(true)">
|
||||||
<Unlock class="h-3.5 w-3.5" />
|
<Unlock class="h-3.5 w-3.5" />
|
||||||
{{ loginEnableLabel }}
|
{{ loginEnableLabel }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="destructive" size="sm" class="h-7 gap-1.5 px-2 text-xs" @click="previewDropUser">
|
<Button v-if="canDropUser" variant="destructive" size="sm" class="h-7 gap-1.5 px-2 text-xs" @click="previewDropUser">
|
||||||
<Trash2 class="h-3.5 w-3.5" />
|
<Trash2 class="h-3.5 w-3.5" />
|
||||||
{{ t("userAdmin.dropUser") }}
|
{{ t("userAdmin.dropUser") }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedUser" class="grid min-h-0 flex-1 grid-cols-[minmax(0,1fr)_320px]">
|
<div v-if="selectedUser" class="grid min-h-0 flex-1" :class="canEditPrivileges ? 'grid-cols-[minmax(0,1fr)_320px]' : 'grid-cols-1'">
|
||||||
<section class="flex min-h-0 flex-col border-r">
|
<section class="flex min-h-0 flex-col" :class="{ 'border-r': canEditPrivileges }">
|
||||||
<div class="flex h-9 shrink-0 items-center gap-2 border-b bg-muted/20 px-3 text-xs font-medium">
|
<div class="flex h-9 shrink-0 items-center gap-2 border-b bg-muted/20 px-3 text-xs font-medium">
|
||||||
<ShieldCheck class="h-3.5 w-3.5" />
|
<ShieldCheck class="h-3.5 w-3.5" />
|
||||||
{{ t("userAdmin.grants") }}
|
{{ t("userAdmin.grants") }}
|
||||||
@@ -420,7 +433,7 @@ onMounted(loadUsers);
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<aside class="flex min-h-0 flex-col bg-muted/10">
|
<aside v-if="canEditPrivileges" class="flex min-h-0 flex-col bg-muted/10">
|
||||||
<div class="border-b p-3">
|
<div class="border-b p-3">
|
||||||
<div class="text-xs font-semibold">{{ t("userAdmin.privilegeEditor") }}</div>
|
<div class="text-xs font-semibold">{{ t("userAdmin.privilegeEditor") }}</div>
|
||||||
<div class="mt-1 text-[11px] leading-4 text-muted-foreground">{{ t("userAdmin.privilegeHint") }}</div>
|
<div class="mt-1 text-[11px] leading-4 text-muted-foreground">{{ t("userAdmin.privilegeHint") }}</div>
|
||||||
@@ -478,10 +491,10 @@ onMounted(loadUsers);
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex shrink-0 items-center justify-end gap-2 border-t p-3">
|
<div class="flex shrink-0 items-center justify-end gap-2 border-t p-3">
|
||||||
<Button variant="outline" size="sm" class="h-7 px-2 text-xs" @click="previewRevoke">
|
<Button v-if="canRevokePrivileges" variant="outline" size="sm" class="h-7 px-2 text-xs" @click="previewRevoke">
|
||||||
{{ t("userAdmin.revoke") }}
|
{{ t("userAdmin.revoke") }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" class="h-7 px-2 text-xs" @click="previewGrant">
|
<Button v-if="canGrantPrivileges" size="sm" class="h-7 px-2 text-xs" @click="previewGrant">
|
||||||
{{ t("userAdmin.grant") }}
|
{{ t("userAdmin.grant") }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ import { sidebarDisplayTableName } from "@/lib/sidebar/sidebarTableNameDisplay";
|
|||||||
import { shouldMeasureSidebarLabelOverflow } from "@/lib/sidebar/sidebarLabelTooltip";
|
import { shouldMeasureSidebarLabelOverflow } from "@/lib/sidebar/sidebarLabelTooltip";
|
||||||
import { selectedTreeNodesInVisibleOrder as orderSelectedTreeNodes, treeSelectionRangeIdsByIndex, treeSelectionRangeIds } from "@/lib/sidebar/sidebarTreeSelection";
|
import { selectedTreeNodesInVisibleOrder as orderSelectedTreeNodes, treeSelectionRangeIdsByIndex, treeSelectionRangeIds } from "@/lib/sidebar/sidebarTreeSelection";
|
||||||
import { connectionPasteTargetGroupId, selectedConnectionClipboardTargets, selectedConnectionDeleteTargets, selectedConnectionDuplicateTargets, selectedConnectionEditTarget } from "@/lib/sidebar/sidebarConnectionSelection";
|
import { connectionPasteTargetGroupId, selectedConnectionClipboardTargets, selectedConnectionDeleteTargets, selectedConnectionDuplicateTargets, selectedConnectionEditTarget } from "@/lib/sidebar/sidebarConnectionSelection";
|
||||||
import { supportsDatabaseUserAdmin } from "@/lib/database/databaseUserAdmin";
|
import { connectionSupportsDatabaseUserAdmin } from "@/lib/database/databaseUserAdmin";
|
||||||
import { connectionSupportsProcessList } from "@/lib/database/processListDrivers";
|
import { connectionSupportsProcessList } from "@/lib/database/processListDrivers";
|
||||||
import { connectionSupportsServerDashboard } from "@/lib/database/mysqlServerStatus";
|
import { connectionSupportsServerDashboard } from "@/lib/database/mysqlServerStatus";
|
||||||
import { connectionSupportsServerDashboard as connectionSupportsPgServerDashboard } from "@/lib/database/postgresServerStatus";
|
import { connectionSupportsServerDashboard as connectionSupportsPgServerDashboard } from "@/lib/database/postgresServerStatus";
|
||||||
@@ -5128,7 +5128,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
|
|||||||
}
|
}
|
||||||
const sqlHistoryMenu = savedSqlHistorySubmenu();
|
const sqlHistoryMenu = savedSqlHistorySubmenu();
|
||||||
if (sqlHistoryMenu) items.push(sqlHistoryMenu);
|
if (sqlHistoryMenu) items.push(sqlHistoryMenu);
|
||||||
if (supportsDatabaseUserAdmin(currentDatabaseType())) {
|
if (node.connectionId && connectionSupportsDatabaseUserAdmin(connectionStore.getConfig(node.connectionId))) {
|
||||||
items.push({ label: t("contextMenu.userAdmin"), action: openUserAdmin, icon: UsersRound });
|
items.push({ label: t("contextMenu.userAdmin"), action: openUserAdmin, icon: UsersRound });
|
||||||
}
|
}
|
||||||
if (node.connectionId && connectionSupportsProcessList(connectionStore.getConfig(node.connectionId))) {
|
if (node.connectionId && connectionSupportsProcessList(connectionStore.getConfig(node.connectionId))) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { DatabaseType, QueryResult } from "@/types/database";
|
import type { ConnectionConfig, DatabaseType, QueryResult } from "@/types/database";
|
||||||
import { supportsDatabaseFeature } from "@/lib/database/databaseDriverManifest";
|
import { supportsDatabaseFeature } from "@/lib/database/databaseDriverManifest";
|
||||||
|
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||||
|
|
||||||
export type UserAdminDialect = "mysql" | "postgres";
|
export type UserAdminDialect = "mysql" | "postgres";
|
||||||
export type PrivilegeScope = "mysql" | "database" | "schema" | "table" | "role";
|
export type PrivilegeScope = "mysql" | "database" | "schema" | "table" | "role";
|
||||||
@@ -33,40 +34,26 @@ export interface DatabaseUserAdminProvider {
|
|||||||
parseUsers(result: QueryResult): DatabaseUserIdentity[];
|
parseUsers(result: QueryResult): DatabaseUserIdentity[];
|
||||||
parseFallbackUsers?: (result: QueryResult) => DatabaseUserIdentity[];
|
parseFallbackUsers?: (result: QueryResult) => DatabaseUserIdentity[];
|
||||||
showGrantsSql(user: DatabaseUserIdentity): string;
|
showGrantsSql(user: DatabaseUserIdentity): string;
|
||||||
createUserSql(input: CreatePrincipalInput): string;
|
parseGrants?(result: QueryResult): string[];
|
||||||
alterPasswordSql(user: DatabaseUserIdentity, password: string): string;
|
createUserSql?(input: CreatePrincipalInput): string;
|
||||||
alterLoginSql(user: DatabaseUserIdentity, enabled: boolean): string;
|
alterPasswordSql?(user: DatabaseUserIdentity, password: string): string;
|
||||||
dropUserSql(user: DatabaseUserIdentity): string;
|
alterLoginSql?(user: DatabaseUserIdentity, enabled: boolean): string;
|
||||||
grantPrivilegesSql(input: PrivilegeChangeInput): string;
|
dropUserSql?(user: DatabaseUserIdentity): string;
|
||||||
revokePrivilegesSql(input: PrivilegeChangeInput): string;
|
grantPrivilegesSql?(input: PrivilegeChangeInput): string;
|
||||||
|
revokePrivilegesSql?(input: PrivilegeChangeInput): string;
|
||||||
label(user: DatabaseUserIdentity): string;
|
label(user: DatabaseUserIdentity): string;
|
||||||
detail(user: DatabaseUserIdentity): string | undefined;
|
detail(user: DatabaseUserIdentity): string | undefined;
|
||||||
privilegesForScope(scope: PrivilegeScope): readonly string[];
|
privilegesForScope?(scope: PrivilegeScope): readonly string[];
|
||||||
defaultPrivilegesForScope(scope: PrivilegeScope): string[];
|
defaultPrivilegesForScope?(scope: PrivilegeScope): string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MYSQL_USER_ADMIN_TYPES = new Set<DatabaseType>(["mysql", "goldendb"]);
|
|
||||||
export const KINGBASE_USER_ADMIN_TYPES = new Set<DatabaseType>(["kingbase"]);
|
|
||||||
export const POSTGRES_USER_ADMIN_TYPES = new Set<DatabaseType>(["postgres", "gaussdb", "highgo", "kwdb", "opengauss", "questdb", "vastbase"]);
|
|
||||||
|
|
||||||
export const MYSQL_COMMON_PRIVILEGES = ["SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER", "INDEX", "REFERENCES", "EXECUTE", "SHOW VIEW", "TRIGGER", "EVENT", "CREATE TEMPORARY TABLES"] as const;
|
export const MYSQL_COMMON_PRIVILEGES = ["SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER", "INDEX", "REFERENCES", "EXECUTE", "SHOW VIEW", "TRIGGER", "EVENT", "CREATE TEMPORARY TABLES"] as const;
|
||||||
|
export const STARROCKS_TABLE_PRIVILEGES = ["SELECT", "INSERT", "UPDATE", "DELETE", "ALTER", "DROP", "EXPORT", "ALL"] as const;
|
||||||
|
|
||||||
export const POSTGRES_DATABASE_PRIVILEGES = ["CONNECT", "CREATE", "TEMPORARY"] as const;
|
export const POSTGRES_DATABASE_PRIVILEGES = ["CONNECT", "CREATE", "TEMPORARY"] as const;
|
||||||
export const POSTGRES_SCHEMA_PRIVILEGES = ["USAGE", "CREATE"] as const;
|
export const POSTGRES_SCHEMA_PRIVILEGES = ["USAGE", "CREATE"] as const;
|
||||||
export const POSTGRES_TABLE_PRIVILEGES = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER"] as const;
|
export const POSTGRES_TABLE_PRIVILEGES = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER"] as const;
|
||||||
|
|
||||||
export function supportsDatabaseUserAdmin(dbType: DatabaseType | undefined): boolean {
|
|
||||||
return !!dbType && supportsDatabaseFeature(dbType, "userAdmin") && !!getDatabaseUserAdminProvider(dbType);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDatabaseUserAdminProvider(dbType: DatabaseType | undefined): DatabaseUserAdminProvider | null {
|
|
||||||
if (!dbType) return null;
|
|
||||||
if (MYSQL_USER_ADMIN_TYPES.has(dbType)) return mysqlUserAdminProvider;
|
|
||||||
if (KINGBASE_USER_ADMIN_TYPES.has(dbType)) return kingbaseUserAdminProvider;
|
|
||||||
if (POSTGRES_USER_ADMIN_TYPES.has(dbType)) return postgresUserAdminProvider;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function quoteSqlString(value: string): string {
|
export function quoteSqlString(value: string): string {
|
||||||
return `'${value.replace(/'/g, "''")}'`;
|
return `'${value.replace(/'/g, "''")}'`;
|
||||||
}
|
}
|
||||||
@@ -103,6 +90,25 @@ export function mysqlListUsersFallbackSql(): string {
|
|||||||
return "SELECT DISTINCT GRANTEE AS grantee FROM information_schema.USER_PRIVILEGES ORDER BY GRANTEE;";
|
return "SELECT DISTINCT GRANTEE AS grantee FROM information_schema.USER_PRIVILEGES ORDER BY GRANTEE;";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function starrocksListUsersSql(): string {
|
||||||
|
return "SHOW USERS;";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function starrocksUsersResult(result: QueryResult): DatabaseUserIdentity[] {
|
||||||
|
const userIndex = columnIndex(result, "user", "User");
|
||||||
|
if (userIndex < 0) return [];
|
||||||
|
return result.rows.flatMap((row) => {
|
||||||
|
const parsed = parseMySqlGrantee(String(row[userIndex] ?? ""));
|
||||||
|
return parsed ? [parsed] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function starrocksGrantsResult(result: QueryResult): string[] {
|
||||||
|
const grantsIndex = columnIndex(result, "grants", "Grants");
|
||||||
|
if (grantsIndex < 0) return result.rows.map((row) => String(row[0] ?? "")).filter(Boolean);
|
||||||
|
return result.rows.map((row) => String(row[grantsIndex] ?? "")).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
export function mysqlShowGrantsSql(user: DatabaseUserIdentity): string {
|
export function mysqlShowGrantsSql(user: DatabaseUserIdentity): string {
|
||||||
return `SHOW GRANTS FOR ${mysqlUserAccount(user)};`;
|
return `SHOW GRANTS FOR ${mysqlUserAccount(user)};`;
|
||||||
}
|
}
|
||||||
@@ -142,6 +148,27 @@ export function mysqlRevokePrivilegesSql(input: PrivilegeChangeInput): string {
|
|||||||
return `REVOKE ${privileges} ON ${mysqlPrivilegeTargetSql(input.database, input.table)} FROM ${mysqlUserAccount(input.user)};`;
|
return `REVOKE ${privileges} ON ${mysqlPrivilegeTargetSql(input.database, input.table)} FROM ${mysqlUserAccount(input.user)};`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function starrocksPrivilegeTargetSql(database: string, table = "*"): string {
|
||||||
|
const db = database.trim();
|
||||||
|
const tbl = table.trim();
|
||||||
|
if (!tbl || tbl === "*") {
|
||||||
|
return !db || db === "*" ? "ALL TABLES IN ALL DATABASES" : `ALL TABLES IN DATABASE ${quoteMySqlIdentifier(db)}`;
|
||||||
|
}
|
||||||
|
const tableName = quoteMySqlIdentifier(tbl);
|
||||||
|
return !db || db === "*" ? `TABLE ${tableName}` : `TABLE ${quoteMySqlIdentifier(db)}.${tableName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function starrocksGrantPrivilegesSql(input: PrivilegeChangeInput): string {
|
||||||
|
const privileges = normalizePrivileges(input.privileges).join(", ");
|
||||||
|
const grantOption = input.grantOption ? " WITH GRANT OPTION" : "";
|
||||||
|
return `GRANT ${privileges} ON ${starrocksPrivilegeTargetSql(input.database, input.table)} TO USER ${mysqlUserAccount(input.user)}${grantOption};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function starrocksRevokePrivilegesSql(input: PrivilegeChangeInput): string {
|
||||||
|
const privileges = normalizePrivileges(input.privileges).join(", ");
|
||||||
|
return `REVOKE ${privileges} ON ${starrocksPrivilegeTargetSql(input.database, input.table)} FROM USER ${mysqlUserAccount(input.user)};`;
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizePrivileges(privileges: string[], fallback = "SELECT"): string[] {
|
export function normalizePrivileges(privileges: string[], fallback = "SELECT"): string[] {
|
||||||
const normalized = privileges.map((privilege) => privilege.trim().toUpperCase()).filter(Boolean);
|
const normalized = privileges.map((privilege) => privilege.trim().toUpperCase()).filter(Boolean);
|
||||||
return Array.from(new Set(normalized.length > 0 ? normalized : [fallback]));
|
return Array.from(new Set(normalized.length > 0 ? normalized : [fallback]));
|
||||||
@@ -466,3 +493,52 @@ export const kingbaseUserAdminProvider: DatabaseUserAdminProvider = {
|
|||||||
listUsersSql: kingbaseListRolesSql,
|
listUsersSql: kingbaseListRolesSql,
|
||||||
showGrantsSql: kingbaseShowGrantsSql,
|
showGrantsSql: kingbaseShowGrantsSql,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const starrocksUserAdminProvider: DatabaseUserAdminProvider = {
|
||||||
|
dialect: "mysql",
|
||||||
|
defaultScope: "table",
|
||||||
|
listUsersSql: starrocksListUsersSql,
|
||||||
|
parseUsers: starrocksUsersResult,
|
||||||
|
showGrantsSql: mysqlShowGrantsSql,
|
||||||
|
parseGrants: starrocksGrantsResult,
|
||||||
|
createUserSql: mysqlCreateUserSql,
|
||||||
|
alterPasswordSql: mysqlAlterUserPasswordSql,
|
||||||
|
dropUserSql: mysqlDropUserSql,
|
||||||
|
grantPrivilegesSql: starrocksGrantPrivilegesSql,
|
||||||
|
revokePrivilegesSql: starrocksRevokePrivilegesSql,
|
||||||
|
label: mysqlUserLabel,
|
||||||
|
detail: (user) => user.plugin,
|
||||||
|
privilegesForScope: () => STARROCKS_TABLE_PRIVILEGES,
|
||||||
|
defaultPrivilegesForScope: () => ["SELECT"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const DATABASE_USER_ADMIN_PROVIDER_BY_TYPE = new Map<DatabaseType, DatabaseUserAdminProvider>([
|
||||||
|
["mysql", mysqlUserAdminProvider],
|
||||||
|
["goldendb", mysqlUserAdminProvider],
|
||||||
|
["kingbase", kingbaseUserAdminProvider],
|
||||||
|
["postgres", postgresUserAdminProvider],
|
||||||
|
["gaussdb", postgresUserAdminProvider],
|
||||||
|
["highgo", postgresUserAdminProvider],
|
||||||
|
["kwdb", postgresUserAdminProvider],
|
||||||
|
["opengauss", postgresUserAdminProvider],
|
||||||
|
["questdb", postgresUserAdminProvider],
|
||||||
|
["vastbase", postgresUserAdminProvider],
|
||||||
|
["starrocks", starrocksUserAdminProvider],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function getDatabaseUserAdminProvider(dbType: DatabaseType | undefined): DatabaseUserAdminProvider | null {
|
||||||
|
return dbType ? (DATABASE_USER_ADMIN_PROVIDER_BY_TYPE.get(dbType) ?? null) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function supportsDatabaseUserAdmin(dbType: DatabaseType | undefined): boolean {
|
||||||
|
return !!dbType && supportsDatabaseFeature(dbType, "userAdmin") && DATABASE_USER_ADMIN_PROVIDER_BY_TYPE.has(dbType);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDatabaseUserAdminProviderForConnection(connection: ConnectionConfig | undefined): DatabaseUserAdminProvider | null {
|
||||||
|
const dbType = effectiveDatabaseTypeForConnection(connection);
|
||||||
|
return supportsDatabaseUserAdmin(dbType) ? getDatabaseUserAdminProvider(dbType) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function connectionSupportsDatabaseUserAdmin(connection: ConnectionConfig | undefined): boolean {
|
||||||
|
return resolveDatabaseUserAdminProviderForConnection(connection) !== null;
|
||||||
|
}
|
||||||
@@ -75,7 +75,7 @@ import { hasTreeNodeDatabaseContext, normalizeCataloglessDatabaseNodes, treeNode
|
|||||||
import { decodeSchemaTreeCache, encodeSchemaTreeCache } from "@/lib/metadata/schemaTreeCache";
|
import { decodeSchemaTreeCache, encodeSchemaTreeCache } from "@/lib/metadata/schemaTreeCache";
|
||||||
import { sortSidebarTreeChildrenForParent } from "@/lib/sidebar/sidebarNodeOrdering";
|
import { sortSidebarTreeChildrenForParent } from "@/lib/sidebar/sidebarNodeOrdering";
|
||||||
import { prunePinnedTreeNodeIdsForConnection } from "@/lib/app/pinnedTreeNodeIds";
|
import { prunePinnedTreeNodeIdsForConnection } from "@/lib/app/pinnedTreeNodeIds";
|
||||||
import { supportsDatabaseUserAdmin } from "@/lib/database/databaseUserAdmin";
|
import { connectionSupportsDatabaseUserAdmin } from "@/lib/database/databaseUserAdmin";
|
||||||
import { getTableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities";
|
import { getTableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities";
|
||||||
import { useSettingsStore } from "@/stores/settingsStore";
|
import { useSettingsStore } from "@/stores/settingsStore";
|
||||||
import { encodeSqlServerLinkedSchema, parseSqlServerLinkedSchema } from "@/lib/database/sqlServerLinkedServers";
|
import { encodeSqlServerLinkedSchema, parseSqlServerLinkedSchema } from "@/lib/database/sqlServerLinkedServers";
|
||||||
@@ -1027,7 +1027,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
|
|
||||||
function buildUserAdminNode(connectionId: string, existingConnectionNode?: TreeNode): TreeNode | undefined {
|
function buildUserAdminNode(connectionId: string, existingConnectionNode?: TreeNode): TreeNode | undefined {
|
||||||
const config = getConfig(connectionId);
|
const config = getConfig(connectionId);
|
||||||
if (!supportsDatabaseUserAdmin(effectiveDatabaseTypeForConnection(config))) return undefined;
|
if (!connectionSupportsDatabaseUserAdmin(config)) return undefined;
|
||||||
const existing = existingConnectionNode?.children?.find((child) => child.type === "user-admin");
|
const existing = existingConnectionNode?.children?.find((child) => child.type === "user-admin");
|
||||||
return {
|
return {
|
||||||
id: `${connectionId}:__user_admin`,
|
id: `${connectionId}:__user_admin`,
|
||||||
|
|||||||
@@ -525,7 +525,7 @@
|
|||||||
"databaseCreate": true,
|
"databaseCreate": true,
|
||||||
"fieldLineage": false,
|
"fieldLineage": false,
|
||||||
"sqlExplain": false,
|
"sqlExplain": false,
|
||||||
"userAdmin": false,
|
"userAdmin": true,
|
||||||
"driverManagement": false
|
"driverManagement": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -325,6 +325,9 @@ fn driver_manifest_declares_expected_product_capabilities() {
|
|||||||
assert!(zookeeper.capabilities.query_execution);
|
assert!(zookeeper.capabilities.query_execution);
|
||||||
assert!(zookeeper.capabilities.driver_management);
|
assert!(zookeeper.capabilities.driver_management);
|
||||||
assert!(!zookeeper.capabilities.metadata_browse);
|
assert!(!zookeeper.capabilities.metadata_browse);
|
||||||
|
|
||||||
|
let starrocks = find_driver(DatabaseType::StarRocks);
|
||||||
|
assert!(starrocks.capabilities.user_admin);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+97
-1
@@ -1,7 +1,9 @@
|
|||||||
import { strict as assert } from "node:assert";
|
import { strict as assert } from "node:assert";
|
||||||
import { test } from "vitest";
|
import { test } from "vitest";
|
||||||
import {
|
import {
|
||||||
|
connectionSupportsDatabaseUserAdmin,
|
||||||
grantsFromQueryResult,
|
grantsFromQueryResult,
|
||||||
|
getDatabaseUserAdminProvider,
|
||||||
mysqlAlterUserAccountLockSql,
|
mysqlAlterUserAccountLockSql,
|
||||||
mysqlAlterUserPasswordSql,
|
mysqlAlterUserPasswordSql,
|
||||||
mysqlCreateUserSql,
|
mysqlCreateUserSql,
|
||||||
@@ -25,11 +27,18 @@ import {
|
|||||||
quoteMySqlIdentifier,
|
quoteMySqlIdentifier,
|
||||||
quoteMySqlString,
|
quoteMySqlString,
|
||||||
quotePostgresIdentifier,
|
quotePostgresIdentifier,
|
||||||
|
resolveDatabaseUserAdminProviderForConnection,
|
||||||
|
starrocksGrantPrivilegesSql,
|
||||||
|
starrocksGrantsResult,
|
||||||
|
starrocksListUsersSql,
|
||||||
|
starrocksPrivilegeTargetSql,
|
||||||
|
starrocksRevokePrivilegesSql,
|
||||||
|
starrocksUsersResult,
|
||||||
usersFromMySqlGranteeResult,
|
usersFromMySqlGranteeResult,
|
||||||
usersFromMySqlUserResult,
|
usersFromMySqlUserResult,
|
||||||
usersFromPostgresRolesResult,
|
usersFromPostgresRolesResult,
|
||||||
} from "../../apps/desktop/src/lib/database/databaseUserAdmin.ts";
|
} from "../../apps/desktop/src/lib/database/databaseUserAdmin.ts";
|
||||||
import type { QueryResult } from "../../apps/desktop/src/types/database.ts";
|
import type { ConnectionConfig, QueryResult } from "../../apps/desktop/src/types/database.ts";
|
||||||
|
|
||||||
function result(columns: string[], rows: QueryResult["rows"]): QueryResult {
|
function result(columns: string[], rows: QueryResult["rows"]): QueryResult {
|
||||||
return {
|
return {
|
||||||
@@ -175,3 +184,90 @@ test("builds PostgreSQL role metadata SQL without directly requiring rolbypassrl
|
|||||||
assert.match(grantsSql, /n\.nspname NOT LIKE 'pg~_%' ESCAPE '~'/);
|
assert.match(grantsSql, /n\.nspname NOT LIKE 'pg~_%' ESCAPE '~'/);
|
||||||
assert.ok(!grantsSql.includes("ESCAPE '\\'"));
|
assert.ok(!grantsSql.includes("ESCAPE '\\'"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("builds StarRocks user listing SQL", () => {
|
||||||
|
assert.equal(starrocksListUsersSql(), "SHOW USERS;");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("builds StarRocks table privilege SQL", () => {
|
||||||
|
const input = {
|
||||||
|
user: { user: "reporter", host: "%" },
|
||||||
|
privileges: ["select", "EXPORT"],
|
||||||
|
database: "analytics",
|
||||||
|
table: "daily`rollup",
|
||||||
|
grantOption: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.equal(starrocksPrivilegeTargetSql("*"), "ALL TABLES IN ALL DATABASES");
|
||||||
|
assert.equal(starrocksPrivilegeTargetSql("analytics"), "ALL TABLES IN DATABASE `analytics`");
|
||||||
|
assert.equal(starrocksPrivilegeTargetSql("analytics", "daily`rollup"), "TABLE `analytics`.`daily``rollup`");
|
||||||
|
assert.equal(starrocksGrantPrivilegesSql(input), "GRANT SELECT, EXPORT ON TABLE `analytics`.`daily``rollup` TO USER 'reporter'@'%' WITH GRANT OPTION;");
|
||||||
|
assert.equal(starrocksRevokePrivilegesSql(input), "REVOKE SELECT, EXPORT ON TABLE `analytics`.`daily``rollup` FROM USER 'reporter'@'%';");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses StarRocks SHOW USERS result", () => {
|
||||||
|
assert.deepEqual(starrocksUsersResult(result(["User"], [["'root'@'%'"], ["'grader_reader'@'%'"], ["'o''brien'@'localhost'"], ["malformed"]])), [
|
||||||
|
{ user: "root", host: "%" },
|
||||||
|
{ user: "grader_reader", host: "%" },
|
||||||
|
{ user: "o'brien", host: "localhost" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses StarRocks SHOW GRANTS three-column result", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
starrocksGrantsResult(
|
||||||
|
result(
|
||||||
|
["UserIdentity", "Catalog", "Grants"],
|
||||||
|
[
|
||||||
|
["'root'@'%'", null, "GRANT 'root' TO 'root'@'%'"],
|
||||||
|
["'grader_reader'@'%'", "default_catalog", "GRANT SELECT ON ALL TABLES IN DATABASE grader_events TO USER 'grader_reader'@'%'"],
|
||||||
|
["'reader'@'%'", "paimon", null],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
["GRANT 'root' TO 'root'@'%'", "GRANT SELECT ON ALL TABLES IN DATABASE grader_events TO USER 'grader_reader'@'%'"],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("StarRocks grants parser falls back to first column when Grants column is absent", () => {
|
||||||
|
assert.deepEqual(starrocksGrantsResult(result(["Grants for app@%"], [["GRANT SELECT ON `app`.* TO 'app'@'%'"]])), ["GRANT SELECT ON `app`.* TO 'app'@'%'"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("routes StarRocks to the StarRocks user admin provider", () => {
|
||||||
|
const provider = getDatabaseUserAdminProvider("starrocks");
|
||||||
|
assert.ok(provider, "expected a provider for starrocks");
|
||||||
|
assert.equal(provider?.dialect, "mysql");
|
||||||
|
assert.equal(provider?.listUsersSql(), "SHOW USERS;");
|
||||||
|
assert.equal(provider?.showGrantsSql({ user: "root", host: "%" }), "SHOW GRANTS FOR 'root'@'%';");
|
||||||
|
assert.equal(provider?.createUserSql?.({ user: "app", host: "%", password: "secret" }), "CREATE USER 'app'@'%' IDENTIFIED BY 'secret';");
|
||||||
|
assert.equal(provider?.dropUserSql?.({ user: "app", host: "%" }), "DROP USER 'app'@'%';");
|
||||||
|
assert.equal(
|
||||||
|
provider?.grantPrivilegesSql?.({
|
||||||
|
user: { user: "reporter", host: "%" },
|
||||||
|
privileges: ["SELECT"],
|
||||||
|
database: "analytics",
|
||||||
|
}),
|
||||||
|
"GRANT SELECT ON ALL TABLES IN DATABASE `analytics` TO USER 'reporter'@'%';",
|
||||||
|
);
|
||||||
|
assert.equal(provider?.alterLoginSql, undefined);
|
||||||
|
assert.deepEqual(provider?.privilegesForScope?.("table"), ["SELECT", "INSERT", "UPDATE", "DELETE", "ALTER", "DROP", "EXPORT", "ALL"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolves user admin support from the effective connection type", () => {
|
||||||
|
const mysqlProtocolStarRocks = {
|
||||||
|
id: "starrocks-1",
|
||||||
|
db_type: "mysql",
|
||||||
|
driver_profile: "starrocks",
|
||||||
|
} as ConnectionConfig;
|
||||||
|
const jdbcStarRocks = {
|
||||||
|
id: "starrocks-jdbc-1",
|
||||||
|
db_type: "jdbc",
|
||||||
|
connection_string: "jdbc:mysql://localhost:9030/analytics",
|
||||||
|
driver_profile: "starrocks",
|
||||||
|
} as ConnectionConfig;
|
||||||
|
|
||||||
|
assert.equal(resolveDatabaseUserAdminProviderForConnection(mysqlProtocolStarRocks), getDatabaseUserAdminProvider("starrocks"));
|
||||||
|
assert.equal(resolveDatabaseUserAdminProviderForConnection(jdbcStarRocks), getDatabaseUserAdminProvider("starrocks"));
|
||||||
|
assert.equal(connectionSupportsDatabaseUserAdmin(mysqlProtocolStarRocks), true);
|
||||||
|
assert.equal(connectionSupportsDatabaseUserAdmin({ id: "sqlite-1", db_type: "sqlite" } as ConnectionConfig), false);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user