Files
dbx/apps/desktop/src/lib/database/databaseUserAdmin.ts
T

709 lines
29 KiB
TypeScript

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";
export interface DatabaseUserIdentity {
user: string;
host: string;
plugin?: string;
}
export interface CreatePrincipalInput extends DatabaseUserIdentity {
password: string;
canLogin?: boolean;
}
export interface PrivilegeChangeInput {
user: DatabaseUserIdentity;
privileges: string[];
database: string;
table?: string;
grantOption?: boolean;
scope?: PrivilegeScope;
role?: string;
}
export interface PrivilegeSelectionInput {
grants: readonly string[];
database: string;
table?: string;
availablePrivileges: readonly string[];
}
export interface PrivilegeSelection {
privileges: string[];
grantOption: boolean;
}
export interface DatabaseUserAdminProvider {
dialect: UserAdminDialect;
defaultScope: PrivilegeScope;
listUsersSql(): string;
fallbackListUsersSql?: () => string;
parseUsers(result: QueryResult): DatabaseUserIdentity[];
parseFallbackUsers?: (result: QueryResult) => DatabaseUserIdentity[];
showGrantsSql(user: DatabaseUserIdentity): 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[];
privilegeSelectionFromGrants?(input: PrivilegeSelectionInput): PrivilegeSelection;
}
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", "CREATE VIEW", "CREATE ROUTINE", "ALTER ROUTINE", "TRIGGER", "EVENT", "CREATE TEMPORARY TABLES", "LOCK TABLES"] as const;
export const DORIS_TABLE_PRIVILEGES = ["SELECT_PRIV", "LOAD_PRIV", "ALTER_PRIV", "CREATE_PRIV", "DROP_PRIV", "SHOW_VIEW_PRIV"] 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 quoteSqlString(value: string): string {
return `'${value.replace(/'/g, "''")}'`;
}
export function quoteMySqlString(value: string): string {
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "''")}'`;
}
export function quoteMySqlIdentifier(value: string): string {
return `\`${value.replace(/`/g, "``")}\``;
}
export function quotePostgresIdentifier(value: string): string {
return `"${value.replace(/"/g, '""')}"`;
}
export function mysqlUserAccount(user: DatabaseUserIdentity): string {
return `${quoteMySqlString(user.user)}@${quoteMySqlString(user.host)}`;
}
export function mysqlUserLabel(user: DatabaseUserIdentity): string {
return `${user.user}@${user.host}`;
}
export function postgresRoleLabel(user: DatabaseUserIdentity): string {
return user.user;
}
export function mysqlListUsersSql(): string {
return "SELECT User AS user, Host AS host, plugin AS plugin FROM mysql.user ORDER BY User, Host;";
}
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 dorisListUsersSql(): string {
return "SHOW ALL GRANTS;";
}
export function dorisListCurrentUserSql(): string {
return "SHOW GRANTS;";
}
export function dorisUsersResult(result: QueryResult): DatabaseUserIdentity[] {
const userIndex = columnIndex(result, "UserIdentity");
if (userIndex < 0) return [];
return result.rows.flatMap((row) => {
const parsed = parseMySqlGrantee(String(row[userIndex] ?? ""));
return parsed ? [parsed] : [];
});
}
export function dorisGrantsResult(result: QueryResult): string[] {
const ignoredColumns = new Set(["useridentity", "comment", "password"]);
return result.rows.flatMap((row) =>
result.columns.flatMap((column, index) => {
if (ignoredColumns.has(column.toLowerCase())) return [];
const value = String(row[index] ?? "").trim();
return value && value.toUpperCase() !== "NULL" ? [`${column}: ${value}`] : [];
}),
);
}
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)};`;
}
export function mysqlCreateUserSql(input: CreatePrincipalInput): string {
return `CREATE USER ${mysqlUserAccount(input)} IDENTIFIED BY ${quoteMySqlString(input.password)};`;
}
export function mysqlAlterUserPasswordSql(user: DatabaseUserIdentity, password: string): string {
return `ALTER USER ${mysqlUserAccount(user)} IDENTIFIED BY ${quoteMySqlString(password)};`;
}
export function dorisAlterUserPasswordSql(user: DatabaseUserIdentity, password: string): string {
return `SET PASSWORD FOR ${mysqlUserAccount(user)} = PASSWORD(${quoteMySqlString(password)});`;
}
export function mysqlAlterUserAccountLockSql(user: DatabaseUserIdentity, locked: boolean): string {
return `ALTER USER ${mysqlUserAccount(user)} ACCOUNT ${locked ? "LOCK" : "UNLOCK"};`;
}
export function mysqlDropUserSql(user: DatabaseUserIdentity): string {
return `DROP USER ${mysqlUserAccount(user)};`;
}
export function mysqlPrivilegeTargetSql(database: string, table = "*"): string {
const db = database.trim() || "*";
const tbl = table.trim() || "*";
const dbSql = db === "*" ? "*" : quoteMySqlIdentifier(db);
const tableSql = tbl === "*" ? "*" : quoteMySqlIdentifier(tbl);
return `${dbSql}.${tableSql}`;
}
export function mysqlGrantPrivilegesSql(input: PrivilegeChangeInput): string {
const privileges = normalizePrivileges(input.privileges).join(", ");
const grantOption = input.grantOption ? " WITH GRANT OPTION" : "";
return `GRANT ${privileges} ON ${mysqlPrivilegeTargetSql(input.database, input.table)} TO ${mysqlUserAccount(input.user)}${grantOption};`;
}
export function mysqlRevokePrivilegesSql(input: PrivilegeChangeInput): string {
const privileges = normalizePrivileges(input.privileges).join(", ");
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 dorisPrivilegeTargetSql(database: string, table = "*"): string {
const db = database.trim() || "*";
const tbl = table.trim() || "*";
if (db === "*") return "*.*.*";
const tableSql = tbl === "*" ? "*" : quoteMySqlIdentifier(tbl);
return `${quoteMySqlIdentifier("internal")}.${quoteMySqlIdentifier(db)}.${tableSql}`;
}
export function dorisGrantPrivilegesSql(input: PrivilegeChangeInput): string {
const privileges = normalizePrivileges(input.privileges, "SELECT_PRIV").join(", ");
// Doris 2.x GRANT has no WITH GRANT OPTION clause; GRANT_PRIV is managed as an explicit privilege instead.
return `GRANT ${privileges} ON ${dorisPrivilegeTargetSql(input.database, input.table)} TO ${mysqlUserAccount(input.user)};`;
}
export function dorisRevokePrivilegesSql(input: PrivilegeChangeInput): string {
const privileges = normalizePrivileges(input.privileges, "SELECT_PRIV").join(", ");
return `REVOKE ${privileges} ON ${dorisPrivilegeTargetSql(input.database, input.table)} FROM ${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]));
}
export const normalizeMySqlPrivileges = normalizePrivileges;
export function mysqlPrivilegeSelectionFromGrants(input: PrivilegeSelectionInput): PrivilegeSelection {
const database = normalizeMySqlScopeIdentifier(input.database || "*");
const table = normalizeMySqlScopeIdentifier(input.table || "*");
const availableByName = new Map(input.availablePrivileges.map((privilege) => [normalizePrivilegeName(privilege), privilege]));
const selected = new Set<string>();
let grantOption = false;
for (const grantSql of input.grants) {
const grant = parseMySqlGrant(grantSql);
if (!grant || grant.objectType === "FUNCTION" || grant.objectType === "PROCEDURE") continue;
if (normalizeMySqlScopeIdentifier(grant.database) !== database || normalizeMySqlScopeIdentifier(grant.table) !== table) continue;
grantOption ||= grant.grantOption;
if (grant.allPrivileges) {
input.availablePrivileges.forEach((privilege) => selected.add(privilege));
continue;
}
grant.privileges.forEach((privilege) => {
const available = availableByName.get(normalizePrivilegeName(privilege));
if (available) selected.add(available);
});
}
return {
privileges: input.availablePrivileges.filter((privilege) => selected.has(privilege)),
grantOption,
};
}
export function usersFromMySqlUserResult(result: QueryResult): DatabaseUserIdentity[] {
const userIndex = columnIndex(result, "user", "User");
const hostIndex = columnIndex(result, "host", "Host");
const pluginIndex = columnIndex(result, "plugin", "Plugin");
if (userIndex < 0 || hostIndex < 0) return [];
return result.rows
.map((row) => ({
user: String(row[userIndex] ?? ""),
host: String(row[hostIndex] ?? ""),
plugin: pluginIndex >= 0 && row[pluginIndex] != null ? String(row[pluginIndex]) : undefined,
}))
.filter((user) => user.user || user.host);
}
export function usersFromMySqlGranteeResult(result: QueryResult): DatabaseUserIdentity[] {
const granteeIndex = columnIndex(result, "grantee", "GRANTEE");
if (granteeIndex < 0) return [];
return result.rows.flatMap((row) => {
const parsed = parseMySqlGrantee(String(row[granteeIndex] ?? ""));
return parsed ? [parsed] : [];
});
}
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,
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();
}
export function usersFromPostgresRolesResult(result: QueryResult): DatabaseUserIdentity[] {
return usersFromMySqlUserResult(result);
}
export function postgresShowGrantsSql(user: DatabaseUserIdentity): string {
const role = quoteSqlString(user.user);
const bypassRls = postgresRoleBypassRlsSql("r");
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, 'Role: ' || quote_ident(rolname) AS line
FROM target
UNION ALL
SELECT 2, '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, '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, '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, '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, '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, 'Table: ' || quote_ident(table_schema) || '.' || quote_ident(table_name) || ' = ' ||
string_agg(privilege_type || CASE WHEN is_grantable = 'YES' THEN ' WITH GRANT OPTION' ELSE '' END, ', ' ORDER BY privilege_type)
FROM information_schema.role_table_grants
WHERE grantee = ${role}
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,
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
FROM sys_catalog.sys_roles r
ORDER BY r.rolname;`.trim();
}
export function kingbaseShowGrantsSql(user: DatabaseUserIdentity): string {
const role = quoteSqlString(user.user);
// SQL Server-compatible Kingbase omits role_table_grants, while
// table_privileges is available across modes; concat also avoids MySQL
// compatibility mode interpreting || as boolean OR.
return `
WITH target AS (
SELECT oid, rolname, rolsuper, rolcreatedb, rolcreaterole, rolcanlogin, rolreplication, rolbypassrls
FROM sys_catalog.sys_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 sys_catalog.sys_auth_members m
JOIN target t ON t.oid = m.member
JOIN sys_catalog.sys_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 sys_catalog.sys_auth_members m
JOIN target t ON t.oid = m.roleid
JOIN sys_catalog.sys_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 sys_catalog.sys_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 sys_catalog.sys_namespace n
WHERE UPPER(n.nspname) <> 'INFORMATION_SCHEMA'
AND UPPER(n.nspname) NOT LIKE 'SYS%'
AND UPPER(n.nspname) NOT LIKE 'XLOG%'
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(CAST(privilege_type AS text), CASE WHEN CAST(is_grantable AS text) = 'YES' THEN ' WITH GRANT OPTION' ELSE '' END), ', ' ORDER BY CAST(privilege_type AS text)))
FROM information_schema.table_privileges
WHERE grantee = ${role}
AND UPPER(table_schema) <> 'INFORMATION_SCHEMA'
AND UPPER(table_schema) NOT LIKE 'SYS%'
AND UPPER(table_schema) NOT LIKE 'XLOG%'
GROUP BY table_schema, table_name
) grants
ORDER BY sort, line;`.trim();
}
export function postgresCreateRoleSql(input: CreatePrincipalInput): string {
const login = input.canLogin === false ? "NOLOGIN" : "LOGIN";
return `CREATE ROLE ${quotePostgresIdentifier(input.user)} ${login} PASSWORD ${quoteSqlString(input.password)};`;
}
export function postgresAlterRolePasswordSql(user: DatabaseUserIdentity, password: string): string {
return `ALTER ROLE ${quotePostgresIdentifier(user.user)} PASSWORD ${quoteSqlString(password)};`;
}
export function postgresAlterRoleLoginSql(user: DatabaseUserIdentity, enabled: boolean): string {
return `ALTER ROLE ${quotePostgresIdentifier(user.user)} ${enabled ? "LOGIN" : "NOLOGIN"};`;
}
export function postgresDropRoleSql(user: DatabaseUserIdentity): string {
return `DROP ROLE ${quotePostgresIdentifier(user.user)};`;
}
export function postgresPrivilegeTargetSql(input: Pick<PrivilegeChangeInput, "scope" | "database" | "table">): string {
const scope = input.scope || "database";
const name = input.database.trim();
if (scope === "database") return `DATABASE ${quotePostgresIdentifier(name || "postgres")}`;
if (scope === "schema") return `SCHEMA ${quotePostgresIdentifier(name || "public")}`;
const schema = name || "public";
const table = input.table?.trim() || "*";
if (table === "*") return `ALL TABLES IN SCHEMA ${quotePostgresIdentifier(schema)}`;
return `TABLE ${quotePostgresIdentifier(schema)}.${quotePostgresIdentifier(table)}`;
}
export function postgresGrantPrivilegesSql(input: PrivilegeChangeInput): string {
if (input.scope === "role") {
return `GRANT ${quotePostgresIdentifier(input.role?.trim() || "")} TO ${quotePostgresIdentifier(input.user.user)}${input.grantOption ? " WITH ADMIN OPTION" : ""};`;
}
const privileges = normalizePrivileges(input.privileges, postgresDefaultPrivilege(input.scope)).join(", ");
return `GRANT ${privileges} ON ${postgresPrivilegeTargetSql(input)} TO ${quotePostgresIdentifier(input.user.user)}${input.grantOption ? " WITH GRANT OPTION" : ""};`;
}
export function postgresRevokePrivilegesSql(input: PrivilegeChangeInput): string {
if (input.scope === "role") {
return `REVOKE ${quotePostgresIdentifier(input.role?.trim() || "")} FROM ${quotePostgresIdentifier(input.user.user)};`;
}
const privileges = normalizePrivileges(input.privileges, postgresDefaultPrivilege(input.scope)).join(", ");
return `REVOKE ${privileges} ON ${postgresPrivilegeTargetSql(input)} FROM ${quotePostgresIdentifier(input.user.user)};`;
}
export function grantsFromQueryResult(result: QueryResult): string[] {
return result.rows.map((row) => String(row[0] ?? "")).filter(Boolean);
}
function columnIndex(result: QueryResult, ...names: string[]): number {
const wanted = new Set(names.map((name) => name.toLowerCase()));
return result.columns.findIndex((column) => wanted.has(column.toLowerCase()));
}
function parseMySqlGrantee(value: string): DatabaseUserIdentity | null {
const match = /^'((?:''|[^'])*)'@'((?:''|[^'])*)'$/.exec(value.trim());
if (!match) return null;
return {
user: match[1].replace(/''/g, "'"),
host: match[2].replace(/''/g, "'"),
};
}
interface ParsedMySqlGrant {
privileges: string[];
database: string;
table: string;
objectType?: "TABLE" | "FUNCTION" | "PROCEDURE";
allPrivileges: boolean;
grantOption: boolean;
}
function parseMySqlGrant(sql: string): ParsedMySqlGrant | null {
const identifier = String.raw`(?:\x60(?:\x60\x60|[^\x60])*\x60|\*|[^\s.]+)`;
const match = new RegExp(String.raw`^\s*GRANT\s+(.+?)\s+ON\s+(?:(TABLE|FUNCTION|PROCEDURE)\s+)?(${identifier})\s*\.\s*(${identifier})\s+TO\s+`, "i").exec(sql);
if (!match) return null;
const privileges = match[1].split(",").map(normalizePrivilegeName).filter(Boolean);
return {
privileges,
database: unquoteMySqlIdentifier(match[3]),
table: unquoteMySqlIdentifier(match[4]),
objectType: match[2]?.toUpperCase() as ParsedMySqlGrant["objectType"],
allPrivileges: privileges.some((privilege) => privilege === "ALL" || privilege === "ALL PRIVILEGES"),
grantOption: /\s+WITH\s+GRANT\s+OPTION\s*;?\s*$/i.test(sql),
};
}
function normalizePrivilegeName(value: string): string {
return value.trim().replace(/\s+/g, " ").toUpperCase();
}
function normalizeMySqlScopeIdentifier(value: string): string {
return unquoteMySqlIdentifier(value.trim()).toLocaleLowerCase("en-US");
}
function unquoteMySqlIdentifier(value: string): string {
const trimmed = value.trim();
if (trimmed.length >= 2 && trimmed.startsWith("`") && trimmed.endsWith("`")) {
return trimmed.slice(1, -1).replace(/``/g, "`");
}
return trimmed;
}
function postgresDefaultPrivilege(scope: PrivilegeScope | undefined): string {
if (scope === "schema") return "USAGE";
if (scope === "database") return "CONNECT";
return "SELECT";
}
function postgresPrivilegesForScope(scope: PrivilegeScope): readonly string[] {
if (scope === "database") return POSTGRES_DATABASE_PRIVILEGES;
if (scope === "schema") return POSTGRES_SCHEMA_PRIVILEGES;
if (scope === "table") return POSTGRES_TABLE_PRIVILEGES;
return [];
}
function postgresRoleBypassRlsSql(alias: string): string {
return `row_to_json(${alias})::text LIKE '%"rolbypassrls":true%'`;
}
export const mysqlUserAdminProvider: DatabaseUserAdminProvider = {
dialect: "mysql",
defaultScope: "mysql",
listUsersSql: mysqlListUsersSql,
fallbackListUsersSql: mysqlListUsersFallbackSql,
parseUsers: usersFromMySqlUserResult,
parseFallbackUsers: usersFromMySqlGranteeResult,
showGrantsSql: mysqlShowGrantsSql,
createUserSql: mysqlCreateUserSql,
alterPasswordSql: mysqlAlterUserPasswordSql,
alterLoginSql: (user, enabled) => mysqlAlterUserAccountLockSql(user, !enabled),
dropUserSql: mysqlDropUserSql,
grantPrivilegesSql: mysqlGrantPrivilegesSql,
revokePrivilegesSql: mysqlRevokePrivilegesSql,
label: mysqlUserLabel,
detail: (user) => user.plugin,
privilegesForScope: () => MYSQL_COMMON_PRIVILEGES,
defaultPrivilegesForScope: () => ["SELECT"],
privilegeSelectionFromGrants: mysqlPrivilegeSelectionFromGrants,
};
export const postgresUserAdminProvider: DatabaseUserAdminProvider = {
dialect: "postgres",
defaultScope: "database",
listUsersSql: postgresListRolesSql,
parseUsers: usersFromPostgresRolesResult,
showGrantsSql: postgresShowGrantsSql,
createUserSql: postgresCreateRoleSql,
alterPasswordSql: postgresAlterRolePasswordSql,
alterLoginSql: postgresAlterRoleLoginSql,
dropUserSql: postgresDropRoleSql,
grantPrivilegesSql: postgresGrantPrivilegesSql,
revokePrivilegesSql: postgresRevokePrivilegesSql,
label: postgresRoleLabel,
detail: (user) => [user.host, user.plugin].filter(Boolean).join(" · ") || undefined,
privilegesForScope: postgresPrivilegesForScope,
defaultPrivilegesForScope: (scope) => (scope === "role" ? [] : [postgresDefaultPrivilege(scope)]),
};
export const kingbaseUserAdminProvider: DatabaseUserAdminProvider = {
...postgresUserAdminProvider,
listUsersSql: kingbaseListRolesSql,
showGrantsSql: kingbaseShowGrantsSql,
};
export const dorisUserAdminProvider: DatabaseUserAdminProvider = {
dialect: "mysql",
defaultScope: "table",
listUsersSql: dorisListUsersSql,
fallbackListUsersSql: dorisListCurrentUserSql,
parseUsers: dorisUsersResult,
parseFallbackUsers: dorisUsersResult,
showGrantsSql: mysqlShowGrantsSql,
parseGrants: dorisGrantsResult,
createUserSql: mysqlCreateUserSql,
alterPasswordSql: dorisAlterUserPasswordSql,
dropUserSql: mysqlDropUserSql,
grantPrivilegesSql: dorisGrantPrivilegesSql,
revokePrivilegesSql: dorisRevokePrivilegesSql,
label: mysqlUserLabel,
detail: (user) => user.plugin,
privilegesForScope: () => DORIS_TABLE_PRIVILEGES,
defaultPrivilegesForScope: () => ["SELECT_PRIV"],
};
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],
["doris", dorisUserAdminProvider],
["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;
}