From 64293345491d76af727d217c9d651c8bf77b913c Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 12 Aug 2026 20:18:18 +0800 Subject: [PATCH] fix(gaussdb): support M-mode user administration --- .../database/databaseUserAdmin.spec.ts | 71 +++++++++- .../src/lib/database/databaseUserAdmin.ts | 124 ++++++++++++++++-- 2 files changed, 182 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src/lib/__tests__/database/databaseUserAdmin.spec.ts b/apps/desktop/src/lib/__tests__/database/databaseUserAdmin.spec.ts index 5da41a6963..7cfa8e1b01 100644 --- a/apps/desktop/src/lib/__tests__/database/databaseUserAdmin.spec.ts +++ b/apps/desktop/src/lib/__tests__/database/databaseUserAdmin.spec.ts @@ -1,11 +1,36 @@ import { describe, expect, it } from "vitest"; -import { dorisGrantPrivilegesSql, dorisGrantsResult, dorisPrivilegeTargetSql, dorisUsersResult, getDatabaseUserAdminProvider, kingbaseShowGrantsSql, mysqlPrivilegeSelectionFromGrants } from "@/lib/database/databaseUserAdmin"; -import type { QueryResult } from "@/types/database"; +import { + dorisGrantPrivilegesSql, + dorisGrantsResult, + dorisPrivilegeTargetSql, + dorisUsersResult, + gaussdbMUserAdminProvider, + getDatabaseUserAdminProvider, + kingbaseShowGrantsSql, + kingbaseUserAdminProvider, + mysqlPrivilegeSelectionFromGrants, + postgresUserAdminProvider, + resolveDatabaseUserAdminProviderForConnection, +} from "@/lib/database/databaseUserAdmin"; +import type { ConnectionConfig, QueryResult } from "@/types/database"; function result(columns: string[], rows: QueryResult["rows"]): QueryResult { return { columns, rows, affected_rows: 0, execution_time_ms: 1 }; } +function connection(dbType: ConnectionConfig["db_type"], driverProfile?: string): ConnectionConfig { + return { + id: `${dbType}-connection`, + name: dbType, + db_type: dbType, + driver_profile: driverProfile, + host: "localhost", + port: 5432, + username: "tester", + password: "", + }; +} + describe("MySQL grant privilege selection", () => { const availablePrivileges = ["SELECT", "INSERT", "UPDATE", "EXECUTE"]; @@ -55,6 +80,48 @@ describe("MySQL grant privilege selection", () => { }); describe("database user admin providers", () => { + it("selects the dedicated provider for GaussDB M-mode connections", () => { + const provider = resolveDatabaseUserAdminProviderForConnection(connection("gaussdb", "GAUSSDB-M")); + + expect(provider).toBe(gaussdbMUserAdminProvider); + }); + + it("quotes role result aliases for M-mode and PostgreSQL-compatible providers", () => { + const gaussdbMSql = gaussdbMUserAdminProvider.listUsersSql(); + const postgresSql = postgresUserAdminProvider.listUsersSql(); + const kingbaseSql = kingbaseUserAdminProvider.listUsersSql(); + + expect(gaussdbMSql).toContain("r.rolname AS `user`"); + expect(gaussdbMSql).toContain("END AS `host`"); + expect(gaussdbMSql).toContain(") AS `plugin`"); + for (const sql of [postgresSql, kingbaseSql]) { + expect(sql).toContain('r.rolname AS "user"'); + expect(sql).toContain('END AS "host"'); + expect(sql).toContain(') AS "plugin"'); + } + }); + + it("uses M-mode-compatible table grant SQL", () => { + const sql = gaussdbMUserAdminProvider.showGrantsSql({ user: "role'o", host: "LOGIN" }); + + expect(sql).toContain("FROM information_schema.table_privileges"); + expect(sql).toContain("concat('Role: '"); + expect(sql).toContain("AS `rolbypassrls`"); + expect(sql).toContain("AS `sort`"); + expect(sql).toContain("AS `line`"); + expect(sql).toContain("ORDER BY `sort`, `line`"); + expect(sql).toContain("WHERE r.rolname = 'role''o'"); + expect(sql).not.toContain("role_table_grants"); + expect(sql).not.toContain(" || "); + }); + + it("keeps native GaussDB, PostgreSQL, and Kingbase providers unchanged", () => { + expect(resolveDatabaseUserAdminProviderForConnection(connection("gaussdb"))).toBe(postgresUserAdminProvider); + expect(resolveDatabaseUserAdminProviderForConnection(connection("postgres"))).toBe(postgresUserAdminProvider); + expect(resolveDatabaseUserAdminProviderForConnection(connection("kingbase"))).toBe(kingbaseUserAdminProvider); + expect(getDatabaseUserAdminProvider("gaussdb")).toBe(postgresUserAdminProvider); + }); + it("uses Doris 2.x user and privilege syntax", () => { const provider = getDatabaseUserAdminProvider("doris"); diff --git a/apps/desktop/src/lib/database/databaseUserAdmin.ts b/apps/desktop/src/lib/database/databaseUserAdmin.ts index ff0dc61020..04653ac923 100644 --- a/apps/desktop/src/lib/database/databaseUserAdmin.ts +++ b/apps/desktop/src/lib/database/databaseUserAdmin.ts @@ -1,6 +1,6 @@ import type { ConnectionConfig, DatabaseType, QueryResult } from "@/types/database"; import { supportsDatabaseFeature } from "@/lib/database/databaseDriverManifest"; -import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; +import { effectiveDatabaseTypeForConnection, gaussdbConnectionMode } from "@/lib/database/jdbcDialect"; export type UserAdminDialect = "mysql" | "postgres"; export type PrivilegeScope = "mysql" | "database" | "schema" | "table" | "role"; @@ -301,15 +301,32 @@ export function postgresListRolesSql(): string { const bypassRls = postgresRoleBypassRlsSql("r"); return ` SELECT - r.rolname AS user, - CASE WHEN r.rolcanlogin THEN 'LOGIN' ELSE 'ROLE' END AS host, + r.rolname AS "user", + CASE WHEN r.rolcanlogin THEN 'LOGIN' ELSE 'ROLE' END AS "host", concat_ws(', ', CASE WHEN r.rolsuper THEN 'SUPERUSER' END, CASE WHEN r.rolcreatedb THEN 'CREATEDB' END, CASE WHEN r.rolcreaterole THEN 'CREATEROLE' END, CASE WHEN r.rolreplication THEN 'REPLICATION' END, CASE WHEN ${bypassRls} THEN 'BYPASSRLS' END - ) AS plugin + ) AS "plugin" +FROM pg_catalog.pg_roles r +ORDER BY r.rolname;`.trim(); +} + +export function gaussdbMListRolesSql(): string { + const bypassRls = postgresRoleBypassRlsSql("r"); + return ` +SELECT + r.rolname AS \`user\`, + CASE WHEN r.rolcanlogin THEN 'LOGIN' ELSE 'ROLE' END AS \`host\`, + concat_ws(', ', + CASE WHEN r.rolsuper THEN 'SUPERUSER' END, + CASE WHEN r.rolcreatedb THEN 'CREATEDB' END, + CASE WHEN r.rolcreaterole THEN 'CREATEROLE' END, + CASE WHEN r.rolreplication THEN 'REPLICATION' END, + CASE WHEN ${bypassRls} THEN 'BYPASSRLS' END + ) AS \`plugin\` FROM pg_catalog.pg_roles r ORDER BY r.rolname;`.trim(); } @@ -384,18 +401,90 @@ FROM ( ORDER BY sort, line;`.trim(); } +export function gaussdbMShowGrantsSql(user: DatabaseUserIdentity): string { + const role = quoteSqlString(user.user); + const bypassRls = postgresRoleBypassRlsSql("r"); + // GaussDB M mode (MySQL-compatible) omits role_table_grants and treats || + // as boolean OR, so use concat() and table_privileges instead. + // CAST(... AS text) is avoided because "text" is a reserved keyword in M + // mode; privilege_type and is_grantable are already text/varchar types. + return ` +WITH target AS ( + SELECT oid, rolname, rolsuper, rolcreatedb, rolcreaterole, rolcanlogin, rolreplication, ${bypassRls} AS \`rolbypassrls\` + FROM pg_catalog.pg_roles r + WHERE r.rolname = ${role} +) +SELECT \`line\` +FROM ( + SELECT 1 AS \`sort\`, concat('Role: ', quote_ident(rolname)) AS \`line\` + FROM target + UNION ALL + SELECT 2, concat('Attributes: ', COALESCE(NULLIF(concat_ws(', ', + CASE WHEN rolsuper THEN 'SUPERUSER' END, + CASE WHEN rolcreatedb THEN 'CREATEDB' END, + CASE WHEN rolcreaterole THEN 'CREATEROLE' END, + CASE WHEN rolcanlogin THEN 'LOGIN' ELSE 'NOLOGIN' END, + CASE WHEN rolreplication THEN 'REPLICATION' END, + CASE WHEN rolbypassrls THEN 'BYPASSRLS' END + ), ''), 'none')) + FROM target + UNION ALL + SELECT 10, concat('Member of: ', quote_ident(parent.rolname), CASE WHEN m.admin_option THEN ' WITH ADMIN OPTION' ELSE '' END) + FROM pg_catalog.pg_auth_members m + JOIN target t ON t.oid = m.member + JOIN pg_catalog.pg_roles parent ON parent.oid = m.roleid + UNION ALL + SELECT 20, concat('Has member: ', quote_ident(member.rolname), CASE WHEN m.admin_option THEN ' WITH ADMIN OPTION' ELSE '' END) + FROM pg_catalog.pg_auth_members m + JOIN target t ON t.oid = m.roleid + JOIN pg_catalog.pg_roles member ON member.oid = m.member + UNION ALL + SELECT 30, concat('Database: ', quote_ident(d.datname), ' = ', + concat_ws(', ', + CASE WHEN has_database_privilege(t.rolname, d.oid, 'CONNECT') THEN 'CONNECT' END, + CASE WHEN has_database_privilege(t.rolname, d.oid, 'CREATE') THEN 'CREATE' END, + CASE WHEN has_database_privilege(t.rolname, d.oid, 'TEMPORARY') THEN 'TEMPORARY' END + )) + FROM target t + CROSS JOIN pg_catalog.pg_database d + WHERE has_database_privilege(t.rolname, d.oid, 'CONNECT') + OR has_database_privilege(t.rolname, d.oid, 'CREATE') + OR has_database_privilege(t.rolname, d.oid, 'TEMPORARY') + UNION ALL + SELECT 40, concat('Schema: ', quote_ident(n.nspname), ' = ', + concat_ws(', ', + CASE WHEN has_schema_privilege(t.rolname, n.oid, 'USAGE') THEN 'USAGE' END, + CASE WHEN has_schema_privilege(t.rolname, n.oid, 'CREATE') THEN 'CREATE' END + )) + FROM target t + CROSS JOIN pg_catalog.pg_namespace n + WHERE n.nspname NOT LIKE 'pg~_%' ESCAPE '~' + AND n.nspname <> 'information_schema' + AND (has_schema_privilege(t.rolname, n.oid, 'USAGE') OR has_schema_privilege(t.rolname, n.oid, 'CREATE')) + UNION ALL + SELECT 50, concat('Table: ', quote_ident(table_schema), '.', quote_ident(table_name), ' = ', + string_agg(concat(privilege_type, CASE WHEN is_grantable = 'YES' THEN ' WITH GRANT OPTION' ELSE '' END), ', ' ORDER BY privilege_type)) + FROM information_schema.table_privileges + WHERE grantee = ${role} + AND UPPER(table_schema) <> 'INFORMATION_SCHEMA' + AND UPPER(table_schema) NOT LIKE 'PG~_%' ESCAPE '~' + GROUP BY table_schema, table_name +) grants +ORDER BY \`sort\`, \`line\`;`.trim(); +} + export function kingbaseListRolesSql(): string { return ` SELECT - r.rolname AS user, - CASE WHEN r.rolcanlogin THEN 'LOGIN' ELSE 'ROLE' END AS host, + r.rolname AS "user", + CASE WHEN r.rolcanlogin THEN 'LOGIN' ELSE 'ROLE' END AS "host", concat_ws(', ', CASE WHEN r.rolsuper THEN 'SUPERUSER' END, CASE WHEN r.rolcreatedb THEN 'CREATEDB' END, CASE WHEN r.rolcreaterole THEN 'CREATEROLE' END, CASE WHEN r.rolreplication THEN 'REPLICATION' END, CASE WHEN r.rolbypassrls THEN 'BYPASSRLS' END - ) AS plugin + ) AS "plugin" FROM sys_catalog.sys_roles r ORDER BY r.rolname;`.trim(); } @@ -637,6 +726,12 @@ export const kingbaseUserAdminProvider: DatabaseUserAdminProvider = { showGrantsSql: kingbaseShowGrantsSql, }; +export const gaussdbMUserAdminProvider: DatabaseUserAdminProvider = { + ...postgresUserAdminProvider, + listUsersSql: gaussdbMListRolesSql, + showGrantsSql: gaussdbMShowGrantsSql, +}; + export const dorisUserAdminProvider: DatabaseUserAdminProvider = { dialect: "mysql", defaultScope: "table", @@ -690,17 +785,24 @@ const DATABASE_USER_ADMIN_PROVIDER_BY_TYPE = new Map