fix(doris): restore user privilege management

This commit is contained in:
t8y2 committed 2026-07-21 19:46:03 +08:00
1 parent c2de49c155
commit 9421a1156e
4 files changed
+162 -2

No files matched your search

@@ -1,5 +1,10 @@
import { describe, expect, it } from "vitest";
import { getDatabaseUserAdminProvider, kingbaseShowGrantsSql, mysqlPrivilegeSelectionFromGrants } from "@/lib/database/databaseUserAdmin";
import { dorisGrantPrivilegesSql, dorisGrantsResult, dorisPrivilegeTargetSql, dorisUsersResult, getDatabaseUserAdminProvider, kingbaseShowGrantsSql, mysqlPrivilegeSelectionFromGrants } from "@/lib/database/databaseUserAdmin";
import type { QueryResult } from "@/types/database";
function result(columns: string[], rows: QueryResult["rows"]): QueryResult {
return { columns, rows, affected_rows: 0, execution_time_ms: 1 };
}
describe("MySQL grant privilege selection", () => {
const availablePrivileges = ["SELECT", "INSERT", "UPDATE", "EXECUTE"];
@@ -50,8 +55,28 @@ describe("MySQL grant privilege selection", () => {
});
describe("database user admin providers", () => {
it("uses Doris 2.x user and privilege syntax", () => {
const provider = getDatabaseUserAdminProvider("doris");
expect(provider?.listUsersSql()).toBe("SHOW ALL GRANTS;");
expect(provider?.showGrantsSql({ user: "reporter", host: "%" })).toBe("SHOW GRANTS FOR 'reporter'@'%';");
expect(provider?.alterPasswordSql?.({ user: "reporter", host: "%" }, "new'secret")).toBe("SET PASSWORD FOR 'reporter'@'%' = PASSWORD('new''secret');");
expect(dorisPrivilegeTargetSql("analytics", "daily`rollup")).toBe("`internal`.`analytics`.`daily``rollup`");
expect(dorisGrantPrivilegesSql({ user: { user: "reporter", host: "%" }, privileges: ["select_priv", "LOAD_PRIV"], database: "analytics", grantOption: true })).toBe("GRANT SELECT_PRIV, LOAD_PRIV ON `internal`.`analytics`.* TO 'reporter'@'%';");
expect(provider?.alterLoginSql).toBeUndefined();
expect(provider?.privilegesForScope?.("table")).toEqual(["SELECT_PRIV", "LOAD_PRIV", "ALTER_PRIV", "CREATE_PRIV", "DROP_PRIV", "SHOW_VIEW_PRIV"]);
});
it("parses Doris SHOW GRANTS users and structured privileges", () => {
const grants = result(["UserIdentity", "Comment", "Password", "Roles", "GlobalPrivs", "DatabasePrivs", "TablePrivs"], [["'root'@'%'", "ROOT", "No", "operator", "Admin_priv", "internal.analytics: Select_priv", null]]);
expect(dorisUsersResult(grants)).toEqual([{ user: "root", host: "%" }]);
expect(dorisGrantsResult(grants)).toEqual(["Roles: operator", "GlobalPrivs: Admin_priv", "DatabasePrivs: internal.analytics: Select_priv"]);
});
it("syncs loaded grants only for the MySQL provider", () => {
const mysqlProvider = getDatabaseUserAdminProvider("mysql");
const dorisProvider = getDatabaseUserAdminProvider("doris");
const postgresProvider = getDatabaseUserAdminProvider("postgres");
const starrocksProvider = getDatabaseUserAdminProvider("starrocks");
@@ -64,9 +89,11 @@ describe("database user admin providers", () => {
}),
).toEqual({ privileges: ["INSERT"], grantOption: false });
expect(postgresProvider?.privilegeSelectionFromGrants).toBeUndefined();
expect(dorisProvider?.privilegeSelectionFromGrants).toBeUndefined();
expect(starrocksProvider?.privilegeSelectionFromGrants).toBeUndefined();
expect(mysqlProvider?.defaultPrivilegesForScope?.("mysql")).toEqual(["SELECT"]);
expect(postgresProvider?.defaultPrivilegesForScope?.("database")).toEqual(["CONNECT"]);
expect(dorisProvider?.defaultPrivilegesForScope?.("table")).toEqual(["SELECT_PRIV"]);
expect(starrocksProvider?.defaultPrivilegesForScope?.("table")).toEqual(["SELECT"]);
});
@@ -65,6 +65,7 @@ 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;
@@ -111,6 +112,30 @@ export function starrocksListUsersSql(): string {
return "SHOW USERS;";
}
export function dorisListUsersSql(): string {
return "SHOW ALL 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 [];
@@ -138,6 +163,10 @@ export function mysqlAlterUserPasswordSql(user: DatabaseUserIdentity, password:
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"};`;
}
@@ -186,6 +215,25 @@ export function starrocksRevokePrivilegesSql(input: PrivilegeChangeInput): strin
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]));
@@ -582,6 +630,24 @@ export const kingbaseUserAdminProvider: DatabaseUserAdminProvider = {
showGrantsSql: kingbaseShowGrantsSql,
};
export const dorisUserAdminProvider: DatabaseUserAdminProvider = {
dialect: "mysql",
defaultScope: "table",
listUsersSql: dorisListUsersSql,
parseUsers: 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",
@@ -603,6 +669,7 @@ export const starrocksUserAdminProvider: DatabaseUserAdminProvider = {
const DATABASE_USER_ADMIN_PROVIDER_BY_TYPE = new Map<DatabaseType, DatabaseUserAdminProvider>([
["mysql", mysqlUserAdminProvider],
["goldendb", mysqlUserAdminProvider],
["doris", dorisUserAdminProvider],
["kingbase", kingbaseUserAdminProvider],
["postgres", postgresUserAdminProvider],
["gaussdb", postgresUserAdminProvider],