fix(gaussdb): support M-mode user administration
This commit is contained in:
1 parent
105920047a
commit
6429334549
2 files changed
+182
-13
No files matched your search
@@ -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");
|
||||
|
||||
|
||||
@@ -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<DatabaseType, DatabaseUserA
|
||||
["starrocks", starrocksUserAdminProvider],
|
||||
]);
|
||||
|
||||
export function getDatabaseUserAdminProvider(dbType: DatabaseType | undefined): DatabaseUserAdminProvider | null {
|
||||
return dbType ? (DATABASE_USER_ADMIN_PROVIDER_BY_TYPE.get(dbType) ?? null) : null;
|
||||
export function getDatabaseUserAdminProvider(dbType: DatabaseType | undefined, connection?: ConnectionConfig): DatabaseUserAdminProvider | null {
|
||||
if (!dbType) return null;
|
||||
// GaussDB M mode (MySQL-compatible) uses backtick quoting and treats "user"
|
||||
// as a reserved keyword, so a dedicated provider with backtick-quoted aliases
|
||||
// is required.
|
||||
if (dbType === "gaussdb" && connection && gaussdbConnectionMode(connection) === "m-jdbc") {
|
||||
return gaussdbMUserAdminProvider;
|
||||
}
|
||||
return DATABASE_USER_ADMIN_PROVIDER_BY_TYPE.get(dbType) ?? null;
|
||||
}
|
||||
|
||||
export function supportsDatabaseUserAdmin(dbType: DatabaseType | undefined): boolean {
|
||||
return !!dbType && supportsDatabaseFeature(dbType, "userAdmin") && DATABASE_USER_ADMIN_PROVIDER_BY_TYPE.has(dbType);
|
||||
return !!dbType && supportsDatabaseFeature(dbType, "userAdmin") && (dbType === "gaussdb" || 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;
|
||||
return supportsDatabaseUserAdmin(dbType) ? getDatabaseUserAdminProvider(dbType, connection) : null;
|
||||
}
|
||||
|
||||
export function connectionSupportsDatabaseUserAdmin(connection: ConnectionConfig | undefined): boolean {
|
||||
|
||||
Reference in new issue
Block a user