feat(starrocks): enable user and privilege management
This commit is contained in:
parent
640e82cdc6
commit
41e0989f96
|
|
@ -14,7 +14,7 @@ import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
|
|||
import type { ConnectionConfig } from "@/types/database";
|
||||
import * as api from "@/lib/backend/api";
|
||||
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<{
|
||||
connection: ConnectionConfig;
|
||||
|
|
@ -54,9 +54,16 @@ const grantOption = ref(false);
|
|||
const selectedPrivileges = ref<string[]>(["SELECT"]);
|
||||
const createCanLogin = ref(true);
|
||||
|
||||
const supported = computed(() => supportsDatabaseUserAdmin(props.connection.db_type));
|
||||
const provider = computed(() => getDatabaseUserAdminProvider(props.connection.db_type));
|
||||
const provider = computed(() => resolveDatabaseUserAdminProviderForConnection(props.connection));
|
||||
const supported = computed(() => provider.value !== null);
|
||||
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 filteredUsers = computed(() => {
|
||||
const query = search.value.trim().toLowerCase();
|
||||
|
|
@ -64,7 +71,7 @@ const filteredUsers = computed(() => {
|
|||
return users.value.filter((user) => userLabel(user).toLowerCase().includes(query));
|
||||
});
|
||||
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 loginDisableLabel = computed(() => (isPostgres.value ? t("userAdmin.disableLogin") : t("userAdmin.lock")));
|
||||
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, {
|
||||
maxRows: 1000,
|
||||
});
|
||||
grants.value = grantsFromQueryResult(result);
|
||||
grants.value = (userProvider.parseGrants ?? grantsFromQueryResult)(result);
|
||||
} catch (error: any) {
|
||||
grantError.value = error?.message || String(error);
|
||||
grants.value = [];
|
||||
|
|
@ -188,10 +195,11 @@ async function applyPendingSql() {
|
|||
|
||||
function previewCreateUser() {
|
||||
const userProvider = provider.value;
|
||||
if (!userProvider) return;
|
||||
const createUserSql = userProvider?.createUserSql;
|
||||
if (!createUserSql) return;
|
||||
if (!createUser.value.trim() || !createPassword.value) return;
|
||||
previewSql(
|
||||
userProvider.createUserSql({
|
||||
createUserSql({
|
||||
user: createUser.value.trim(),
|
||||
host: createHost.value.trim() || "%",
|
||||
password: createPassword.value,
|
||||
|
|
@ -209,8 +217,9 @@ function previewCreateUser() {
|
|||
function previewPasswordChange() {
|
||||
const user = selectedUser.value;
|
||||
const userProvider = provider.value;
|
||||
if (!user || !userProvider || !newPassword.value) return;
|
||||
previewSql(userProvider.alterPasswordSql(user, newPassword.value), {
|
||||
const alterPasswordSql = userProvider?.alterPasswordSql;
|
||||
if (!user || !alterPasswordSql || !newPassword.value) return;
|
||||
previewSql(alterPasswordSql(user, newPassword.value), {
|
||||
danger: true,
|
||||
afterApply: async () => {
|
||||
passwordDialogOpen.value = false;
|
||||
|
|
@ -222,23 +231,26 @@ function previewPasswordChange() {
|
|||
function previewDropUser() {
|
||||
const user = selectedUser.value;
|
||||
const userProvider = provider.value;
|
||||
if (!user || !userProvider) return;
|
||||
previewSql(userProvider.dropUserSql(user), { danger: true });
|
||||
const dropUserSql = userProvider?.dropUserSql;
|
||||
if (!user || !dropUserSql) return;
|
||||
previewSql(dropUserSql(user), { danger: true });
|
||||
}
|
||||
|
||||
function previewLoginChange(enabled: boolean) {
|
||||
const user = selectedUser.value;
|
||||
const userProvider = provider.value;
|
||||
if (!user || !userProvider) return;
|
||||
previewSql(userProvider.alterLoginSql(user, enabled), { danger: true });
|
||||
const alterLoginSql = userProvider?.alterLoginSql;
|
||||
if (!user || !alterLoginSql) return;
|
||||
previewSql(alterLoginSql(user, enabled), { danger: true });
|
||||
}
|
||||
|
||||
function previewGrant() {
|
||||
const user = selectedUser.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(
|
||||
userProvider.grantPrivilegesSql({
|
||||
grantPrivilegesSql({
|
||||
user,
|
||||
privileges: selectedPrivileges.value,
|
||||
database: privilegeDatabase.value,
|
||||
|
|
@ -253,9 +265,10 @@ function previewGrant() {
|
|||
function previewRevoke() {
|
||||
const user = selectedUser.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(
|
||||
userProvider.revokePrivilegesSql({
|
||||
revokePrivilegesSql({
|
||||
user,
|
||||
privileges: selectedPrivileges.value,
|
||||
database: privilegeDatabase.value,
|
||||
|
|
@ -270,7 +283,7 @@ function previewRevoke() {
|
|||
function resetPrivilegeDefaults(scope: PrivilegeScope) {
|
||||
const userProvider = provider.value;
|
||||
if (!userProvider) return;
|
||||
selectedPrivileges.value = userProvider.defaultPrivilegesForScope(scope);
|
||||
selectedPrivileges.value = userProvider.defaultPrivilegesForScope?.(scope) ?? [];
|
||||
if (userProvider.dialect === "postgres") {
|
||||
if (scope === "database") privilegeDatabase.value = props.connection.database || "postgres";
|
||||
if (scope === "schema" || scope === "table") privilegeDatabase.value = "public";
|
||||
|
|
@ -296,7 +309,7 @@ watch(
|
|||
);
|
||||
|
||||
watch(
|
||||
() => provider.value?.dialect,
|
||||
() => provider.value,
|
||||
() => {
|
||||
privilegeScope.value = provider.value?.defaultScope ?? "mysql";
|
||||
resetPrivilegeDefaults(privilegeScope.value);
|
||||
|
|
@ -326,7 +339,7 @@ onMounted(loadUsers);
|
|||
<RefreshCcw v-else class="h-3.5 w-3.5" />
|
||||
{{ t("grid.refresh") }}
|
||||
</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" />
|
||||
{{ t("userAdmin.newUser") }}
|
||||
</Button>
|
||||
|
|
@ -385,27 +398,27 @@ onMounted(loadUsers);
|
|||
</Badge>
|
||||
</div>
|
||||
<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" />
|
||||
{{ t("userAdmin.changePassword") }}
|
||||
</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" />
|
||||
{{ loginDisableLabel }}
|
||||
</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" />
|
||||
{{ loginEnableLabel }}
|
||||
</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" />
|
||||
{{ t("userAdmin.dropUser") }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedUser" class="grid min-h-0 flex-1 grid-cols-[minmax(0,1fr)_320px]">
|
||||
<section class="flex min-h-0 flex-col border-r">
|
||||
<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" :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">
|
||||
<ShieldCheck class="h-3.5 w-3.5" />
|
||||
{{ t("userAdmin.grants") }}
|
||||
|
|
@ -420,7 +433,7 @@ onMounted(loadUsers);
|
|||
</div>
|
||||
</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="text-xs font-semibold">{{ t("userAdmin.privilegeEditor") }}</div>
|
||||
<div class="mt-1 text-[11px] leading-4 text-muted-foreground">{{ t("userAdmin.privilegeHint") }}</div>
|
||||
|
|
@ -478,10 +491,10 @@ onMounted(loadUsers);
|
|||
</label>
|
||||
</div>
|
||||
<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") }}
|
||||
</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") }}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ import { sidebarDisplayTableName } from "@/lib/sidebar/sidebarTableNameDisplay";
|
|||
import { shouldMeasureSidebarLabelOverflow } from "@/lib/sidebar/sidebarLabelTooltip";
|
||||
import { selectedTreeNodesInVisibleOrder as orderSelectedTreeNodes, treeSelectionRangeIdsByIndex, treeSelectionRangeIds } from "@/lib/sidebar/sidebarTreeSelection";
|
||||
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 { connectionSupportsServerDashboard } from "@/lib/database/mysqlServerStatus";
|
||||
import { connectionSupportsServerDashboard as connectionSupportsPgServerDashboard } from "@/lib/database/postgresServerStatus";
|
||||
|
|
@ -5128,7 +5128,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
|
|||
}
|
||||
const sqlHistoryMenu = savedSqlHistorySubmenu();
|
||||
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 });
|
||||
}
|
||||
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 { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
|
||||
export type UserAdminDialect = "mysql" | "postgres";
|
||||
export type PrivilegeScope = "mysql" | "database" | "schema" | "table" | "role";
|
||||
|
|
@ -33,40 +34,26 @@ export interface DatabaseUserAdminProvider {
|
|||
parseUsers(result: QueryResult): DatabaseUserIdentity[];
|
||||
parseFallbackUsers?: (result: QueryResult) => DatabaseUserIdentity[];
|
||||
showGrantsSql(user: DatabaseUserIdentity): string;
|
||||
createUserSql(input: CreatePrincipalInput): string;
|
||||
alterPasswordSql(user: DatabaseUserIdentity, password: string): string;
|
||||
alterLoginSql(user: DatabaseUserIdentity, enabled: boolean): string;
|
||||
dropUserSql(user: DatabaseUserIdentity): string;
|
||||
grantPrivilegesSql(input: PrivilegeChangeInput): string;
|
||||
revokePrivilegesSql(input: PrivilegeChangeInput): string;
|
||||
parseGrants?(result: QueryResult): string[];
|
||||
createUserSql?(input: CreatePrincipalInput): string;
|
||||
alterPasswordSql?(user: DatabaseUserIdentity, password: string): string;
|
||||
alterLoginSql?(user: DatabaseUserIdentity, enabled: boolean): string;
|
||||
dropUserSql?(user: DatabaseUserIdentity): string;
|
||||
grantPrivilegesSql?(input: PrivilegeChangeInput): string;
|
||||
revokePrivilegesSql?(input: PrivilegeChangeInput): string;
|
||||
label(user: DatabaseUserIdentity): string;
|
||||
detail(user: DatabaseUserIdentity): string | undefined;
|
||||
privilegesForScope(scope: PrivilegeScope): readonly string[];
|
||||
defaultPrivilegesForScope(scope: PrivilegeScope): string[];
|
||||
privilegesForScope?(scope: PrivilegeScope): readonly 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 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_SCHEMA_PRIVILEGES = ["USAGE", "CREATE"] 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 {
|
||||
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;";
|
||||
}
|
||||
|
||||
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 {
|
||||
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)};`;
|
||||
}
|
||||
|
||||
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[] {
|
||||
const normalized = privileges.map((privilege) => privilege.trim().toUpperCase()).filter(Boolean);
|
||||
return Array.from(new Set(normalized.length > 0 ? normalized : [fallback]));
|
||||
|
|
@ -466,3 +493,52 @@ export const kingbaseUserAdminProvider: DatabaseUserAdminProvider = {
|
|||
listUsersSql: kingbaseListRolesSql,
|
||||
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 { sortSidebarTreeChildrenForParent } from "@/lib/sidebar/sidebarNodeOrdering";
|
||||
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 { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { encodeSqlServerLinkedSchema, parseSqlServerLinkedSchema } from "@/lib/database/sqlServerLinkedServers";
|
||||
|
|
@ -1027,7 +1027,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
|
||||
function buildUserAdminNode(connectionId: string, existingConnectionNode?: TreeNode): TreeNode | undefined {
|
||||
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");
|
||||
return {
|
||||
id: `${connectionId}:__user_admin`,
|
||||
|
|
|
|||
|
|
@ -525,7 +525,7 @@
|
|||
"databaseCreate": true,
|
||||
"fieldLineage": false,
|
||||
"sqlExplain": false,
|
||||
"userAdmin": false,
|
||||
"userAdmin": true,
|
||||
"driverManagement": false
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -325,6 +325,9 @@ fn driver_manifest_declares_expected_product_capabilities() {
|
|||
assert!(zookeeper.capabilities.query_execution);
|
||||
assert!(zookeeper.capabilities.driver_management);
|
||||
assert!(!zookeeper.capabilities.metadata_browse);
|
||||
|
||||
let starrocks = find_driver(DatabaseType::StarRocks);
|
||||
assert!(starrocks.capabilities.user_admin);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { test } from "vitest";
|
||||
import {
|
||||
connectionSupportsDatabaseUserAdmin,
|
||||
grantsFromQueryResult,
|
||||
getDatabaseUserAdminProvider,
|
||||
mysqlAlterUserAccountLockSql,
|
||||
mysqlAlterUserPasswordSql,
|
||||
mysqlCreateUserSql,
|
||||
|
|
@ -25,11 +27,18 @@ import {
|
|||
quoteMySqlIdentifier,
|
||||
quoteMySqlString,
|
||||
quotePostgresIdentifier,
|
||||
resolveDatabaseUserAdminProviderForConnection,
|
||||
starrocksGrantPrivilegesSql,
|
||||
starrocksGrantsResult,
|
||||
starrocksListUsersSql,
|
||||
starrocksPrivilegeTargetSql,
|
||||
starrocksRevokePrivilegesSql,
|
||||
starrocksUsersResult,
|
||||
usersFromMySqlGranteeResult,
|
||||
usersFromMySqlUserResult,
|
||||
usersFromPostgresRolesResult,
|
||||
} 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 {
|
||||
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.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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue