From ec4f8582e6188318a1a98c0236af0c78678ea754 Mon Sep 17 00:00:00 2001 From: GIWTO <40267153+GIWTO@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:35:03 +0800 Subject: [PATCH] feat(dameng): add user and role administration --- .../src/components/admin/DamengRoleAdmin.vue | 536 ++++++++++++ .../src/components/admin/DamengUserAdmin.vue | 787 ++++++++++++++++++ .../src/components/layout/ContentArea.vue | 10 + .../sidebar/SidebarTreeRuntimeHost.vue | 33 + .../src/components/sidebar/TreeItem.vue | 6 +- apps/desktop/src/i18n/locales/en.ts | 110 +++ apps/desktop/src/i18n/locales/es.ts | 112 +++ apps/desktop/src/i18n/locales/it.ts | 112 +++ apps/desktop/src/i18n/locales/ja.ts | 110 +++ apps/desktop/src/i18n/locales/ko.ts | 110 +++ apps/desktop/src/i18n/locales/pt-BR.ts | 112 +++ apps/desktop/src/i18n/locales/zh-CN.ts | 110 +++ apps/desktop/src/i18n/locales/zh-TW.ts | 110 +++ .../src/lib/database/damengPrincipalAdmin.ts | 585 +++++++++++++ .../src/lib/sidebar/sidebarNodeOrdering.ts | 4 +- .../src/lib/sidebar/sidebarTreeItemLayout.ts | 2 + apps/desktop/src/lib/sidebar/treeNodeClick.ts | 2 + apps/desktop/src/lib/sidebar/treeNodeIcon.ts | 4 + apps/desktop/src/stores/connectionStore.ts | 32 +- apps/desktop/src/stores/queryStore.ts | 52 ++ apps/desktop/src/types/database.ts | 4 + .../2026-08-06-dameng-user-role-admin.md | 61 ++ .../app-tests/damengPrincipalAdmin.test.ts | 406 +++++++++ .../app-tests/sidebarNodeOrdering.test.ts | 22 + packages/app-tests/treeNodeClick.test.ts | 7 + 25 files changed, 3435 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/components/admin/DamengRoleAdmin.vue create mode 100644 apps/desktop/src/components/admin/DamengUserAdmin.vue create mode 100644 apps/desktop/src/lib/database/damengPrincipalAdmin.ts create mode 100644 docs/pips/plans/2026-08-06-dameng-user-role-admin.md create mode 100644 packages/app-tests/damengPrincipalAdmin.test.ts diff --git a/apps/desktop/src/components/admin/DamengRoleAdmin.vue b/apps/desktop/src/components/admin/DamengRoleAdmin.vue new file mode 100644 index 0000000000..8073c4f441 --- /dev/null +++ b/apps/desktop/src/components/admin/DamengRoleAdmin.vue @@ -0,0 +1,536 @@ + + + + + + + + {{ t("damengRoleAdmin.title") }} + {{ connection.name }} + + + + + {{ t("contextMenu.refreshChildren") }} + + + + {{ t("damengRoleAdmin.newRole") }} + + + + + {{ t("damengRoleAdmin.unsupported") }} + + + + + + + + + + + {{ selectedRole.role }} + + + + + {{ t("damengRoleAdmin.dropRole") }} + + + + + {{ t("damengRoleAdmin.members") }} + {{ t("damengRoleAdmin.systemPrivileges") }} + + + + + + {{ t("damengRoleAdmin.selectRole") }} + + + {{ t("damengRoleAdmin.loadFailed") }} + {{ detailError }} + + + + + {{ t("damengRoleAdmin.grantedToUsers") }} ({{ members.length }}) + + + {{ t("damengRoleAdmin.grantToUser") }} + + + + + + {{ member.grantee }} + {{ t("damengRoleAdmin.systemUser") }} + {{ t("damengRoleAdmin.builtinRole") }} + ADMIN OPTION + + + {{ t("damengRoleAdmin.revokeFromUser") }} + + + + {{ t("damengRoleAdmin.noMembers") }} + + + + + + + {{ t("damengRoleAdmin.grantedPrivileges") }} ({{ grantedPrivileges.length }}) + + + {{ t("damengRoleAdmin.grantPrivilege") }} + + + + + + {{ grant.privilege }} + ADMIN OPTION + + + {{ t("damengRoleAdmin.revokePrivilege") }} + + + + {{ t("damengRoleAdmin.noGrantedPrivileges") }} + + + + + + + + + + + {{ t("damengRoleAdmin.newRole") }} + + + + {{ t("damengRoleAdmin.roleName") }} + + + + + {{ t("common.cancel") }} + {{ t("damengRoleAdmin.previewSql") }} + + + + + + + + {{ t("damengRoleAdmin.grantToUser") }} + + + + {{ t("damengRoleAdmin.userToGrant") }} + + + + {{ user.username }} + + + + {{ t("damengRoleAdmin.grantUserHint", { role: selectedRole?.role ?? "" }) }} + + + {{ t("common.cancel") }} + {{ t("damengRoleAdmin.previewSql") }} + + + + + + + + {{ t("damengRoleAdmin.grantPrivilege") }} + + + + {{ t("damengRoleAdmin.privilegeToGrant") }} + + + + {{ privilege }} + + + + {{ t("damengRoleAdmin.anyPrivilegeDisabled") }} + {{ t("damengRoleAdmin.grantPrivilegeHint", { role: selectedRole?.role ?? "" }) }} + + + {{ t("common.cancel") }} + {{ t("damengRoleAdmin.previewSql") }} + + + + + + + + {{ t("damengRoleAdmin.sqlPreview") }} + + {{ pendingSql }} + + {{ t("common.cancel") }} + + + {{ t("damengRoleAdmin.applySql") }} + + + + + + diff --git a/apps/desktop/src/components/admin/DamengUserAdmin.vue b/apps/desktop/src/components/admin/DamengUserAdmin.vue new file mode 100644 index 0000000000..2cbb1169ae --- /dev/null +++ b/apps/desktop/src/components/admin/DamengUserAdmin.vue @@ -0,0 +1,787 @@ + + + + + + + + {{ t("damengUserAdmin.title") }} + {{ connection.name }} + + + + + {{ t("contextMenu.refreshChildren") }} + + + + {{ t("damengUserAdmin.newUser") }} + + + + + {{ t("damengUserAdmin.unsupported") }} + + + + + + + + + + + {{ selectedUser.username }} + {{ selectedUser.accountStatus }} + {{ t("damengUserAdmin.open") }} + {{ selectedUserCategoryLabel }} + + {{ t("damengUserAdmin.defaultTablespace") }}: {{ selectedUser.defaultTablespace || "-" }} · {{ t("damengUserAdmin.created") }}: {{ selectedUser.created || "-" }} + + + + {{ t("damengUserAdmin.changePassword") }} + + + + + {{ selectedUser.locked ? t("damengUserAdmin.unlock") : t("damengUserAdmin.lock") }} + + + + {{ t("damengUserAdmin.dropUser") }} + + + + + {{ t("damengUserAdmin.roles") }} + {{ t("damengUserAdmin.systemPrivileges") }} + + + + + + {{ t("damengUserAdmin.selectUser") }} + + + {{ t("damengUserAdmin.loadFailed") }} + {{ detailError }} + + + + + {{ t("damengUserAdmin.grantedRoles") }} ({{ grantedRoles.length }}) + + + {{ t("damengUserAdmin.grantRole") }} + + + + + + {{ grant.grantedRole }} + ADMIN OPTION + {{ t("damengUserAdmin.builtinRole") }} + {{ t("damengUserAdmin.inheritedFrom", { via: grant.via ?? "" }) }} + + + {{ t("damengUserAdmin.revokeRole") }} + + + + {{ t("damengUserAdmin.noGrantedRoles") }} + + + + + + + {{ t("damengUserAdmin.grantedPrivileges") }} ({{ grantedPrivileges.length }}) + + + {{ t("damengUserAdmin.grantPrivilege") }} + + + + + + {{ grant.privilege }} + ADMIN OPTION + + + {{ t("damengUserAdmin.revokePrivilege") }} + + + + {{ t("damengUserAdmin.noGrantedPrivileges") }} + + + + + + + + + + + {{ t("damengUserAdmin.newUser") }} + + + + {{ t("damengUserAdmin.username") }} + + + + {{ t("damengUserAdmin.userType") }} + + + + {{ groupLabel(group) }} + + + {{ t("damengUserAdmin.userTypeHint") }} + + + {{ t("damengUserAdmin.password") }} + + {{ t("damengUserAdmin.passwordPolicyHint") }} + + + {{ t("damengUserAdmin.tablespace") }} + + + + {{ space }} + + + + + + + {{ t("damengUserAdmin.locked") }} + + + + {{ t("common.cancel") }} + {{ t("damengUserAdmin.previewSql") }} + + + + + + + + {{ t("damengUserAdmin.changePassword") }} + + + + {{ t("damengUserAdmin.newPassword") }} + + {{ t("damengUserAdmin.passwordPolicyHint") }} + + + {{ t("damengUserAdmin.confirmPassword") }} + + + + + {{ t("common.cancel") }} + {{ t("damengUserAdmin.previewSql") }} + + + + + + + + {{ t("damengUserAdmin.grantRole") }} + + + + {{ t("damengUserAdmin.roleToGrant") }} + + + + {{ role.role }} + + + + {{ t("damengUserAdmin.grantRoleHint", { user: selectedUser?.username ?? "" }) }} + + + {{ t("common.cancel") }} + {{ t("damengUserAdmin.previewSql") }} + + + + + + + + {{ t("damengUserAdmin.grantPrivilege") }} + + + + {{ t("damengUserAdmin.privilegeToGrant") }} + + + + {{ privilege }} + + + + {{ t("damengUserAdmin.anyPrivilegeDisabled") }} + {{ t("damengUserAdmin.grantPrivilegeHint", { user: selectedUser?.username ?? "" }) }} + + + {{ t("common.cancel") }} + {{ t("damengUserAdmin.previewSql") }} + + + + + + + + {{ t("damengUserAdmin.sqlPreview") }} + + {{ pendingSql }} + + {{ t("common.cancel") }} + + + {{ t("damengUserAdmin.applySql") }} + + + + + + diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index e36c7f578d..6278ff6021 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -74,6 +74,8 @@ const ProcessListPanel = defineAsyncComponent(() => import("@/components/admin/P const MySqlDashboard = defineAsyncComponent(() => import("@/components/admin/MySqlDashboard.vue")); const PostgresDashboard = defineAsyncComponent(() => import("@/components/admin/PostgresDashboard.vue")); const DamengJobAdmin = defineAsyncComponent(() => import("@/components/admin/DamengJobAdmin.vue")); +const DamengUserAdmin = defineAsyncComponent(() => import("@/components/admin/DamengUserAdmin.vue")); +const DamengRoleAdmin = defineAsyncComponent(() => import("@/components/admin/DamengRoleAdmin.vue")); const ExplainPlanViewer = defineAsyncComponent(() => import("@/components/explain/ExplainPlanViewer.vue")); const QueryChart = defineAsyncComponent(() => import("@/components/chart/QueryChart.vue")); import { useQueryStore } from "@/stores/queryStore"; @@ -2119,6 +2121,14 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand + + + + + + + + diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index 1c6b84e512..cec334bf05 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -45,6 +45,7 @@ import { ListFilter, Clipboard, UsersRound, + ShieldCheck, Activity, Gauge, CalendarClock, @@ -725,6 +726,12 @@ async function toggle() { } else if (node.type === "user-admin" && node.connectionId) { await connectionStore.ensureConnected(node.connectionId); queryStore.openUserAdmin(node.connectionId); + } else if (node.type === "dameng-users" && node.connectionId) { + await connectionStore.ensureConnected(node.connectionId); + queryStore.openDamengUsers(node.connectionId); + } else if (node.type === "dameng-roles" && node.connectionId) { + await connectionStore.ensureConnected(node.connectionId); + queryStore.openDamengRoles(node.connectionId); } else if (node.type === "dameng-job-admin" && node.connectionId) { await connectionStore.ensureConnected(node.connectionId); queryStore.openDamengJobAdmin(node.connectionId); @@ -1301,6 +1308,30 @@ async function openServerDashboard() { } } +async function openDamengUsers() { + const node = activeNode.value; + if (!node.connectionId) return; + try { + await connectionStore.ensureConnected(node.connectionId); + connectionStore.activeConnectionId = node.connectionId; + queryStore.openDamengUsers(node.connectionId); + } catch (e: any) { + toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000); + } +} + +async function openDamengRoles() { + const node = activeNode.value; + if (!node.connectionId) return; + try { + await connectionStore.ensureConnected(node.connectionId); + connectionStore.activeConnectionId = node.connectionId; + queryStore.openDamengRoles(node.connectionId); + } catch (e: any) { + toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000); + } +} + async function openDamengJobAdmin() { const node = activeNode.value; if (!node.connectionId) return; @@ -4148,6 +4179,8 @@ function buildConnectionSidebarMenu(context: SidebarMenuFactoryContext): boolean items.push({ label: t("contextMenu.serverDashboard"), action: openServerDashboard, icon: Gauge }); } if (currentDatabaseType() === "dameng") { + items.push({ label: t("contextMenu.damengUsers"), action: openDamengUsers, icon: UsersRound }); + items.push({ label: t("contextMenu.damengRoles"), action: openDamengRoles, icon: ShieldCheck }); items.push({ label: t("contextMenu.damengJobAdmin"), action: openDamengJobAdmin, icon: CalendarClock }); } if (canCopyFinalProxyPort.value) { diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 82b832a6f9..16d62b7e03 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -255,6 +255,10 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null { return { icon: TableProperties, colorClass: "text-primary" }; case "user-admin": return { icon: UsersRound, colorClass: "text-primary" }; + case "dameng-users": + return { icon: UsersRound, colorClass: "text-primary" }; + case "dameng-roles": + return { icon: ShieldCheck, colorClass: "text-primary" }; case "dameng-job-admin": return { icon: CalendarClock, colorClass: "text-primary" }; case "index": @@ -349,7 +353,7 @@ function isGroupLabel(node: TreeNode): boolean { function displayLabel(node: TreeNode): string { if (node.type === "load-more") return t(node.label); if (node.type === "object-browser") return t(node.label, { count: node.objectCount ?? 0 }); - if (node.type === "user-admin" || node.type === "dameng-job-admin") return t(node.label); + if (node.type === "user-admin" || node.type === "dameng-users" || node.type === "dameng-roles" || node.type === "dameng-job-admin") return t(node.label); if (node.type === "linked-server-root") return t(node.label); if (node.type === "mqtt-topic" && node.id.endsWith(":mqtt-topic:__console__")) return t(node.label); if (node.label === "tree.defaultDatabase") return t(node.label); diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 3211a5240e..f9e266e201 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -2540,6 +2540,10 @@ export default { openUserAdmin: "Open Users & Privileges", processList: "Process List", serverDashboard: "Server Dashboard", + damengUsers: "Dameng Users", + openDamengUsers: "Open Dameng User Admin", + damengRoles: "Dameng Roles", + openDamengRoles: "Open Dameng Role Admin", damengJobAdmin: "Dameng Agent Jobs", openDamengJobAdmin: "Open Dameng Agent Jobs", duplicateConnection: "Duplicate Connection", @@ -2866,6 +2870,8 @@ export default { tree: { savedSql: "SQL Library", userAdmin: "Users & Privileges", + damengUsers: "Users", + damengRoles: "Roles", damengJobAdmin: "Agent Jobs", linkedServers: "Linked Servers", defaultDatabase: "Default DB", @@ -3114,6 +3120,110 @@ export default { applySuccess: "Job operation applied", applyFailed: "Execution failed: {message}", }, + damengUserAdmin: { + title: "Dameng Users", + unsupported: "This page only supports Dameng database connections.", + newUser: "New User", + searchUser: "Search users", + loadFailed: "Load failed", + emptyUsers: "No users, or the current account cannot read the user list.", + selectUser: "Select a user", + username: "Username", + usernamePlaceholder: "e.g. APP_USER", + password: "Password", + passwordPolicyHint: "Password must satisfy the DM8 password policy (default: at least 9 characters; ideally mix uppercase, lowercase and digits)", + tablespace: "Default tablespace", + tablespacePlaceholder: "Enter tablespace name", + locked: "Lock account after creation", + changePassword: "Change Password", + newPassword: "New password", + confirmPassword: "Confirm password", + open: "Open", + systemUser: "System user", + systemUserAdmin: "Admin user", + systemUserAuditor: "Audit user", + systemUserSecurity: "Security user", + systemUserSystem: "System user", + cannotChangePassword: "This user's password cannot be changed by the current connection (only SYSDBA or the user themselves can change it)", + inheritedFrom: "Inherited from {via}", + userType: "User type", + userTypeHint: "Admin automatically grants DBA, audit grants DB_AUDIT_ADMIN, security grants DB_POLICY_ADMIN", + groupOther: "Other users", + systemUserProtected: "System users cannot be altered or dropped", + builtinRole: "Built-in role", + builtinRoleProtected: "Built-in roles cannot be revoked", + defaultTablespace: "Default tablespace", + created: "Created", + lock: "Lock", + unlock: "Unlock", + dropUser: "Drop User", + roles: "Roles", + systemPrivileges: "System Privileges", + grantedRoles: "Granted roles", + grantedPrivileges: "Granted privileges", + grantRole: "Grant Role", + revokeRole: "Revoke Role", + roleToGrant: "Role to grant", + selectRole: "Select role", + grantRoleHint: "Grant the role to user {user}.", + noRolesToGrant: "No roles available to grant", + noGrantedRoles: "This user has no granted roles", + grantPrivilege: "Grant Privilege", + revokePrivilege: "Revoke Privilege", + privilegeToGrant: "Privilege to grant", + selectPrivilege: "Select privilege", + grantPrivilegeHint: "Grant the system privilege to user {user}.", + noPrivilegesToGrant: "No privileges available to grant", + noGrantedPrivileges: "This user has no granted system privileges", + previewSql: "Preview SQL", + sqlPreview: "SQL Preview", + applySql: "Apply SQL", + applySuccess: "User operation applied", + applyFailed: "Execution failed: {message}", + enableDdlAnyPrivHint: "Hint: DM8 disables granting DDL ANY privileges by default (ENABLE_DDL_ANY_PRIV=0). To grant ANY-type privileges, run as SYSDBA: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). For non-ANY privileges, make sure the current account holds the ADMIN OPTION for that privilege.", + anyPrivilegeDisabled: "ANY-type privileges are disabled by DM8 (ENABLE_DDL_ANY_PRIV=0) and cannot be granted or revoked", + }, + damengRoleAdmin: { + title: "Dameng Roles", + unsupported: "This page only supports Dameng database connections.", + newRole: "New Role", + searchRole: "Search roles", + loadFailed: "Load failed", + emptyRoles: "No roles, or the current account cannot read the role list.", + selectRole: "Select a role", + roleName: "Role name", + roleNamePlaceholder: "e.g. APP_ROLE", + members: "Members", + systemPrivileges: "System Privileges", + grantedToUsers: "Users granted this role", + grantedPrivileges: "Granted privileges", + noMembers: "No users have been granted this role", + grantToUser: "Grant to User", + userToGrant: "User to grant", + selectUser: "Select user", + grantUserHint: "Grant role {role} to the user.", + noUsersToGrant: "No users available to grant", + revokeFromUser: "Revoke", + systemUser: "System user", + systemUserProtected: "Cannot revoke from system users", + builtinRole: "Built-in role", + builtinRoleProtected: "Predefined roles cannot be dropped or modified", + grantPrivilege: "Grant Privilege", + revokePrivilege: "Revoke Privilege", + privilegeToGrant: "Privilege to grant", + selectPrivilege: "Select privilege", + grantPrivilegeHint: "Grant the system privilege to role {role}.", + noPrivilegesToGrant: "No privileges available to grant", + noGrantedPrivileges: "This role has no granted system privileges", + dropRole: "Drop Role", + previewSql: "Preview SQL", + sqlPreview: "SQL Preview", + applySql: "Apply SQL", + applySuccess: "Role operation applied", + applyFailed: "Execution failed: {message}", + enableDdlAnyPrivHint: "Hint: DM8 disables granting DDL ANY privileges by default (ENABLE_DDL_ANY_PRIV=0). To grant ANY-type privileges, run as SYSDBA: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). For non-ANY privileges, make sure the current account holds the ADMIN OPTION for that privilege.", + anyPrivilegeDisabled: "ANY-type privileges are disabled by DM8 (ENABLE_DDL_ANY_PRIV=0) and cannot be granted or revoked", + }, objects: { all: "All", tables: "Tables", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index fcc5bc137d..aa4b783c2c 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -2474,6 +2474,10 @@ export default withEnglishFallback({ openUserAdmin: "Abrir Usuarios y Privilegios", damengJobAdmin: "Trabajos del agente Dameng", openDamengJobAdmin: "Abrir trabajos del agente Dameng", + damengUsers: "Usuarios Dameng", + openDamengUsers: "Abrir administración de usuarios Dameng", + damengRoles: "Roles Dameng", + openDamengRoles: "Abrir administración de roles Dameng", duplicateConnection: "Duplicar conexión", duplicateSelectedConnections: "Duplicar {count} conexiones seleccionadas", instanceInfo: "Información de la instancia", @@ -2800,6 +2804,8 @@ export default withEnglishFallback({ tree: { savedSql: "Biblioteca SQL", userAdmin: "Usuarios y Privilegios", + damengUsers: "Usuarios", + damengRoles: "Roles", damengJobAdmin: "Trabajos del agente", linkedServers: "Servidores vinculados", defaultDatabase: "Base predeterminada", @@ -2959,6 +2965,112 @@ export default withEnglishFallback({ applySuccess: "Operación de trabajo aplicada", applyFailed: "Ejecución fallida: {message}", }, + damengUserAdmin: { + title: "Usuarios Dameng", + unsupported: "Esta página solo admite conexiones a bases de datos Dameng.", + newUser: "Nuevo usuario", + searchUser: "Buscar usuarios", + loadFailed: "Error al cargar", + emptyUsers: "Sin usuarios, o la cuenta actual no puede leer la lista de usuarios.", + selectUser: "Seleccione un usuario", + username: "Nombre de usuario", + usernamePlaceholder: "p. ej. APP_USER", + password: "Contraseña", + passwordPolicyHint: "La contraseña debe cumplir la política de contraseñas de DM8 (por defecto: al menos 9 caracteres; idealmente combine mayúsculas, minúsculas y dígitos)", + tablespace: "Tablespace predeterminado", + tablespacePlaceholder: "Introduzca el nombre del tablespace", + locked: "Bloquear cuenta tras la creación", + changePassword: "Cambiar contraseña", + newPassword: "Nueva contraseña", + confirmPassword: "Confirmar contraseña", + open: "Abierto", + systemUser: "Usuario del sistema", + systemUserAdmin: "Usuario administrador", + systemUserAuditor: "Usuario de auditoría", + systemUserSecurity: "Usuario de seguridad", + systemUserSystem: "Usuario del sistema", + cannotChangePassword: "La contraseña de este usuario no se puede cambiar desde la conexión actual (solo SYSDBA o el propio usuario puede cambiarla)", + inheritedFrom: "Heredado de {via}", + userType: "Tipo de usuario", + userTypeHint: "Administrador otorga automáticamente DBA, auditoría otorga DB_AUDIT_ADMIN, seguridad otorga DB_POLICY_ADMIN", + groupOther: "Otros usuarios", + systemUserProtected: "Los usuarios del sistema no se pueden modificar ni eliminar", + builtinRole: "Rol integrado", + builtinRoleProtected: "Los roles integrados no se pueden revocar", + defaultTablespace: "Tablespace predeterminado", + created: "Creado", + lock: "Bloquear", + unlock: "Desbloquear", + dropUser: "Eliminar usuario", + roles: "Roles", + systemPrivileges: "Privilegios del sistema", + grantedRoles: "Roles concedidos", + grantedPrivileges: "Privilegios concedidos", + grantRole: "Conceder rol", + revokeRole: "Revocar rol", + roleToGrant: "Rol a conceder", + selectRole: "Seleccionar rol", + grantRoleHint: "Conceder el rol al usuario {user}.", + noRolesToGrant: "No hay roles disponibles para conceder", + noGrantedRoles: "Este usuario no tiene roles concedidos", + grantPrivilege: "Conceder privilegio", + revokePrivilege: "Revocar privilegio", + privilegeToGrant: "Privilegio a conceder", + selectPrivilege: "Seleccionar privilegio", + grantPrivilegeHint: "Conceder el privilegio del sistema al usuario {user}.", + noPrivilegesToGrant: "No hay privilegios disponibles para conceder", + noGrantedPrivileges: "Este usuario no tiene privilegios del sistema concedidos", + previewSql: "Vista previa SQL", + sqlPreview: "Vista previa SQL", + applySql: "Ejecutar SQL", + applySuccess: "Operación de usuario aplicada", + applyFailed: "Error de ejecución: {message}", + enableDdlAnyPrivHint: + "Sugerencia: DM8 deshabilita la concesión de privilegios ANY de DDL de forma predeterminada (ENABLE_DDL_ANY_PRIV=0). Para otorgar privilegios ANY, ejecute como SYSDBA: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). Para privilegios no ANY, asegúrese de que la cuenta actual tenga el ADMIN OPTION de ese privilegio.", + anyPrivilegeDisabled: "Los privilegios de tipo ANY están deshabilitados por DM8 (ENABLE_DDL_ANY_PRIV=0) y no se pueden otorgar ni revocar", + }, + damengRoleAdmin: { + title: "Roles Dameng", + unsupported: "Esta página solo admite conexiones a bases de datos Dameng.", + newRole: "Nuevo rol", + searchRole: "Buscar roles", + loadFailed: "Error al cargar", + emptyRoles: "Sin roles, o la cuenta actual no puede leer la lista de roles.", + selectRole: "Seleccione un rol", + roleName: "Nombre del rol", + roleNamePlaceholder: "p. ej. APP_ROLE", + members: "Miembros", + systemPrivileges: "Privilegios del sistema", + grantedToUsers: "Usuarios con este rol", + grantedPrivileges: "Privilegios concedidos", + noMembers: "Ningún usuario tiene este rol concedido", + grantToUser: "Conceder a usuario", + userToGrant: "Usuario a conceder", + selectUser: "Seleccionar usuario", + grantUserHint: "Conceder el rol {role} al usuario.", + noUsersToGrant: "No hay usuarios disponibles para conceder", + revokeFromUser: "Revocar", + systemUser: "Usuario del sistema", + systemUserProtected: "No se puede revocar de usuarios del sistema", + builtinRole: "Rol integrado", + builtinRoleProtected: "Los roles predefinidos no se pueden eliminar ni modificar", + grantPrivilege: "Conceder privilegio", + revokePrivilege: "Revocar privilegio", + privilegeToGrant: "Privilegio a conceder", + selectPrivilege: "Seleccionar privilegio", + grantPrivilegeHint: "Conceder el privilegio del sistema al rol {role}.", + noPrivilegesToGrant: "No hay privilegios disponibles para conceder", + noGrantedPrivileges: "Este rol no tiene privilegios del sistema concedidos", + dropRole: "Eliminar rol", + previewSql: "Vista previa SQL", + sqlPreview: "Vista previa SQL", + applySql: "Ejecutar SQL", + applySuccess: "Operación de rol aplicada", + applyFailed: "Error de ejecución: {message}", + enableDdlAnyPrivHint: + "Sugerencia: DM8 deshabilita la concesión de privilegios ANY de DDL de forma predeterminada (ENABLE_DDL_ANY_PRIV=0). Para otorgar privilegios ANY, ejecute como SYSDBA: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). Para privilegios no ANY, asegúrese de que la cuenta actual tenga el ADMIN OPTION de ese privilegio.", + anyPrivilegeDisabled: "Los privilegios de tipo ANY están deshabilitados por DM8 (ENABLE_DDL_ANY_PRIV=0) y no se pueden otorgar ni revocar", + }, objects: { all: "Todos", tables: "Tablas", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 9393c9ebae..563b4aa4af 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -2472,6 +2472,10 @@ export default withEnglishFallback({ openUserAdmin: "Apri Utenti e Privilegi", damengJobAdmin: "Job agente Dameng", openDamengJobAdmin: "Apri job agente Dameng", + damengUsers: "Utenti Dameng", + openDamengUsers: "Apri gestione utenti Dameng", + damengRoles: "Ruoli Dameng", + openDamengRoles: "Apri gestione ruoli Dameng", duplicateConnection: "Duplica Connessione", duplicateSelectedConnections: "Duplica {count} connessioni selezionate", newQuery: "Nuova Query", @@ -2798,6 +2802,8 @@ export default withEnglishFallback({ tree: { savedSql: "Libreria SQL", userAdmin: "Utenti e Privilegi", + damengUsers: "Utenti", + damengRoles: "Ruoli", damengJobAdmin: "Job agente", linkedServers: "Server Collegati", defaultDatabase: "DB Predefinito", @@ -2957,6 +2963,112 @@ export default withEnglishFallback({ applySuccess: "Operazione job applicata", applyFailed: "Esecuzione non riuscita: {message}", }, + damengUserAdmin: { + title: "Utenti Dameng", + unsupported: "Questa pagina supporta solo connessioni a database Dameng.", + newUser: "Nuovo utente", + searchUser: "Cerca utenti", + loadFailed: "Caricamento non riuscito", + emptyUsers: "Nessun utente o l'account corrente non può leggere l'elenco utenti.", + selectUser: "Seleziona un utente", + username: "Nome utente", + usernamePlaceholder: "es. APP_USER", + password: "Password", + passwordPolicyHint: "La password deve soddisfare i criteri DM8 (default: almeno 9 caratteri; idealmente maiuscole, minuscole e cifre)", + tablespace: "Tablespace predefinito", + tablespacePlaceholder: "Inserisci il nome del tablespace", + locked: "Blocca account dopo la creazione", + changePassword: "Cambia password", + newPassword: "Nuova password", + confirmPassword: "Conferma password", + open: "Aperto", + systemUser: "Utente di sistema", + systemUserAdmin: "Utente amministratore", + systemUserAuditor: "Utente audit", + systemUserSecurity: "Utente di sicurezza", + systemUserSystem: "Utente di sistema", + cannotChangePassword: "La password di questo utente non può essere modificata dalla connessione corrente (solo SYSDBA o l'utente stesso possono modificarla)", + inheritedFrom: "Ereditato da {via}", + userType: "Tipo di utente", + userTypeHint: "Amministratore concede automaticamente DBA, audit concede DB_AUDIT_ADMIN, sicurezza concede DB_POLICY_ADMIN", + groupOther: "Altri utenti", + systemUserProtected: "Gli utenti di sistema non possono essere modificati o eliminati", + builtinRole: "Ruolo integrato", + builtinRoleProtected: "I ruoli integrati non possono essere revocati", + defaultTablespace: "Tablespace predefinito", + created: "Creato", + lock: "Blocca", + unlock: "Sblocca", + dropUser: "Elimina utente", + roles: "Ruoli", + systemPrivileges: "Privilegi di sistema", + grantedRoles: "Ruoli concessi", + grantedPrivileges: "Privilegi concessi", + grantRole: "Concedi ruolo", + revokeRole: "Revoca ruolo", + roleToGrant: "Ruolo da concedere", + selectRole: "Seleziona ruolo", + grantRoleHint: "Concedi il ruolo all'utente {user}.", + noRolesToGrant: "Nessun ruolo disponibile da concedere", + noGrantedRoles: "Questo utente non ha ruoli concessi", + grantPrivilege: "Concedi privilegio", + revokePrivilege: "Revoca privilegio", + privilegeToGrant: "Privilegio da concedere", + selectPrivilege: "Seleziona privilegio", + grantPrivilegeHint: "Concedi il privilegio di sistema all'utente {user}.", + noPrivilegesToGrant: "Nessun privilegio disponibile da concedere", + noGrantedPrivileges: "Questo utente non ha privilegi di sistema concessi", + previewSql: "Anteprima SQL", + sqlPreview: "Anteprima SQL", + applySql: "Esegui SQL", + applySuccess: "Operazione utente applicata", + applyFailed: "Esecuzione non riuscita: {message}", + enableDdlAnyPrivHint: + "Suggerimento: DM8 disabilita la concessione dei privilegi ANY DDL per impostazione predefinita (ENABLE_DDL_ANY_PRIV=0). Per concedere privilegi ANY, esegui come SYSDBA: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). Per privilegi non ANY, assicurati che l'account corrente abbia l'ADMIN OPTION per quel privilegio.", + anyPrivilegeDisabled: "I privilegi di tipo ANY sono disabilitati da DM8 (ENABLE_DDL_ANY_PRIV=0) e non possono essere concessi o revocati", + }, + damengRoleAdmin: { + title: "Ruoli Dameng", + unsupported: "Questa pagina supporta solo connessioni a database Dameng.", + newRole: "Nuovo ruolo", + searchRole: "Cerca ruoli", + loadFailed: "Caricamento non riuscito", + emptyRoles: "Nessun ruolo o l'account corrente non può leggere l'elenco ruoli.", + selectRole: "Seleziona un ruolo", + roleName: "Nome ruolo", + roleNamePlaceholder: "es. APP_ROLE", + members: "Membri", + systemPrivileges: "Privilegi di sistema", + grantedToUsers: "Utenti con questo ruolo", + grantedPrivileges: "Privilegi concessi", + noMembers: "Nessun utente ha questo ruolo concesso", + grantToUser: "Concedi all'utente", + userToGrant: "Utente a cui concedere", + selectUser: "Seleziona utente", + grantUserHint: "Concedi il ruolo {role} all'utente.", + noUsersToGrant: "Nessun utente disponibile da concedere", + revokeFromUser: "Revoca", + systemUser: "Utente di sistema", + systemUserProtected: "Impossibile revocare dagli utenti di sistema", + builtinRole: "Ruolo integrato", + builtinRoleProtected: "I ruoli predefiniti non possono essere eliminati o modificati", + grantPrivilege: "Concedi privilegio", + revokePrivilege: "Revoca privilegio", + privilegeToGrant: "Privilegio da concedere", + selectPrivilege: "Seleziona privilegio", + grantPrivilegeHint: "Concedi il privilegio di sistema al ruolo {role}.", + noPrivilegesToGrant: "Nessun privilegio disponibile da concedere", + noGrantedPrivileges: "Questo ruolo non ha privilegi di sistema concessi", + dropRole: "Elimina ruolo", + previewSql: "Anteprima SQL", + sqlPreview: "Anteprima SQL", + applySql: "Esegui SQL", + applySuccess: "Operazione ruolo applicata", + applyFailed: "Esecuzione non riuscita: {message}", + enableDdlAnyPrivHint: + "Suggerimento: DM8 disabilita la concessione dei privilegi ANY DDL per impostazione predefinita (ENABLE_DDL_ANY_PRIV=0). Per concedere privilegi ANY, esegui come SYSDBA: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). Per privilegi non ANY, assicurati che l'account corrente abbia l'ADMIN OPTION per quel privilegio.", + anyPrivilegeDisabled: "I privilegi di tipo ANY sono disabilitati da DM8 (ENABLE_DDL_ANY_PRIV=0) e non possono essere concessi o revocati", + }, objects: { all: "Tutti", tables: "Tabelle", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 838291ea03..4450449462 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -2498,6 +2498,10 @@ export default withEnglishFallback({ openUserAdmin: "ユーザーと権限を開く", damengJobAdmin: "Dameng エージェントジョブ", openDamengJobAdmin: "Dameng エージェントジョブを開く", + damengUsers: "Dameng ユーザー", + openDamengUsers: "Dameng ユーザー管理を開く", + damengRoles: "Dameng ロール", + openDamengRoles: "Dameng ロール管理を開く", duplicateConnection: "接続を複製", duplicateSelectedConnections: "選択した{count}件の接続を複製", newQuery: "新しいクエリ", @@ -2825,6 +2829,8 @@ export default withEnglishFallback({ tree: { savedSql: "SQLライブラリ", userAdmin: "ユーザーと権限", + damengUsers: "ユーザー", + damengRoles: "ロール", damengJobAdmin: "エージェントジョブ", defaultDatabase: "デフォルトDB", columns: "列", @@ -3019,6 +3025,110 @@ export default withEnglishFallback({ applySuccess: "ジョブ操作を適用しました", applyFailed: "実行に失敗しました: {message}", }, + damengUserAdmin: { + title: "Dameng ユーザー", + unsupported: "このページは Dameng データベース接続のみサポートしています。", + newUser: "ユーザー作成", + searchUser: "ユーザーを検索", + loadFailed: "読み込み失敗", + emptyUsers: "ユーザーがありません。または現在のアカウントにユーザー一覧の読み取り権限がありません。", + selectUser: "ユーザーを選択してください", + username: "ユーザー名", + usernamePlaceholder: "例: APP_USER", + password: "パスワード", + passwordPolicyHint: "パスワードはDM8のパスワードポリシーを満たす必要があります(デフォルト: 9文字以上。大文字・小文字・数字の組み合わせ推奨)", + tablespace: "デフォルト表領域", + tablespacePlaceholder: "表領域名を入力", + locked: "作成後にアカウントをロック", + changePassword: "パスワード変更", + newPassword: "新しいパスワード", + confirmPassword: "パスワード確認", + open: "通常", + systemUser: "システムユーザー", + systemUserAdmin: "管理ユーザー", + systemUserAuditor: "監査ユーザー", + systemUserSecurity: "セキュリティユーザー", + systemUserSystem: "システムユーザー", + cannotChangePassword: "このユーザーのパスワードは現在の接続では変更できません(変更できるのは SYSDBA またはユーザー本人のみ)", + inheritedFrom: "{via} から継承", + userType: "ユーザータイプ", + userTypeHint: "管理ユーザーはDBA、監査ユーザーはDB_AUDIT_ADMIN、セキュリティユーザーはDB_POLICY_ADMINを自動付与します", + groupOther: "その他のユーザー", + systemUserProtected: "システムユーザーは変更・削除できません", + builtinRole: "組み込みロール", + builtinRoleProtected: "組み込みロールは回収できません", + defaultTablespace: "デフォルト表領域", + created: "作成日時", + lock: "ロック", + unlock: "ロック解除", + dropUser: "ユーザー削除", + roles: "ロール", + systemPrivileges: "システム権限", + grantedRoles: "付与済みロール", + grantedPrivileges: "付与済み権限", + grantRole: "ロール付与", + revokeRole: "ロール回収", + roleToGrant: "付与するロール", + selectRole: "ロールを選択", + grantRoleHint: "ユーザー {user} にロールを付与します。", + noRolesToGrant: "付与できるロールがありません", + noGrantedRoles: "このユーザーには付与されたロールがありません", + grantPrivilege: "権限付与", + revokePrivilege: "権限回収", + privilegeToGrant: "付与する権限", + selectPrivilege: "権限を選択", + grantPrivilegeHint: "ユーザー {user} にシステム権限を付与します。", + noPrivilegesToGrant: "付与できる権限がありません", + noGrantedPrivileges: "このユーザーには付与されたシステム権限がありません", + previewSql: "SQL プレビュー", + sqlPreview: "SQL プレビュー", + applySql: "SQL を実行", + applySuccess: "ユーザー操作を実行しました", + applyFailed: "実行に失敗しました: {message}", + enableDdlAnyPrivHint: "ヒント: DM8はデフォルトでDDL ANY権限の付与を無効にしています(ENABLE_DDL_ANY_PRIV=0)。ANY系権限を付与するにはSYSDBAで実行: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1)。ANY以外の権限は、現在のアカウントがその権限のADMIN OPTIONを持っている必要があります。", + anyPrivilegeDisabled: "ANY系権限はDM8で無効化されています(ENABLE_DDL_ANY_PRIV=0)ため、付与・取り消しはできません", + }, + damengRoleAdmin: { + title: "Dameng ロール", + unsupported: "このページは Dameng データベース接続のみサポートしています。", + newRole: "ロール作成", + searchRole: "ロールを検索", + loadFailed: "読み込み失敗", + emptyRoles: "ロールがありません。または現在のアカウントにロール一覧の読み取り権限がありません。", + selectRole: "ロールを選択してください", + roleName: "ロール名", + roleNamePlaceholder: "例: APP_ROLE", + members: "メンバー", + systemPrivileges: "システム権限", + grantedToUsers: "このロールが付与されたユーザー", + grantedPrivileges: "付与済み権限", + noMembers: "このロールが付与されたユーザーはいません", + grantToUser: "ユーザーに付与", + userToGrant: "付与するユーザー", + selectUser: "ユーザーを選択", + grantUserHint: "ロール {role} をユーザーに付与します。", + noUsersToGrant: "付与できるユーザーがいません", + revokeFromUser: "回収", + systemUser: "システムユーザー", + systemUserProtected: "システムユーザーからは回収できません", + builtinRole: "組み込みロール", + builtinRoleProtected: "定義済みロールは削除・変更できません", + grantPrivilege: "権限付与", + revokePrivilege: "権限回収", + privilegeToGrant: "付与する権限", + selectPrivilege: "権限を選択", + grantPrivilegeHint: "ロール {role} にシステム権限を付与します。", + noPrivilegesToGrant: "付与できる権限がありません", + noGrantedPrivileges: "このロールには付与されたシステム権限がありません", + dropRole: "ロール削除", + previewSql: "SQL プレビュー", + sqlPreview: "SQL プレビュー", + applySql: "SQL を実行", + applySuccess: "ロール操作を実行しました", + applyFailed: "実行に失敗しました: {message}", + enableDdlAnyPrivHint: "ヒント: DM8はデフォルトでDDL ANY権限の付与を無効にしています(ENABLE_DDL_ANY_PRIV=0)。ANY系権限を付与するにはSYSDBAで実行: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1)。ANY以外の権限は、現在のアカウントがその権限のADMIN OPTIONを持っている必要があります。", + anyPrivilegeDisabled: "ANY系権限はDM8で無効化されています(ENABLE_DDL_ANY_PRIV=0)ため、付与・取り消しはできません", + }, objects: { all: "すべて", tables: "テーブル", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index ccae270362..9158ecc338 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -2399,6 +2399,10 @@ export default withEnglishFallback({ serverDashboard: "서버 대시보드", damengJobAdmin: "Dameng 에이전트 작업", openDamengJobAdmin: "Dameng 에이전트 작업 열기", + damengUsers: "Dameng 사용자", + openDamengUsers: "Dameng 사용자 관리 열기", + damengRoles: "Dameng 역할", + openDamengRoles: "Dameng 역할 관리 열기", duplicateConnection: "연결 복제", duplicateSelectedConnections: "선택한 연결 {count}개 복제", newQuery: "새 쿼리", @@ -2717,6 +2721,8 @@ export default withEnglishFallback({ tree: { savedSql: "SQL 라이브러리", userAdmin: "사용자 및 권한", + damengUsers: "사용자", + damengRoles: "역할", damengJobAdmin: "에이전트 작업", linkedServers: "연결된 서버", defaultDatabase: "기본 DB", @@ -2964,6 +2970,110 @@ export default withEnglishFallback({ applySuccess: "작업 작업이 적용되었습니다", applyFailed: "실행 실패: {message}", }, + damengUserAdmin: { + title: "Dameng 사용자", + unsupported: "이 페이지는 Dameng 데이터베이스 연결만 지원합니다.", + newUser: "새 사용자", + searchUser: "사용자 검색", + loadFailed: "불러오기 실패", + emptyUsers: "사용자가 없거나 현재 계정에 사용자 목록을 읽을 권한이 없습니다.", + selectUser: "사용자를 선택하세요", + username: "사용자 이름", + usernamePlaceholder: "예: APP_USER", + password: "비밀번호", + passwordPolicyHint: "비밀번호는 DM8 암호 정책을 충족해야 합니다(기본: 9자 이상, 대소문자와 숫자 조합 권장)", + tablespace: "기본 테이블스페이스", + tablespacePlaceholder: "테이블스페이스 이름 입력", + locked: "생성 후 계정 잠금", + changePassword: "비밀번호 변경", + newPassword: "새 비밀번호", + confirmPassword: "비밀번호 확인", + open: "정상", + systemUser: "시스템 사용자", + systemUserAdmin: "관리 사용자", + systemUserAuditor: "감사 사용자", + systemUserSecurity: "보안 사용자", + systemUserSystem: "시스템 사용자", + cannotChangePassword: "이 사용자의 비밀번호는 현재 연결에서 변경할 수 없습니다(SYSDBA 또는 사용자 본인만 변경 가능)", + inheritedFrom: "{via}에서 상속됨", + userType: "사용자 유형", + userTypeHint: "관리 사용자는 DBA, 감사 사용자는 DB_AUDIT_ADMIN, 보안 사용자는 DB_POLICY_ADMIN을 자동 부여합니다", + groupOther: "기타 사용자", + systemUserProtected: "시스템 사용자는 수정하거나 삭제할 수 없습니다", + builtinRole: "내장 역할", + builtinRoleProtected: "내장 역할은 회수할 수 없습니다", + defaultTablespace: "기본 테이블스페이스", + created: "생성 시간", + lock: "잠금", + unlock: "잠금 해제", + dropUser: "사용자 삭제", + roles: "역할", + systemPrivileges: "시스템 권한", + grantedRoles: "부여된 역할", + grantedPrivileges: "부여된 권한", + grantRole: "역할 부여", + revokeRole: "역할 회수", + roleToGrant: "부여할 역할", + selectRole: "역할 선택", + grantRoleHint: "사용자 {user}에게 역할을 부여합니다.", + noRolesToGrant: "부여할 수 있는 역할이 없습니다", + noGrantedRoles: "이 사용자에게 부여된 역할이 없습니다", + grantPrivilege: "권한 부여", + revokePrivilege: "권한 회수", + privilegeToGrant: "부여할 권한", + selectPrivilege: "권한 선택", + grantPrivilegeHint: "사용자 {user}에게 시스템 권한을 부여합니다.", + noPrivilegesToGrant: "부여할 수 있는 권한이 없습니다", + noGrantedPrivileges: "이 사용자에게 부여된 시스템 권한이 없습니다", + previewSql: "SQL 미리보기", + sqlPreview: "SQL 미리보기", + applySql: "SQL 실행", + applySuccess: "사용자 작업이 실행되었습니다", + applyFailed: "실행 실패: {message}", + enableDdlAnyPrivHint: "힌트: DM8은 기본적으로 DDL ANY 권한 부여를 비활성화합니다(ENABLE_DDL_ANY_PRIV=0). ANY 권한을 부여하려면 SYSDBA로 실행: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). ANY가 아닌 권한은 현재 계정에 해당 권한의 ADMIN OPTION이 있어야 합니다.", + anyPrivilegeDisabled: "ANY 유형 권한은 DM8에서 비활성화되어(ENABLE_DDL_ANY_PRIV=0) 부여하거나 회수할 수 없습니다", + }, + damengRoleAdmin: { + title: "Dameng 역할", + unsupported: "이 페이지는 Dameng 데이터베이스 연결만 지원합니다.", + newRole: "새 역할", + searchRole: "역할 검색", + loadFailed: "불러오기 실패", + emptyRoles: "역할이 없거나 현재 계정에 역할 목록을 읽을 권한이 없습니다.", + selectRole: "역할을 선택하세요", + roleName: "역할 이름", + roleNamePlaceholder: "예: APP_ROLE", + members: "멤버", + systemPrivileges: "시스템 권한", + grantedToUsers: "이 역할이 부여된 사용자", + grantedPrivileges: "부여된 권한", + noMembers: "이 역할이 부여된 사용자가 없습니다", + grantToUser: "사용자에게 부여", + userToGrant: "부여할 사용자", + selectUser: "사용자 선택", + grantUserHint: "역할 {role}을 사용자에게 부여합니다.", + noUsersToGrant: "부여할 수 있는 사용자가 없습니다", + revokeFromUser: "회수", + systemUser: "시스템 사용자", + systemUserProtected: "시스템 사용자에게서는 회수할 수 없습니다", + builtinRole: "내장 역할", + builtinRoleProtected: "미리 정의된 역할은 삭제하거나 수정할 수 없습니다", + grantPrivilege: "권한 부여", + revokePrivilege: "권한 회수", + privilegeToGrant: "부여할 권한", + selectPrivilege: "권한 선택", + grantPrivilegeHint: "역할 {role}에 시스템 권한을 부여합니다.", + noPrivilegesToGrant: "부여할 수 있는 권한이 없습니다", + noGrantedPrivileges: "이 역할에 부여된 시스템 권한이 없습니다", + dropRole: "역할 삭제", + previewSql: "SQL 미리보기", + sqlPreview: "SQL 미리보기", + applySql: "SQL 실행", + applySuccess: "역할 작업이 실행되었습니다", + applyFailed: "실행 실패: {message}", + enableDdlAnyPrivHint: "힌트: DM8은 기본적으로 DDL ANY 권한 부여를 비활성화합니다(ENABLE_DDL_ANY_PRIV=0). ANY 권한을 부여하려면 SYSDBA로 실행: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). ANY가 아닌 권한은 현재 계정에 해당 권한의 ADMIN OPTION이 있어야 합니다.", + anyPrivilegeDisabled: "ANY 유형 권한은 DM8에서 비활성화되어(ENABLE_DDL_ANY_PRIV=0) 부여하거나 회수할 수 없습니다", + }, objects: { all: "전체", tables: "테이블", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 2e029d6be8..687dd95f8a 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -2474,6 +2474,10 @@ export default withEnglishFallback({ openUserAdmin: "Abrir Usuários e Privilégios", damengJobAdmin: "Jobs do agente Dameng", openDamengJobAdmin: "Abrir jobs do agente Dameng", + damengUsers: "Usuários Dameng", + openDamengUsers: "Abrir administração de usuários Dameng", + damengRoles: "Funções Dameng", + openDamengRoles: "Abrir administração de funções Dameng", duplicateConnection: "Duplicar Conexão", duplicateSelectedConnections: "Duplicar {count} conexões selecionadas", instanceInfo: "Informações da Instância", @@ -2800,6 +2804,8 @@ export default withEnglishFallback({ tree: { savedSql: "Biblioteca SQL", userAdmin: "Usuários e Privilégios", + damengUsers: "Usuários", + damengRoles: "Funções", damengJobAdmin: "Jobs do agente", linkedServers: "Servidores Vinculados", materializedViews: "Visualizações Materializadas", @@ -2959,6 +2965,112 @@ export default withEnglishFallback({ applySuccess: "Operação de job aplicada", applyFailed: "Execução falhou: {message}", }, + damengUserAdmin: { + title: "Usuários Dameng", + unsupported: "Esta página só suporta conexões de banco de dados Dameng.", + newUser: "Novo usuário", + searchUser: "Pesquisar usuários", + loadFailed: "Falha ao carregar", + emptyUsers: "Nenhum usuário, ou a conta atual não pode ler a lista de usuários.", + selectUser: "Selecione um usuário", + username: "Nome de usuário", + usernamePlaceholder: "ex.: APP_USER", + password: "Senha", + passwordPolicyHint: "A senha deve atender à política de senhas do DM8 (padrão: pelo menos 9 caracteres; idealmente combine maiúsculas, minúsculas e dígitos)", + tablespace: "Tablespace padrão", + tablespacePlaceholder: "Digite o nome do tablespace", + locked: "Bloquear conta após a criação", + changePassword: "Alterar senha", + newPassword: "Nova senha", + confirmPassword: "Confirmar senha", + open: "Aberto", + systemUser: "Usuário do sistema", + systemUserAdmin: "Usuário administrador", + systemUserAuditor: "Usuário de auditoria", + systemUserSecurity: "Usuário de segurança", + systemUserSystem: "Usuário do sistema", + cannotChangePassword: "A senha deste usuário não pode ser alterada pela conexão atual (apenas SYSDBA ou o próprio usuário pode alterá-la)", + inheritedFrom: "Herdado de {via}", + userType: "Tipo de usuário", + userTypeHint: "Administrador concede automaticamente DBA, auditoria concede DB_AUDIT_ADMIN, segurança concede DB_POLICY_ADMIN", + groupOther: "Outros usuários", + systemUserProtected: "Usuários do sistema não podem ser alterados ou removidos", + builtinRole: "Papel integrado", + builtinRoleProtected: "Papéis integrados não podem ser revogados", + defaultTablespace: "Tablespace padrão", + created: "Criado em", + lock: "Bloquear", + unlock: "Desbloquear", + dropUser: "Excluir usuário", + roles: "Funções", + systemPrivileges: "Privilégios do sistema", + grantedRoles: "Funções concedidas", + grantedPrivileges: "Privilégios concedidos", + grantRole: "Conceder função", + revokeRole: "Revogar função", + roleToGrant: "Função a conceder", + selectRole: "Selecionar função", + grantRoleHint: "Conceder a função ao usuário {user}.", + noRolesToGrant: "Nenhuma função disponível para conceder", + noGrantedRoles: "Este usuário não possui funções concedidas", + grantPrivilege: "Conceder privilégio", + revokePrivilege: "Revogar privilégio", + privilegeToGrant: "Privilégio a conceder", + selectPrivilege: "Selecionar privilégio", + grantPrivilegeHint: "Conceder o privilégio de sistema ao usuário {user}.", + noPrivilegesToGrant: "Nenhum privilégio disponível para conceder", + noGrantedPrivileges: "Este usuário não possui privilégios de sistema concedidos", + previewSql: "Pré-visualizar SQL", + sqlPreview: "Pré-visualização SQL", + applySql: "Executar SQL", + applySuccess: "Operação de usuário aplicada", + applyFailed: "Falha na execução: {message}", + enableDdlAnyPrivHint: + "Dica: o DM8 desabilita a concessão de privilégios ANY de DDL por padrão (ENABLE_DDL_ANY_PRIV=0). Para conceder privilégios ANY, execute como SYSDBA: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). Para privilégios não ANY, certifique-se de que a conta atual tenha a ADMIN OPTION desse privilégio.", + anyPrivilegeDisabled: "Privilégios do tipo ANY estão desabilitados pelo DM8 (ENABLE_DDL_ANY_PRIV=0) e não podem ser concedidos ou revogados", + }, + damengRoleAdmin: { + title: "Funções Dameng", + unsupported: "Esta página só suporta conexões de banco de dados Dameng.", + newRole: "Nova função", + searchRole: "Pesquisar funções", + loadFailed: "Falha ao carregar", + emptyRoles: "Nenhuma função, ou a conta atual não pode ler a lista de funções.", + selectRole: "Selecione uma função", + roleName: "Nome da função", + roleNamePlaceholder: "ex.: APP_ROLE", + members: "Membros", + systemPrivileges: "Privilégios do sistema", + grantedToUsers: "Usuários com esta função", + grantedPrivileges: "Privilégios concedidos", + noMembers: "Nenhum usuário possui esta função", + grantToUser: "Conceder a usuário", + userToGrant: "Usuário a conceder", + selectUser: "Selecionar usuário", + grantUserHint: "Conceder a função {role} ao usuário.", + noUsersToGrant: "Nenhum usuário disponível para conceder", + revokeFromUser: "Revogar", + systemUser: "Usuário do sistema", + systemUserProtected: "Não é possível revogar de usuários do sistema", + builtinRole: "Papel integrado", + builtinRoleProtected: "Papéis predefinidos não podem ser removidos ou modificados", + grantPrivilege: "Conceder privilégio", + revokePrivilege: "Revogar privilégio", + privilegeToGrant: "Privilégio a conceder", + selectPrivilege: "Selecionar privilégio", + grantPrivilegeHint: "Conceder o privilégio de sistema à função {role}.", + noPrivilegesToGrant: "Nenhum privilégio disponível para conceder", + noGrantedPrivileges: "Esta função não possui privilégios de sistema concedidos", + dropRole: "Excluir função", + previewSql: "Pré-visualizar SQL", + sqlPreview: "Pré-visualização SQL", + applySql: "Executar SQL", + applySuccess: "Operação de função aplicada", + applyFailed: "Falha na execução: {message}", + enableDdlAnyPrivHint: + "Dica: o DM8 desabilita a concessão de privilégios ANY de DDL por padrão (ENABLE_DDL_ANY_PRIV=0). Para conceder privilégios ANY, execute como SYSDBA: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). Para privilégios não ANY, certifique-se de que a conta atual tenha a ADMIN OPTION desse privilégio.", + anyPrivilegeDisabled: "Privilégios do tipo ANY estão desabilitados pelo DM8 (ENABLE_DDL_ANY_PRIV=0) e não podem ser concedidos ou revogados", + }, objects: { all: "Todos", tables: "Tabelas", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 63c0a16b73..5a201c9035 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -2467,6 +2467,10 @@ export default withEnglishFallback({ openUserAdmin: "打开用户与权限", processList: "进程列表", serverDashboard: "服务器仪表盘", + damengUsers: "达梦用户", + openDamengUsers: "打开达梦用户管理", + damengRoles: "达梦角色", + openDamengRoles: "打开达梦角色管理", damengJobAdmin: "达梦代理作业", openDamengJobAdmin: "打开达梦代理作业", newQuery: "新建查询", @@ -2791,6 +2795,8 @@ export default withEnglishFallback({ tree: { savedSql: "SQL 库", userAdmin: "用户与权限", + damengUsers: "用户", + damengRoles: "角色", damengJobAdmin: "代理作业", linkedServers: "链接服务器", defaultDatabase: "默认库", @@ -3038,6 +3044,110 @@ export default withEnglishFallback({ applySuccess: "作业操作已执行", applyFailed: "执行失败:{message}", }, + damengUserAdmin: { + title: "达梦用户", + unsupported: "当前页面仅支持达梦数据库连接。", + newUser: "新建用户", + searchUser: "搜索用户", + loadFailed: "加载失败", + emptyUsers: "暂无用户,或当前账号没有读取用户列表的权限。", + selectUser: "请选择一个用户", + username: "用户名", + usernamePlaceholder: "例如 APP_USER", + password: "密码", + passwordPolicyHint: "口令需满足 DM8 密码策略(默认长度 ≥ 9,建议大小写字母和数字组合)", + tablespace: "默认表空间", + tablespacePlaceholder: "输入表空间名", + locked: "创建后锁定账号", + changePassword: "修改密码", + newPassword: "新密码", + confirmPassword: "确认密码", + open: "正常", + systemUser: "系统用户", + systemUserAdmin: "管理用户", + systemUserAuditor: "审计用户", + systemUserSecurity: "安全用户", + systemUserSystem: "系统用户", + cannotChangePassword: "该用户密码无法由当前连接修改(仅 SYSDBA 或用户本人可修改)", + inheritedFrom: "继承自 {via}", + userType: "用户类型", + userTypeHint: "管理用户自动授予 DBA,审计用户自动授予 DB_AUDIT_ADMIN,安全用户自动授予 DB_POLICY_ADMIN", + groupOther: "其他用户", + systemUserProtected: "系统用户禁止修改或删除", + builtinRole: "内置角色", + builtinRoleProtected: "内置角色不可回收", + defaultTablespace: "默认表空间", + created: "创建时间", + lock: "锁定", + unlock: "解锁", + dropUser: "删除用户", + roles: "角色", + systemPrivileges: "系统权限", + grantedRoles: "已授予的角色", + grantedPrivileges: "已授予的权限", + grantRole: "授予角色", + revokeRole: "回收角色", + roleToGrant: "要授予的角色", + selectRole: "选择角色", + grantRoleHint: "将角色授予用户 {user}。", + noRolesToGrant: "没有可授予的角色", + noGrantedRoles: "该用户暂未被授予任何角色", + grantPrivilege: "授予权限", + revokePrivilege: "回收权限", + privilegeToGrant: "要授予的权限", + selectPrivilege: "选择权限", + grantPrivilegeHint: "将系统权限授予用户 {user}。", + noPrivilegesToGrant: "没有可授予的权限", + noGrantedPrivileges: "该用户暂未被授予任何系统权限", + previewSql: "预览 SQL", + sqlPreview: "SQL 预览", + applySql: "执行 SQL", + applySuccess: "用户操作已执行", + applyFailed: "执行失败:{message}", + enableDdlAnyPrivHint: "提示:DM8 默认禁用 DDL 的 ANY 权限授予(ENABLE_DDL_ANY_PRIV=0)。授 ANY 类权限请用 SYSDBA 执行 sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1) 开启;授普通权限请确认当前账号持有该权限的 ADMIN OPTION。", + anyPrivilegeDisabled: "ANY 类权限已被 DM8 禁用(ENABLE_DDL_ANY_PRIV=0),无法授予或回收", + }, + damengRoleAdmin: { + title: "达梦角色", + unsupported: "当前页面仅支持达梦数据库连接。", + newRole: "新建角色", + searchRole: "搜索角色", + loadFailed: "加载失败", + emptyRoles: "暂无角色,或当前账号没有读取角色列表的权限。", + selectRole: "请选择一个角色", + roleName: "角色名", + roleNamePlaceholder: "例如 APP_ROLE", + members: "成员", + systemPrivileges: "系统权限", + grantedToUsers: "已授予该角色的用户", + grantedPrivileges: "已授予的权限", + noMembers: "该角色暂未被授予任何用户", + grantToUser: "授予用户", + userToGrant: "要授予的用户", + selectUser: "选择用户", + grantUserHint: "将角色 {role} 授予用户。", + noUsersToGrant: "没有可授予的用户", + revokeFromUser: "回收", + systemUser: "系统用户", + systemUserProtected: "系统用户禁止回收", + builtinRole: "内置角色", + builtinRoleProtected: "预定义角色不允许删除或修改", + grantPrivilege: "授予权限", + revokePrivilege: "回收权限", + privilegeToGrant: "要授予的权限", + selectPrivilege: "选择权限", + grantPrivilegeHint: "将系统权限授予角色 {role}。", + noPrivilegesToGrant: "没有可授予的权限", + noGrantedPrivileges: "该角色暂未被授予任何系统权限", + dropRole: "删除角色", + previewSql: "预览 SQL", + sqlPreview: "SQL 预览", + applySql: "执行 SQL", + applySuccess: "角色操作已执行", + applyFailed: "执行失败:{message}", + enableDdlAnyPrivHint: "提示:DM8 默认禁用 DDL 的 ANY 权限授予(ENABLE_DDL_ANY_PRIV=0)。授 ANY 类权限请用 SYSDBA 执行 sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1) 开启;授普通权限请确认当前账号持有该权限的 ADMIN OPTION。", + anyPrivilegeDisabled: "ANY 类权限已被 DM8 禁用(ENABLE_DDL_ANY_PRIV=0),无法授予或回收", + }, objects: { all: "全部", tables: "表", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 291289f999..a37c385151 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -2473,6 +2473,10 @@ export default withEnglishFallback({ openUserAdmin: "開啟使用者與權限", damengJobAdmin: "達夢代理作業", openDamengJobAdmin: "開啟達夢代理作業", + damengUsers: "達夢使用者", + openDamengUsers: "開啟達夢使用者管理", + damengRoles: "達夢角色", + openDamengRoles: "開啟達夢角色管理", duplicateConnection: "複製連線", duplicateSelectedConnections: "複製選取的 {count} 個連線", newQuery: "建立查詢", @@ -2799,6 +2803,8 @@ export default withEnglishFallback({ tree: { savedSql: "SQL 庫", userAdmin: "使用者與權限", + damengUsers: "使用者", + damengRoles: "角色", damengJobAdmin: "代理作業", linkedServers: "連結伺服器", materializedViews: "具體化檢視", @@ -5867,6 +5873,110 @@ export default withEnglishFallback({ applySuccess: "作業操作已執行", applyFailed: "執行失敗:{message}", }, + damengUserAdmin: { + title: "達夢使用者", + unsupported: "目前頁面僅支援達夢資料庫連線。", + newUser: "新增使用者", + searchUser: "搜尋使用者", + loadFailed: "載入失敗", + emptyUsers: "暫無使用者,或目前帳號沒有讀取使用者清單的權限。", + selectUser: "請選擇一個使用者", + username: "使用者名稱", + usernamePlaceholder: "例如 APP_USER", + password: "密碼", + passwordPolicyHint: "口令需符合 DM8 密碼策略(預設長度 ≥ 9,建議大小寫字母與數字組合)", + tablespace: "預設資料表空間", + tablespacePlaceholder: "輸入資料表空間名稱", + locked: "建立後鎖定帳號", + changePassword: "修改密碼", + newPassword: "新密碼", + confirmPassword: "確認密碼", + open: "正常", + systemUser: "系統使用者", + systemUserAdmin: "管理使用者", + systemUserAuditor: "稽核使用者", + systemUserSecurity: "安全使用者", + systemUserSystem: "系統使用者", + cannotChangePassword: "該用戶密碼無法由當前連線修改(僅 SYSDBA 或用戶本人可修改)", + inheritedFrom: "繼承自 {via}", + userType: "使用者類型", + userTypeHint: "管理使用者自動授予 DBA,稽核使用者自動授予 DB_AUDIT_ADMIN,安全使用者自動授予 DB_POLICY_ADMIN", + groupOther: "其他使用者", + systemUserProtected: "系統使用者禁止修改或刪除", + builtinRole: "內建角色", + builtinRoleProtected: "內建角色不可回收", + defaultTablespace: "預設資料表空間", + created: "建立時間", + lock: "鎖定", + unlock: "解鎖", + dropUser: "刪除使用者", + roles: "角色", + systemPrivileges: "系統權限", + grantedRoles: "已授予的角色", + grantedPrivileges: "已授予的權限", + grantRole: "授予角色", + revokeRole: "回收角色", + roleToGrant: "要授予的角色", + selectRole: "選擇角色", + grantRoleHint: "將角色授予使用者 {user}。", + noRolesToGrant: "沒有可授予的角色", + noGrantedRoles: "該使用者暫未被授予任何角色", + grantPrivilege: "授予權限", + revokePrivilege: "回收權限", + privilegeToGrant: "要授予的權限", + selectPrivilege: "選擇權限", + grantPrivilegeHint: "將系統權限授予使用者 {user}。", + noPrivilegesToGrant: "沒有可授予的權限", + noGrantedPrivileges: "該使用者暫未被授予任何系統權限", + previewSql: "預覽 SQL", + sqlPreview: "SQL 預覽", + applySql: "執行 SQL", + applySuccess: "使用者操作已執行", + applyFailed: "執行失敗:{message}", + enableDdlAnyPrivHint: "提示:DM8 預設停用 DDL 的 ANY 權限授予(ENABLE_DDL_ANY_PRIV=0)。授 ANY 類權限請用 SYSDBA 執行 sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1) 開啟;授一般權限請確認目前帳號持有該權限的 ADMIN OPTION。", + anyPrivilegeDisabled: "ANY 類權限已被 DM8 停用(ENABLE_DDL_ANY_PRIV=0),無法授予或回收", + }, + damengRoleAdmin: { + title: "達夢角色", + unsupported: "目前頁面僅支援達夢資料庫連線。", + newRole: "新增角色", + searchRole: "搜尋角色", + loadFailed: "載入失敗", + emptyRoles: "暫無角色,或目前帳號沒有讀取角色清單的權限。", + selectRole: "請選擇一個角色", + roleName: "角色名稱", + roleNamePlaceholder: "例如 APP_ROLE", + members: "成員", + systemPrivileges: "系統權限", + grantedToUsers: "已授予該角色的使用者", + grantedPrivileges: "已授予的權限", + noMembers: "該角色暫未被授予任何使用者", + grantToUser: "授予使用者", + userToGrant: "要授予的使用者", + selectUser: "選擇使用者", + grantUserHint: "將角色 {role} 授予使用者。", + noUsersToGrant: "沒有可授予的使用者", + revokeFromUser: "回收", + systemUser: "系統使用者", + systemUserProtected: "系統使用者禁止回收", + builtinRole: "內建角色", + builtinRoleProtected: "預先定義角色不允許刪除或修改", + grantPrivilege: "授予權限", + revokePrivilege: "回收權限", + privilegeToGrant: "要授予的權限", + selectPrivilege: "選擇權限", + grantPrivilegeHint: "將系統權限授予角色 {role}。", + noPrivilegesToGrant: "沒有可授予的權限", + noGrantedPrivileges: "該角色暫未被授予任何系統權限", + dropRole: "刪除角色", + previewSql: "預覽 SQL", + sqlPreview: "SQL 預覽", + applySql: "執行 SQL", + applySuccess: "角色操作已執行", + applyFailed: "執行失敗:{message}", + enableDdlAnyPrivHint: "提示:DM8 預設停用 DDL 的 ANY 權限授予(ENABLE_DDL_ANY_PRIV=0)。授 ANY 類權限請用 SYSDBA 執行 sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1) 開啟;授一般權限請確認目前帳號持有該權限的 ADMIN OPTION。", + anyPrivilegeDisabled: "ANY 類權限已被 DM8 停用(ENABLE_DDL_ANY_PRIV=0),無法授予或回收", + }, etcd: { prefixPlaceholder: "搜尋 Key 路徑,例如 /app/ 或 service", newKey: "新增 Key", diff --git a/apps/desktop/src/lib/database/damengPrincipalAdmin.ts b/apps/desktop/src/lib/database/damengPrincipalAdmin.ts new file mode 100644 index 0000000000..189188f75f --- /dev/null +++ b/apps/desktop/src/lib/database/damengPrincipalAdmin.ts @@ -0,0 +1,585 @@ +import type { QueryResult } from "@/types/database"; + +/** + * Dameng (达梦 DM8) user & role administration SQL layer. + * + * Follows the same architecture as `damengJobAdmin.ts` / `databaseUserAdmin.ts`: + * SQL is generated on the frontend and executed through the generic query + * channel (`api.executeQuery` / `executeWithProductionSqlGuard`). + * + * Metadata views are Oracle-style: DBA_USERS, DBA_ROLES, DBA_ROLE_PRIVS, + * DBA_SYS_PRIVS and DBA_TABLESPACES. Identifiers (users, roles, tablespaces) + * are double-quoted; string literals (passwords) are single-quoted and escaped. + */ + +export interface DamengUser { + username: string; + accountStatus: string; + locked: boolean; + defaultTablespace: string; + temporaryTablespace: string; + profile: string; + created: string; + raw: Record; +} + +export interface DamengRole { + role: string; + passwordRequired: string; + raw: Record; +} + +export interface DamengGrant { + grantee: string; + grantedRole: string; + adminOption: boolean; + defaultRole: boolean; + /** True when the role is only reachable through role nesting (not granted directly). */ + inherited?: boolean; + /** The parent role through which an inherited role is reachable. */ + via?: string; +} + +export interface DamengRoleEntry { + role: string; + direct: boolean; + via?: string; +} + +export interface DamengSysPrivilege { + grantee: string; + privilege: string; + adminOption: boolean; +} + +export interface DamengCreateUserInput { + username: string; + password: string; + tablespace?: string; + locked?: boolean; +} + +/** + * Built-in system accounts that must never be dropped, locked or altered from + * the UI. Compared case-insensitively (see `isDamengSystemUser`). + */ +export const DAMENG_SYSTEM_USERS = ["SYSDBA", "SYSSSO", "SYSAUDITOR", "SYSDBO", "SYS"] as const; + +/** + * Identity category of each built-in system account, matching the DM + * management tool terminology: SYSDBA = admin, SYSAUDITOR = audit, + * SYSSSO = security. SYSDBO and SYS remain in the protected system group. + */ +export const DAMENG_SYSTEM_USER_CATEGORIES: Record = { + SYSDBA: "admin", + SYSAUDITOR: "auditor", + SYSSSO: "security", + SYSDBO: "system", + SYS: "system", +}; + +/** Returns the category key ("admin" | "auditor" | "security" | "system") of a system user, or undefined for regular users. */ +export function damengSystemUserCategory(username: string): string | undefined { + const normalized = username.trim().toUpperCase(); + return DAMENG_SYSTEM_USER_CATEGORIES[normalized]; +} + +/** Sidebar list grouping: the four system-user categories plus regular users. */ +export type DamengUserGroup = "admin" | "auditor" | "security" | "system" | "other"; + +export const DAMENG_USER_GROUPS: readonly DamengUserGroup[] = ["admin", "auditor", "security", "system", "other"]; + +/** + * Groups a username: the four system accounts map to their category; a regular + * user is grouped by the management roles they hold (DBA -> admin, + * DB_AUDIT_ADMIN -> auditor, DB_POLICY_ADMIN -> security), everything else to + * "other". Pass the user's directly granted roles (e.g. from DBA_ROLE_PRIVS). + */ +export function damengUserGroup(username: string, grantedRoles?: Iterable): DamengUserGroup { + const normalized = username.trim().toUpperCase(); + const system = DAMENG_SYSTEM_USER_CATEGORIES[normalized]; + if (system) return system as DamengUserGroup; + if (grantedRoles) { + const roles = new Set([...grantedRoles].map((role) => role.trim().toUpperCase())); + if (roles.has("DBA")) return "admin"; + if (roles.has("DB_AUDIT_ADMIN")) return "auditor"; + if (roles.has("DB_POLICY_ADMIN")) return "security"; + } + return "other"; +} + +/** Common Oracle-style system privileges supported by Dameng DM8. */ +export const DAMENG_SYSTEM_PRIVILEGES = [ + "CREATE SESSION", + "CREATE TABLE", + "CREATE ANY TABLE", + "ALTER ANY TABLE", + "DROP ANY TABLE", + "SELECT ANY TABLE", + "INSERT ANY TABLE", + "UPDATE ANY TABLE", + "DELETE ANY TABLE", + "CREATE VIEW", + "CREATE ANY VIEW", + "DROP ANY VIEW", + "CREATE PROCEDURE", + "CREATE ANY PROCEDURE", + "ALTER ANY PROCEDURE", + "DROP ANY PROCEDURE", + "EXECUTE ANY PROCEDURE", + "CREATE FUNCTION", + "CREATE PACKAGE", + "CREATE SEQUENCE", + "CREATE ANY SEQUENCE", + "ALTER ANY SEQUENCE", + "DROP ANY SEQUENCE", + "SELECT ANY SEQUENCE", + "CREATE TRIGGER", + "CREATE ANY TRIGGER", + "ALTER ANY TRIGGER", + "DROP ANY TRIGGER", + "CREATE SYNONYM", + "CREATE PUBLIC SYNONYM", + "DROP PUBLIC SYNONYM", + "CREATE ROLE", + "DROP ROLE", + "CREATE USER", + "ALTER USER", + "DROP USER", + "ALTER ANY USER", + "MANAGE ANY USER", + "CREATE TABLESPACE", + "ALTER TABLESPACE", + "DROP TABLESPACE", + "MANAGE TABLESPACE", + "CREATE DATABASE LINK", + "CREATE MATERIALIZED VIEW", + "ALTER DATABASE", + "ALTER SYSTEM", + "GRANT ANY PRIVILEGE", + "GRANT ANY ROLE", + "UNLIMITED TABLESPACE", + "SELECT ANY DICTIONARY", + "EXECUTE ANY TYPE", +] as const; + +export function isDamengSystemUser(username: string): boolean { + const normalized = username.trim().toUpperCase(); + return DAMENG_SYSTEM_USERS.some((systemUser) => systemUser === normalized); +} + +/** Direct role grants on regular users may be revoked, including predefined roles. */ +export function canDamengRevokeRoleGrant(username: string | undefined, grant: Pick | undefined): boolean { + return !!username && !!grant && !isDamengSystemUser(username) && !grant.inherited; +} + +/** + * Whether the given connection user may ALTER USER (change password) for the + * target user, following the DM (达梦) security model: + * - SYS is a built-in non-login user with no password: never changeable. + * - SYSDBA / SYSSSO / SYSAUDITOR passwords can only be changed by themselves + * (a SYSDBA session cannot reset SYSSSO or SYSAUDITOR passwords). + * - SYSDBA can change any regular (non-system) user's password. + * - A regular user can only change their own password. + */ +export function canDamengAlterUserPassword(connectionUser: string | undefined, target: string | undefined): boolean { + if (!target) return false; + const conn = (connectionUser || "").trim().toUpperCase(); + const tgt = target.trim().toUpperCase(); + if (tgt === "SYS") return false; + if (isDamengSystemUser(tgt)) return conn === tgt; + return conn === "SYSDBA" || conn === tgt; +} + +// --------------------------------------------------------------------------- +// Queries +// --------------------------------------------------------------------------- + +export function damengListUsersSql(): string { + return `SELECT USERNAME, ACCOUNT_STATUS, DEFAULT_TABLESPACE, TEMPORARY_TABLESPACE, PROFILE, CREATED +FROM DBA_USERS +ORDER BY USERNAME`; +} + +export function damengListRolesSql(): string { + return `SELECT ROLE, PASSWORD_REQUIRED +FROM DBA_ROLES +ORDER BY ROLE`; +} + +export function damengListTablespacesSql(): string { + return `SELECT TABLESPACE_NAME +FROM DBA_TABLESPACES + ORDER BY TABLESPACE_NAME`; +} + +export function damengSystemPrivilegeMapSql(): string { + return `SELECT NAME FROM SYSTEM_PRIVILEGE_MAP ORDER BY NAME`; +} + +export function damengEnableDdlAnyPrivSql(): string { + return `SELECT PARA_VALUE FROM V$DM_INI WHERE PARA_NAME = 'ENABLE_DDL_ANY_PRIV'`; +} + +export function damengListRoleMembersSql(role: string): string { + return `SELECT GRANTEE, GRANTED_ROLE, ADMIN_OPTION, DEFAULT_ROLE +FROM DBA_ROLE_PRIVS +WHERE GRANTED_ROLE = ${quoteDamengString(role)} +ORDER BY GRANTEE`; +} + +export function damengListUserRolesSql(user: string): string { + return `SELECT GRANTEE, GRANTED_ROLE, ADMIN_OPTION, DEFAULT_ROLE +FROM DBA_ROLE_PRIVS +WHERE GRANTEE = ${quoteDamengString(user)} +ORDER BY GRANTED_ROLE`; +} + +export function damengRoleGraphSql(): string { + return `SELECT GRANTEE, GRANTED_ROLE FROM DBA_ROLE_PRIVS`; +} + +export function damengListUserSysPrivsSql(user: string): string { + return `SELECT GRANTEE, PRIVILEGE, ADMIN_OPTION +FROM DBA_SYS_PRIVS +WHERE GRANTEE = ${quoteDamengString(user)} +ORDER BY PRIVILEGE`; +} + +export function damengListRoleSysPrivsSql(role: string): string { + return `SELECT GRANTEE, PRIVILEGE, ADMIN_OPTION +FROM DBA_SYS_PRIVS +WHERE GRANTEE = ${quoteDamengString(role)} +ORDER BY PRIVILEGE`; +} + +// --------------------------------------------------------------------------- +// DDL +// --------------------------------------------------------------------------- + +export function damengCreateUserSql(input: DamengCreateUserInput): string { + const username = input.username.trim(); + const password = input.password; + if (!username) throw new Error("Dameng username must not be empty"); + if (!password || !password.trim()) throw new Error("Dameng user password must not be empty"); + const parts: string[] = [`CREATE USER ${quoteDamengIdentifier(username)} IDENTIFIED BY ${quoteDamengPassword(password)}`]; + if (input.tablespace && input.tablespace.trim()) { + parts.push(`DEFAULT TABLESPACE ${quoteDamengIdentifier(input.tablespace)}`); + } + if (input.locked) parts.push("ACCOUNT LOCK"); + return `${parts.join(" ")};`; +} + +/** + * Predefined DM role automatically granted when creating a user of each type, + * matching the roles the built-in accounts hold: SYSDBA has DBA, SYSAUDITOR has + * DB_AUDIT_ADMIN, SYSSSO has DB_POLICY_ADMIN. "system" (SYS) has no grantable + * role — SYS is a built-in non-login user that cannot be replicated via GRANT. + */ +export const DAMENG_USER_TYPE_GRANTS: Record = { + admin: ["DBA"], + auditor: ["DB_AUDIT_ADMIN"], + security: ["DB_POLICY_ADMIN"], + system: [], +}; + +/** GRANT statements to append when creating a user with the given type. */ +export function damengUserTypeGrantSqls(userType: string, username: string): string[] { + const roles = DAMENG_USER_TYPE_GRANTS[userType] ?? []; + return roles.map((role) => `GRANT ${quoteDamengIdentifier(role)} TO ${quoteDamengIdentifier(username)};`); +} + +export function damengAlterUserPasswordSql(username: string, password: string): string { + return `ALTER USER ${quoteDamengIdentifier(username)} IDENTIFIED BY ${quoteDamengPassword(password)};`; +} + +export function damengLockUserSql(username: string): string { + return `ALTER USER ${quoteDamengIdentifier(username)} ACCOUNT LOCK;`; +} + +export function damengUnlockUserSql(username: string): string { + return `ALTER USER ${quoteDamengIdentifier(username)} ACCOUNT UNLOCK;`; +} + +export function damengDropUserSql(username: string): string { + return `DROP USER ${quoteDamengIdentifier(username)} CASCADE;`; +} + +export function damengCreateRoleSql(role: string): string { + return `CREATE ROLE ${quoteDamengIdentifier(role)};`; +} + +export function damengDropRoleSql(role: string): string { + return `DROP ROLE ${quoteDamengIdentifier(role)};`; +} + +// --------------------------------------------------------------------------- +// DCL +// --------------------------------------------------------------------------- + +export function damengGrantRoleSql(grantee: string, role: string): string { + return `GRANT ${quoteDamengIdentifier(role)} TO ${quoteDamengIdentifier(grantee)};`; +} + +export function damengRevokeRoleSql(grantee: string, role: string): string { + return `REVOKE ${quoteDamengIdentifier(role)} FROM ${quoteDamengIdentifier(grantee)};`; +} + +export function damengGrantSystemPrivilegeSql(grantee: string, privilege: string, adminOption = false): string { + const suffix = adminOption ? " WITH ADMIN OPTION" : ""; + return `GRANT ${privilege} TO ${quoteDamengIdentifier(grantee)}${suffix};`; +} + +export function damengRevokeSystemPrivilegeSql(grantee: string, privilege: string): string { + return `REVOKE ${privilege} FROM ${quoteDamengIdentifier(grantee)};`; +} + +// --------------------------------------------------------------------------- +// Parsers +// --------------------------------------------------------------------------- + +export function parseDamengUsers(result: QueryResult): DamengUser[] { + return result.rows.map((row) => { + const raw = rowToObject(result.columns, row); + const accountStatus = valueAt(raw, "ACCOUNT_STATUS").toUpperCase(); + return { + username: valueAt(raw, "USERNAME"), + accountStatus, + locked: /LOCKED/i.test(accountStatus), + defaultTablespace: valueAt(raw, "DEFAULT_TABLESPACE"), + temporaryTablespace: valueAt(raw, "TEMPORARY_TABLESPACE"), + profile: valueAt(raw, "PROFILE"), + created: valueAt(raw, "CREATED"), + raw, + }; + }); +} + +export function parseDamengRoles(result: QueryResult): DamengRole[] { + return result.rows.map((row) => { + const raw = rowToObject(result.columns, row); + return { + role: valueAt(raw, "ROLE"), + passwordRequired: valueAt(raw, "PASSWORD_REQUIRED"), + raw, + }; + }); +} + +export function parseDamengRoleGrants(result: QueryResult): Map> { + const grants = new Map>(); + for (const row of result.rows) { + const raw = rowToObject(result.columns, row); + const grantee = valueAt(raw, "GRANTEE").trim().toUpperCase(); + const role = valueAt(raw, "GRANTED_ROLE").trim().toUpperCase(); + if (!grantee || !role) continue; + let roles = grants.get(grantee); + if (!roles) { + roles = new Set(); + grants.set(grantee, roles); + } + roles.add(role); + } + return grants; +} + +export function parseDamengRolePrivs(result: QueryResult): DamengGrant[] { + return result.rows.map((row) => { + const raw = rowToObject(result.columns, row); + return { + grantee: valueAt(raw, "GRANTEE"), + grantedRole: valueAt(raw, "GRANTED_ROLE"), + adminOption: isYes(valueAt(raw, "ADMIN_OPTION")), + defaultRole: isYes(valueAt(raw, "DEFAULT_ROLE")), + }; + }); +} + +export function parseDamengSysPrivs(result: QueryResult): DamengSysPrivilege[] { + return result.rows.map((row) => { + const raw = rowToObject(result.columns, row); + return { + grantee: valueAt(raw, "GRANTEE"), + privilege: valueAt(raw, "PRIVILEGE"), + adminOption: isYes(valueAt(raw, "ADMIN_OPTION")), + }; + }); +} + +export function parseDamengTablespaces(result: QueryResult): string[] { + return result.rows + .map((row) => { + const raw = rowToObject(result.columns, row); + return valueAt(raw, "TABLESPACE_NAME"); + }) + .filter((name) => name !== ""); +} + +/** The set of system privilege names this DM instance supports (SYSTEM_PRIVILEGE_MAP). */ +export function parseDamengSystemPrivilegeMap(result: QueryResult): Set { + const names = new Set(); + for (const row of result.rows) { + const raw = rowToObject(result.columns, row); + const name = valueAt(raw, "NAME").trim().toUpperCase(); + if (name) names.add(name); + } + return names; +} + +/** + * Whether the instance allows granting/revoking ANY-type DDL system privileges + * (DM8 default: ENABLE_DDL_ANY_PRIV=0, which rejects them with error -5567). + */ +export function parseDamengEnableDdlAnyPriv(result: QueryResult): boolean { + if (result.rows.length === 0) return true; + const raw = rowToObject(result.columns, result.rows[0]); + return String(valueAt(raw, "PARA_VALUE") ?? "0").trim() !== "0"; +} + +/** True for ANY-type system privileges (CREATE ANY TABLE, SELECT ANY VIEW, ...). */ +export function isDamengAnyPrivilege(privilege: string): boolean { + return /\bANY\b/.test(privilege.toUpperCase()); +} + +/** + * Filters the static privilege catalog down to privileges that actually exist + * on this DM instance. The static catalog is an Oracle-style best-effort list + * and DM versions differ; the runtime map makes the grant dialog accurate. + */ +export function damengAvailableSystemPrivileges(catalog: readonly string[], existing: Set): string[] { + const normalized = new Set([...existing].map((name) => name.toUpperCase())); + return catalog.filter((privilege) => normalized.has(privilege.toUpperCase())); +} + +/** + * The DM role graph as GRANTEE -> directly granted roles. SVI / VTI and other + * roles are often granted through nesting (DBA -> VTI, PUBLIC -> SVI), and the + * official DM management tool shows the transitive closure, so we compute it + * client-side from this full graph. + */ +export function parseDamengRoleGraph(result: QueryResult): Map { + const graph = new Map(); + for (const row of result.rows) { + const raw = rowToObject(result.columns, row); + const grantee = valueAt(raw, "GRANTEE").toUpperCase(); + const role = valueAt(raw, "GRANTED_ROLE").toUpperCase(); + if (!grantee || !role) continue; + const list = graph.get(grantee) ?? []; + if (!list.includes(role)) list.push(role); + graph.set(grantee, list); + } + return graph; +} + +/** Internal DM roles that cannot be granted or revoked (GRANT fails with + * "Grantor no granted privilege") and are hidden from the official client. */ +/** Internal DM roles that cannot be granted or revoked (GRANT fails with + * "Grantor no granted privilege") and are hidden from the official client. */ +export const DAMENG_HIDDEN_ROLES = ["SYS_ADMIN"] as const; + +/** + * DM pre-defined roles that exist from database initialization and cannot be + * dropped or modified (their privilege sets are fixed by the system). Includes + * the DBA / AUDITOR / SSO / DBO type roles documented in the DM8 safety manual. + */ +export const DAMENG_PREDEFINED_ROLES: readonly string[] = [ + "DBA", + "RESOURCE", + "PUBLIC", + "VTI", + "SOI", + "SVI", + "SYS_ADMIN", + "SELECT_CATALOG_ROLE", + "DB_AUDIT_ADMIN", + "DB_AUDIT_OPER", + "DB_AUDIT_PUBLIC", + "DB_AUDIT_VTI", + "DB_AUDIT_SOI", + "DB_AUDIT_SVI", + "DB_AUDIT_SELECT_CATALOG_ROLE", + "DB_POLICY_ADMIN", + "DB_POLICY_OPER", + "DB_POLICY_PUBLIC", + "DB_POLICY_VTI", + "DB_POLICY_SOI", + "DB_POLICY_SVI", + "DB_POLICY_SELECT_CATALOG_ROLE", + "DB_OBJECT_ADMIN", + "DB_OBJECT_OPER", + "DB_OBJECT_PUBLIC", + "DB_OBJECT_VTI", + "DB_OBJECT_SOI", + "DB_OBJECT_SVI", + "DB_OBJECT_SELECT_CATALOG_ROLE", +]; + +/** Case-insensitive check for DM pre-defined roles. */ +export function isDamengPredefinedRole(role: string | undefined): boolean { + if (!role) return false; + const normalized = role.trim().toUpperCase(); + return DAMENG_PREDEFINED_ROLES.some((predefined) => predefined === normalized); +} + +/** + * Transitive closure of the roles a user effectively holds, mirroring the + * official DM management tool: direct grants plus roles reachable through + * role nesting (e.g. SYSDBA -> DBA -> VTI). Hidden internal roles such as + * SYS_ADMIN are excluded. + */ +export function damengRolesClosure(username: string, graph: Map): DamengRoleEntry[] { + const start = username.trim().toUpperCase(); + const seen = new Set(); + const entries: DamengRoleEntry[] = []; + const queue: { role: string; direct: boolean; via?: string }[] = (graph.get(start) ?? []).map((role) => ({ + role, + direct: true, + })); + while (queue.length > 0) { + const { role, direct, via } = queue.shift()!; + if (seen.has(role) || DAMENG_HIDDEN_ROLES.includes(role as (typeof DAMENG_HIDDEN_ROLES)[number])) continue; + seen.add(role); + entries.push({ role, direct, via }); + for (const child of graph.get(role) ?? []) { + queue.push({ role: child, direct: false, via: role }); + } + } + return entries; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +export function quoteDamengIdentifier(value: string): string { + return `"${value.replace(/"/g, '""')}"`; +} + +export function quoteDamengString(value: string): string { + return `'${value.replace(/'/g, "''")}'`; +} + +export function quoteDamengPassword(value: string): string { + // DM8 CREATE/ALTER USER IDENTIFIED BY takes a double-quoted password token + // (single quotes are string literals and fail with a parse error there). + // Double quotes preserve case; a literal " inside the password is escaped as "". + return `"${value.replace(/"/g, '""')}"`; +} + +function isYes(value: string): boolean { + return value.trim().toUpperCase() === "YES"; +} + +function rowToObject(columns: string[], row: QueryResult["rows"][number]): Record { + const object: Record = {}; + columns.forEach((column, index) => { + object[column] = row[index] ?? null; + }); + return object; +} + +function valueAt(row: Record, column: string): string { + const key = Object.keys(row).find((candidate) => candidate.toLowerCase() === column.toLowerCase()); + const value = key ? row[key] : undefined; + return value == null ? "" : String(value); +} diff --git a/apps/desktop/src/lib/sidebar/sidebarNodeOrdering.ts b/apps/desktop/src/lib/sidebar/sidebarNodeOrdering.ts index faf7ac69bb..64cfe263b6 100644 --- a/apps/desktop/src/lib/sidebar/sidebarNodeOrdering.ts +++ b/apps/desktop/src/lib/sidebar/sidebarNodeOrdering.ts @@ -36,8 +36,8 @@ export function sortSidebarTreeChildrenForParent(parent: Pick, if (parent.type === "connection") { const savedSqlNodes = normalized.filter((child) => child.type === "saved-sql-root"); - const userAdminNodes = normalized.filter((child) => child.type === "user-admin"); - const regularChildren = normalized.filter((child) => child.type !== "user-admin" && child.type !== "saved-sql-root"); + const userAdminNodes = normalized.filter((child) => child.type === "user-admin" || child.type === "dameng-users" || child.type === "dameng-roles"); + const regularChildren = normalized.filter((child) => child.type !== "user-admin" && child.type !== "dameng-users" && child.type !== "dameng-roles" && child.type !== "saved-sql-root"); const withConnectionUtilityOrder = (children: TreeNode[]) => [...savedSqlNodes, ...children, ...userAdminNodes]; if (databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "qdrant" || databaseType === "milvus" || databaseType === "weaviate" || databaseType === "chromadb") { diff --git a/apps/desktop/src/lib/sidebar/sidebarTreeItemLayout.ts b/apps/desktop/src/lib/sidebar/sidebarTreeItemLayout.ts index 1374bd2f02..a218baa692 100644 --- a/apps/desktop/src/lib/sidebar/sidebarTreeItemLayout.ts +++ b/apps/desktop/src/lib/sidebar/sidebarTreeItemLayout.ts @@ -28,6 +28,8 @@ const leafTypes: Set = new Set([ "vector-collection", "elasticsearch-index", "user-admin", + "dameng-users", + "dameng-roles", "saved-sql-file", "table-search-control", "load-more", diff --git a/apps/desktop/src/lib/sidebar/treeNodeClick.ts b/apps/desktop/src/lib/sidebar/treeNodeClick.ts index 9edcb9155e..26d3205db1 100644 --- a/apps/desktop/src/lib/sidebar/treeNodeClick.ts +++ b/apps/desktop/src/lib/sidebar/treeNodeClick.ts @@ -25,6 +25,8 @@ const toggleLeafNodeTypes = new Set([ "vector-collection", "elasticsearch-index", "user-admin", + "dameng-users", + "dameng-roles", ]); // These are application entry points rather than database objects. They should // always navigate on a single click, even when the user prefers double-click diff --git a/apps/desktop/src/lib/sidebar/treeNodeIcon.ts b/apps/desktop/src/lib/sidebar/treeNodeIcon.ts index 92c2ac4206..db87a9c4f8 100644 --- a/apps/desktop/src/lib/sidebar/treeNodeIcon.ts +++ b/apps/desktop/src/lib/sidebar/treeNodeIcon.ts @@ -80,6 +80,10 @@ export function getTreeNodeIconInfo(node: TreeNode): TreeNodeIconInfo | null { return { icon: TableProperties, colorClass: "text-primary" }; case "user-admin": return { icon: UsersRound, colorClass: "text-primary" }; + case "dameng-users": + return { icon: UsersRound, colorClass: "text-primary" }; + case "dameng-roles": + return { icon: ShieldCheck, colorClass: "text-primary" }; case "redis-db": return { icon: Database, colorClass: "text-red-400" }; case "mongo-gridfs": diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 78953336e3..fa9290b862 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -1380,6 +1380,34 @@ export const useConnectionStore = defineStore("connection", () => { }; } + function buildDamengUserNode(connectionId: string, existingConnectionNode?: TreeNode): TreeNode | undefined { + const config = getConfig(connectionId); + if (effectiveDatabaseTypeForConnection(config) !== "dameng") return undefined; + const existing = existingConnectionNode?.children?.find((child) => child.type === "dameng-users"); + return { + id: `${connectionId}:__dameng_users`, + label: "tree.damengUsers", + type: "dameng-users", + connectionId, + database: "", + isExpanded: existing?.isExpanded ?? false, + }; + } + + function buildDamengRoleNode(connectionId: string, existingConnectionNode?: TreeNode): TreeNode | undefined { + const config = getConfig(connectionId); + if (effectiveDatabaseTypeForConnection(config) !== "dameng") return undefined; + const existing = existingConnectionNode?.children?.find((child) => child.type === "dameng-roles"); + return { + id: `${connectionId}:__dameng_roles`, + label: "tree.damengRoles", + type: "dameng-roles", + connectionId, + database: "", + isExpanded: existing?.isExpanded ?? false, + }; + } + function buildDamengJobAdminNode(connectionId: string, existingConnectionNode?: TreeNode): TreeNode | undefined { const config = getConfig(connectionId); if (effectiveDatabaseTypeForConnection(config) !== "dameng") return undefined; @@ -1397,8 +1425,10 @@ export const useConnectionStore = defineStore("connection", () => { function withConnectionUtilityNodes(connectionId: string, children: TreeNode[], existingConnectionNode?: TreeNode): TreeNode[] { const nonUtilityChildren = connectionMetadataChildren(children); const userAdminNode = buildUserAdminNode(connectionId, existingConnectionNode); + const damengUserNode = buildDamengUserNode(connectionId, existingConnectionNode); + const damengRoleNode = buildDamengRoleNode(connectionId, existingConnectionNode); const damengJobAdminNode = buildDamengJobAdminNode(connectionId, existingConnectionNode); - return [...nonUtilityChildren, userAdminNode, damengJobAdminNode].filter(Boolean) as TreeNode[]; + return [...nonUtilityChildren, userAdminNode, damengUserNode, damengRoleNode, damengJobAdminNode].filter(Boolean) as TreeNode[]; } function withSavedSqlRoot(connectionId: string, children: TreeNode[], existingConnectionNode?: TreeNode): TreeNode[] { diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index a4be214715..4872326375 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -1880,6 +1880,56 @@ export const useQueryStore = defineStore("query", () => { return id; } + function openDamengUsers(connectionId: string) { + const existing = tabs.value.find((tab) => tab.mode === "dameng-users" && tab.connectionId === connectionId); + if (existing) { + switchTab(existing.id); + return existing.id; + } + + const conn = useConnectionStore().getConfig(connectionId); + const id = uuid(); + const tab: QueryTab = { + id, + title: t("damengUserAdmin.title"), + connectionId, + database: conn?.database || "", + sql: "", + isExecuting: false, + isCancelling: false, + isExplaining: false, + mode: "dameng-users", + }; + tabs.value.push(tab); + activeTabId.value = id; + return id; + } + + function openDamengRoles(connectionId: string) { + const existing = tabs.value.find((tab) => tab.mode === "dameng-roles" && tab.connectionId === connectionId); + if (existing) { + switchTab(existing.id); + return existing.id; + } + + const conn = useConnectionStore().getConfig(connectionId); + const id = uuid(); + const tab: QueryTab = { + id, + title: t("damengRoleAdmin.title"), + connectionId, + database: conn?.database || "", + sql: "", + isExecuting: false, + isCancelling: false, + isExplaining: false, + mode: "dameng-roles", + }; + tabs.value.push(tab); + activeTabId.value = id; + return id; + } + function openMongoBucket(connectionId: string, database: string, bucketName: string) { const title = `${database}.${bucketName}`; const existing = tabs.value.find((tab) => tab.mode === "mongo-bucket" && tab.connectionId === connectionId && tab.database === database && tab.mongoBucket?.bucketName === bucketName); @@ -5768,6 +5818,8 @@ export const useQueryStore = defineStore("query", () => { openMysqlDashboard, openPostgresDashboard, openNacosDashboard, + openDamengUsers, + openDamengRoles, openDamengJobAdmin, openMqAdmin, openMqttAdmin, diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 4508d022f3..667ac9e8a0 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -899,6 +899,8 @@ export type TreeNodeType = | "extension" | "object-browser" | "user-admin" + | "dameng-users" + | "dameng-roles" | "dameng-job-admin" | "saved-sql-root" | "saved-sql-folder" @@ -1157,6 +1159,8 @@ export interface QueryTab { | "objects" | "structure" | "users" + | "dameng-users" + | "dameng-roles" | "dameng-jobs" | "processlist" | "mysql-dashboard" diff --git a/docs/pips/plans/2026-08-06-dameng-user-role-admin.md b/docs/pips/plans/2026-08-06-dameng-user-role-admin.md new file mode 100644 index 0000000000..152790e7f6 --- /dev/null +++ b/docs/pips/plans/2026-08-06-dameng-user-role-admin.md @@ -0,0 +1,61 @@ +# PIP: 达梦数据库用户/角色管理(侧边栏"用户"、"角色"菜单) + +- 日期:2026-08-06 +- 分支:`feature/dameng-user-role-admin`(基线 upstream/main @ 2daf8cb4b) +- 范围:标准版(用户+角色:列表/新建/删除/锁定解锁/改密 + 角色授予回收 + 系统权限 GRANT/REVOKE + 默认表空间下拉;不含对象级权限、资源限制) + +## 背景 + +达梦(Dameng)在 DBX 中是受支持方言,侧边栏已有 `dameng-job-admin`(作业管理)特殊节点。 +官方 DM 管理工具的用户/角色管理功能(DBA_USERS / DBA_ROLES / DBA_ROLE_PRIVS / DBA_SYS_PRIVS / DBA_TABLESPACES) +尚未覆盖。本计划参照现有 `dameng-job-admin` + `user-admin` 模式新增"用户"、"角色"两类菜单。 + +## 实现路径(前端拼 SQL + 通用查询执行,无专用后端 API,与 DamengJobAdmin 一致) + +### 新增文件 + +1. `apps/desktop/src/lib/database/damengPrincipalAdmin.ts` — SQL 生成 + 结果解析层 + - 查询:用户列表、角色列表、用户已授角色、角色成员、用户系统权限、角色系统权限、表空间列表 + - DDL:创建用户、改密、锁定/解锁、删除用户(CASCADE)、创建角色、删除角色 + - DCL:授予/回收角色、授予/回收系统权限 + - 系统权限集合(Oracle 风格常用项):CREATE SESSION/TABLE/VIEW/PROCEDURE/SEQUENCE/TRIGGER/ANY 等;运行时通过 SYSTEM_PRIVILEGE_MAP 过滤出当前实例真实存在的权限 + - 系统用户保护集合:SYSDBA/SYSSSO/SYSAUDITOR/SYS;预定义角色集合:DBA/PUBLIC/RESOURCE/VTI/SOI/SVI 及 AUDITOR/SSO/DBO 类型角色(不可删除/修改) + - 解析函数:parseDamengUsers / parseDamengRoles / parseDamengRolePrivs / parseDamengSysPrivs / parseDamengTablespaces / parseDamengSystemPrivilegeMap / parseDamengRoleGraph / damengRolesClosure + +2. `apps/desktop/src/components/admin/DamengUserAdmin.vue` — 用户管理组件(独立) + - 列表:用户名、锁定状态、默认表空间、创建时间 + - 操作:新建(用户名/密码/默认表空间/是否锁定)、改密、锁定/解锁、删除(系统用户禁用) + - 详情面板:已授角色(可授予/回收)、系统权限(可授予/回收) + - 破坏性操作走 executeWithProductionSqlGuard + SQL 预览确认(DamengJobAdmin 同款) + +3. `apps/desktop/src/components/admin/DamengRoleAdmin.vue` — 角色管理组件(独立) + - 列表:角色名、角色 ID + - 操作:新建、删除(预定义角色禁用) + - 详情面板:角色成员(用户,系统用户禁止回收)、系统权限(预定义角色不可授予/回收) + +### 修改文件 + +- `apps/desktop/src/types/database.ts` — TreeNodeType 加 `dameng-users`/`dameng-roles`;QueryTabMode 加对应值 +- `apps/desktop/src/stores/connectionStore.ts` — buildDamengUserNode / buildDamengRoleNode(参照 buildDamengJobAdminNode),withConnectionUtilityNodes 注册 +- `apps/desktop/src/stores/queryStore.ts` — openDamengUsers() / openDamengRoles()(参照 openDamengJobAdmin) +- `apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue` — 节点点击路由(参照 dameng-job-admin 分支) +- `apps/desktop/src/components/sidebar/TreeItem.vue` — 图标 + 节点 label +- `apps/desktop/src/lib/sidebar/treeNodeIcon.ts` — 图标映射 +- `apps/desktop/src/lib/sidebar/treeNodeClick.ts` / `sidebarNodeOrdering.ts` / `sidebarTreeItemLayout.ts` — 节点类型注册 +- `apps/desktop/src/components/layout/ContentArea.vue` — tab mode → 组件渲染(参照 dameng-jobs) +- `apps/desktop/src/i18n/locales/zh-CN.ts` / `en.ts` 等 8 语言 — 文案 + +### 测试 + +- `packages/app-tests/damengPrincipalAdmin.test.ts` — SQL 层单测(转义、SQL 形状、系统用户保护) +- 组件测试(可选) + +## 关键设计 + +- 系统用户(SYSDBA/SYSSSO/SYSAUDITOR/SYS)禁止删除/改密/锁定,详情面板内对系统用户禁止授予/回收角色与权限 +- 改密权限模型(实测校正):SYS 永不改;SYSSSO/SYSAUDITOR 仅本人可改;SYSDBA 可改普通用户;普通用户只能改自己 +- 角色展示为传递闭包(实测:SYSDBA→DBA→VTI、PUBLIC→SVI 为嵌套授予),与官方客户端一致;内部隐藏角色 SYS_ADMIN 过滤(不可授予/回收) +- 预定义角色(DBA/PUBLIC/RESOURCE/VTI/SOI/SVI/DB_AUDIT_*/DB_POLICY_*/DB_OBJECT_*)不可删除、不可修改权限集;PUBLIC 不建议回收(官方文档警告回收后大量功能不可用);VTI/SOI/SVI 的默认固定授权(DBA→VTI、SYSDBA→SOI、PUBLIC→SVI)不可回收 +- 达梦 DBA_ROLES 无 ROLE_ID 列(与 Oracle 不同),角色列表不展示角色 ID +- DBA_ 视图查询失败(非 SYSDBA 权限不足)时提示降级信息 +- 所有 DDL/DCL 走 executeWithProductionSqlGuard diff --git a/packages/app-tests/damengPrincipalAdmin.test.ts b/packages/app-tests/damengPrincipalAdmin.test.ts new file mode 100644 index 0000000000..aa8d777040 --- /dev/null +++ b/packages/app-tests/damengPrincipalAdmin.test.ts @@ -0,0 +1,406 @@ +import { strict as assert } from "node:assert"; +import { test } from "vitest"; + +import { + DAMENG_SYSTEM_PRIVILEGES, + DAMENG_SYSTEM_USERS, + damengAlterUserPasswordSql, + damengCreateRoleSql, + damengCreateUserSql, + damengDropRoleSql, + damengDropUserSql, + damengGrantRoleSql, + damengGrantSystemPrivilegeSql, + damengListRoleMembersSql, + damengListRoleSysPrivsSql, + damengListRolesSql, + damengListTablespacesSql, + damengListUserRolesSql, + damengListUserSysPrivsSql, + damengListUsersSql, + damengLockUserSql, + damengRevokeRoleSql, + damengRevokeSystemPrivilegeSql, + damengSystemPrivilegeMapSql, + damengEnableDdlAnyPrivSql, + isDamengAnyPrivilege, + parseDamengEnableDdlAnyPriv, + damengUnlockUserSql, + damengUserTypeGrantSqls, + parseDamengSystemPrivilegeMap, + damengAvailableSystemPrivileges, + canDamengAlterUserPassword, + canDamengRevokeRoleGrant, + DAMENG_HIDDEN_ROLES, + damengRoleGraphSql, + parseDamengRoleGraph, + parseDamengRoleGrants, + damengRolesClosure, + isDamengSystemUser, + damengUserGroup, + isDamengPredefinedRole, + DAMENG_USER_GROUPS, + parseDamengRoles, + parseDamengRolePrivs, + parseDamengSysPrivs, + parseDamengTablespaces, + parseDamengUsers, +} from "../../apps/desktop/src/lib/database/damengPrincipalAdmin.ts"; +import type { QueryResult } from "../../apps/desktop/src/types/database.ts"; + +function queryResult(columns: string[], rows: unknown[][]): QueryResult { + return { columns, rows, types: [], success: true, rowCount: rows.length } as QueryResult; +} + +test("user list SQL reads DBA_USERS ordered by username", () => { + const sql = damengListUsersSql(); + assert.match(sql, /FROM\s+DBA_USERS/i); + assert.match(sql, /ORDER BY\s+USERNAME/i); + assert.match(sql, /USERNAME/i); + assert.match(sql, /ACCOUNT_STATUS/i); +}); + +test("role list SQL reads DBA_ROLES ordered by role", () => { + const sql = damengListRolesSql(); + assert.match(sql, /FROM\s+DBA_ROLES/i); + assert.match(sql, /ORDER BY\s+ROLE/i); +}); + +test("tablespace list SQL reads DBA_TABLESPACES", () => { + const sql = damengListTablespacesSql(); + assert.match(sql, /FROM\s+DBA_TABLESPACES/i); + assert.match(sql, /ORDER BY\s+TABLESPACE_NAME/i); +}); + +test("role member SQL filters by granted role and orders by grantee", () => { + const sql = damengListRoleMembersSql("APP_ROLE"); + assert.match(sql, /FROM\s+DBA_ROLE_PRIVS/i); + assert.match(sql, /WHERE\s+GRANTED_ROLE\s*=\s*'APP_ROLE'/i); + assert.match(sql, /ORDER BY\s+GRANTEE/i); +}); + +test("user roles SQL filters by grantee and orders by granted role", () => { + const sql = damengListUserRolesSql("alice"); + assert.match(sql, /FROM\s+DBA_ROLE_PRIVS/i); + assert.match(sql, /WHERE\s+GRANTEE\s*=\s*'alice'/i); + assert.match(sql, /ORDER BY\s+GRANTED_ROLE/i); +}); + +test("user system privileges SQL filters by grantee", () => { + const sql = damengListUserSysPrivsSql("alice"); + assert.match(sql, /FROM\s+DBA_SYS_PRIVS/i); + assert.match(sql, /WHERE\s+GRANTEE\s*=\s*'alice'/i); +}); + +test("role system privileges SQL filters by grantee", () => { + const sql = damengListRoleSysPrivsSql("APP_ROLE"); + assert.match(sql, /FROM\s+DBA_SYS_PRIVS/i); + assert.match(sql, /WHERE\s+GRANTEE\s*=\s*'APP_ROLE'/i); +}); + +test("create user SQL quotes identifiers and escapes password", () => { + assert.equal( + damengCreateUserSql({ username: 'bob"x', password: 'p"wd', tablespace: "TS_DATA" }), + `CREATE USER "bob""x" IDENTIFIED BY "p""wd" DEFAULT TABLESPACE "TS_DATA";`, + ); +}); + +test("create user SQL omits tablespace clause when not provided", () => { + const sql = damengCreateUserSql({ username: "alice", password: "secret" }); + assert.match(sql, /^CREATE USER "alice" IDENTIFIED BY "secret";$/); + assert.ok(!sql.includes("DEFAULT TABLESPACE")); +}); + +test("create user SQL appends ACCOUNT LOCK when locked", () => { + const sql = damengCreateUserSql({ username: "alice", password: "secret", locked: true }); + assert.match(sql, /ACCOUNT LOCK;$/); +}); + +test("user type grants map to DM predefined roles", () => { + assert.deepEqual(damengUserTypeGrantSqls("admin", "app_admin"), [`GRANT "DBA" TO "app_admin";`]); + assert.deepEqual(damengUserTypeGrantSqls("auditor", "app_audit"), [`GRANT "DB_AUDIT_ADMIN" TO "app_audit";`]); + assert.deepEqual(damengUserTypeGrantSqls("security", "app_sso"), [`GRANT "DB_POLICY_ADMIN" TO "app_sso";`]); + assert.deepEqual(damengUserTypeGrantSqls("system", "app_sys"), []); + assert.deepEqual(damengUserTypeGrantSqls("unknown", "x"), []); +}); + +test("alter password SQL quotes both values", () => { + assert.equal(damengAlterUserPasswordSql("alice", 'new"pwd'), `ALTER USER "alice" IDENTIFIED BY "new""pwd";`); +}); + +test("lock and unlock SQL", () => { + assert.equal(damengLockUserSql("alice"), `ALTER USER "alice" ACCOUNT LOCK;`); + assert.equal(damengUnlockUserSql("alice"), `ALTER USER "alice" ACCOUNT UNLOCK;`); +}); + +test("drop user SQL uses CASCADE", () => { + assert.equal(damengDropUserSql("alice"), `DROP USER "alice" CASCADE;`); +}); + +test("create and drop role SQL quote identifiers", () => { + assert.equal(damengCreateRoleSql("APP_ROLE"), `CREATE ROLE "APP_ROLE";`); + assert.equal(damengDropRoleSql('r"x'), `DROP ROLE "r""x";`); +}); + +test("grant and revoke role SQL", () => { + assert.equal(damengGrantRoleSql("alice", "APP_ROLE"), `GRANT "APP_ROLE" TO "alice";`); + assert.equal(damengRevokeRoleSql("alice", "APP_ROLE"), `REVOKE "APP_ROLE" FROM "alice";`); +}); + +test("grant system privilege SQL supports admin option", () => { + assert.equal(damengGrantSystemPrivilegeSql("alice", "CREATE SESSION"), `GRANT CREATE SESSION TO "alice";`); + assert.equal(damengGrantSystemPrivilegeSql("alice", "CREATE TABLE", true), `GRANT CREATE TABLE TO "alice" WITH ADMIN OPTION;`); + assert.equal(damengRevokeSystemPrivilegeSql("alice", "CREATE SESSION"), `REVOKE CREATE SESSION FROM "alice";`); +}); + +test("system users are protected", () => { + for (const name of DAMENG_SYSTEM_USERS) assert.equal(isDamengSystemUser(name), true, name); + assert.equal(isDamengSystemUser("sysdba"), true); + assert.equal(isDamengSystemUser("sysdbo"), true); + assert.equal(isDamengSystemUser("SYSDBO"), true); + assert.equal(isDamengSystemUser("sys"), true); + assert.equal(isDamengSystemUser("alice"), false); +}); + +test("user grouping maps system accounts to categories and others to other", () => { + assert.equal(damengUserGroup("SYSDBA"), "admin"); + assert.equal(damengUserGroup("SYSAUDITOR"), "auditor"); + assert.equal(damengUserGroup("SYSSSO"), "security"); + assert.equal(damengUserGroup("SYSDBO"), "system"); + assert.equal(damengUserGroup("sysdbo"), "system"); + assert.equal(damengUserGroup("SYS"), "system"); + assert.equal(damengUserGroup("sysdba"), "admin"); + assert.equal(damengUserGroup("alice"), "other"); + assert.deepEqual(DAMENG_USER_GROUPS, ["admin", "auditor", "security", "system", "other"]); +}); + +test("user grouping places regular users with management roles into matching categories", () => { + // Directly granted DBA groups a regular user as admin. + assert.equal(damengUserGroup("APP_ADMIN", ["DBA"]), "admin"); + assert.equal(damengUserGroup("APP_ADMIN", ["dba", "resource"]), "admin"); + assert.equal(damengUserGroup("APP_AUDIT", ["DB_AUDIT_ADMIN"]), "auditor"); + assert.equal(damengUserGroup("APP_SSO", ["DB_POLICY_ADMIN"]), "security"); + // System accounts keep their category regardless of granted roles. + assert.equal(damengUserGroup("SYSDBA", ["RESOURCE"]), "admin"); + // No management role (or unknown roles) falls back to other. + assert.equal(damengUserGroup("APP_READ", ["PUBLIC", "RESOURCE"]), "other"); + assert.equal(damengUserGroup("APP_READ"), "other"); +}); + +test("parseDamengRoleGrants builds a grantee to role set map", () => { + const grants = parseDamengRoleGrants({ + columns: ["GRANTEE", "GRANTED_ROLE"], + rows: [ + ["APP_ADMIN", "DBA"], + ["APP_ADMIN", "PUBLIC"], + ["alice", "resource"], + ], + }); + assert.deepEqual([...grants.get("APP_ADMIN")!].sort(), ["DBA", "PUBLIC"]); + assert.deepEqual([...grants.get("ALICE")!], ["RESOURCE"]); + assert.equal(grants.get("nobody"), undefined); +}); + +test("system privilege catalog contains common session and DDL privileges", () => { + assert.ok(DAMENG_SYSTEM_PRIVILEGES.includes("CREATE SESSION")); + assert.ok(DAMENG_SYSTEM_PRIVILEGES.includes("CREATE TABLE")); + assert.ok(DAMENG_SYSTEM_PRIVILEGES.includes("CREATE ANY TABLE")); + assert.ok(DAMENG_SYSTEM_PRIVILEGES.includes("GRANT ANY PRIVILEGE")); + assert.ok(DAMENG_SYSTEM_PRIVILEGES.includes("UNLIMITED TABLESPACE")); +}); + +test("parseDamengUsers maps rows to user objects with lock status", () => { + const users = parseDamengUsers( + queryResult( + ["USERNAME", "ACCOUNT_STATUS", "DEFAULT_TABLESPACE", "TEMPORARY_TABLESPACE", "PROFILE", "CREATED"], + [ + ["SYSDBA", "OPEN", "MAIN", "TEMP", "DEFAULT", "2024-01-01 00:00:00"], + ["alice", "LOCKED", "TS_DATA", "TEMP", "DEFAULT", "2024-02-01 00:00:00"], + ["bob", "EXPIRED", "MAIN", "TEMP", "DEFAULT", null], + ], + ), + ); + assert.equal(users.length, 3); + assert.deepEqual(users[0], { + username: "SYSDBA", + accountStatus: "OPEN", + locked: false, + defaultTablespace: "MAIN", + temporaryTablespace: "TEMP", + profile: "DEFAULT", + created: "2024-01-01 00:00:00", + raw: { USERNAME: "SYSDBA", ACCOUNT_STATUS: "OPEN", DEFAULT_TABLESPACE: "MAIN", TEMPORARY_TABLESPACE: "TEMP", PROFILE: "DEFAULT", CREATED: "2024-01-01 00:00:00" }, + }); + assert.equal(users[1].locked, true); + assert.equal(users[1].accountStatus, "LOCKED"); + assert.equal(users[2].created, ""); +}); + +test("parseDamengRoles maps rows", () => { + const roles = parseDamengRoles(queryResult(["ROLE", "PASSWORD_REQUIRED"], [["APP_ROLE", "NO"]])); + assert.equal(roles.length, 1); + assert.equal(roles[0].role, "APP_ROLE"); + assert.equal(roles[0].passwordRequired, "NO"); +}); + +test("parseDamengRolePrivs maps grants", () => { + const privs = parseDamengRolePrivs(queryResult(["GRANTEE", "GRANTED_ROLE", "ADMIN_OPTION", "DEFAULT_ROLE"], [["alice", "APP_ROLE", "YES", "YES"]])); + assert.deepEqual(privs, [{ grantee: "alice", grantedRole: "APP_ROLE", adminOption: true, defaultRole: true }]); +}); + +test("parseDamengSysPrivs maps privileges", () => { + const privs = parseDamengSysPrivs(queryResult(["GRANTEE", "PRIVILEGE", "ADMIN_OPTION"], [["alice", "CREATE SESSION", "NO"]])); + assert.deepEqual(privs, [{ grantee: "alice", privilege: "CREATE SESSION", adminOption: false }]); +}); + +test("parseDamengTablespaces returns table space names", () => { + const spaces = parseDamengTablespaces(queryResult(["TABLESPACE_NAME", "STATUS"], [["MAIN", "ONLINE"], ["TS_DATA", "ONLINE"]])); + assert.deepEqual(spaces, ["MAIN", "TS_DATA"]); +}); + + test("canDamengAlterUserPassword follows the DM security model", () => { + // SYS is a built-in non-login user with no password: never changeable. + assert.equal(canDamengAlterUserPassword("SYSDBA", "SYS"), false); + assert.equal(canDamengAlterUserPassword("SYS", "SYS"), false); + // System admin passwords can only be changed by themselves (SYSDBA cannot + // reset SYSSSO / SYSAUDITOR passwords). + assert.equal(canDamengAlterUserPassword("SYSDBA", "SYSDBA"), true); + assert.equal(canDamengAlterUserPassword("SYSDBA", "SYSSSO"), false); + assert.equal(canDamengAlterUserPassword("SYSDBA", "SYSAUDITOR"), false); + assert.equal(canDamengAlterUserPassword("SYSSSO", "SYSSSO"), true); + assert.equal(canDamengAlterUserPassword("SYSAUDITOR", "SYSAUDITOR"), true); + assert.equal(canDamengAlterUserPassword("SYSDBO", "SYSDBO"), true); + assert.equal(canDamengAlterUserPassword("SYSDBA", "SYSDBO"), false); + // SYSDBA can change any regular user's password. + assert.equal(canDamengAlterUserPassword("SYSDBA", "alice"), true); + // A regular user can only change their own password. + assert.equal(canDamengAlterUserPassword("alice", "alice"), true); + assert.equal(canDamengAlterUserPassword("alice", "bob"), false); + assert.equal(canDamengAlterUserPassword("alice", "SYSDBA"), false); + // Non-SYSDBA admins cannot change regular users. + assert.equal(canDamengAlterUserPassword("SYSSSO", "alice"), false); + assert.equal(canDamengAlterUserPassword("SYSAUDITOR", "alice"), false); + // Matching is case-insensitive; undefined connection user cannot change anyone. + assert.equal(canDamengAlterUserPassword("sysdba", "ALICE"), true); + assert.equal(canDamengAlterUserPassword(undefined, "alice"), false); + assert.equal(canDamengAlterUserPassword("SYSDBA", undefined), false); +}); + +test("role revoke policy permits direct predefined grants only for regular users", () => { + assert.equal(canDamengRevokeRoleGrant("alice", { grantedRole: "DBA", inherited: false }), true); + assert.equal(canDamengRevokeRoleGrant("alice", { grantedRole: "RESOURCE" }), true); + assert.equal(canDamengRevokeRoleGrant("alice", { grantedRole: "VTI", inherited: true }), false); + assert.equal(canDamengRevokeRoleGrant("alice", { grantedRole: "SVI", inherited: true }), false); + assert.equal(canDamengRevokeRoleGrant("SYSDBO", { grantedRole: "DB_OBJECT_ADMIN", inherited: false }), false); + assert.equal(canDamengRevokeRoleGrant("sysdba", { grantedRole: "DBA", inherited: false }), false); + assert.equal(canDamengRevokeRoleGrant(undefined, { grantedRole: "DBA", inherited: false }), false); +}); +test("damengRoleGraphSql selects the full role graph", () => { + assert.equal(damengRoleGraphSql(), "SELECT GRANTEE, GRANTED_ROLE FROM DBA_ROLE_PRIVS"); +}); + +test("parseDamengRoleGraph builds GRANTEE -> roles map", () => { + const graph = parseDamengRoleGraph( + queryResult(["GRANTEE", "GRANTED_ROLE"], [["SYSDBA", "DBA"], ["DBA", "VTI"], ["PUBLIC", "SVI"], ["SYSDBA", "DBA"]]), + ); + assert.deepEqual([...graph.get("SYSDBA")!], ["DBA"]); + assert.deepEqual([...graph.get("DBA")!], ["VTI"]); + assert.deepEqual([...graph.get("PUBLIC")!], ["SVI"]); +}); + +test("damengRolesClosure mirrors the official DM tool (SYSDBA has 6 roles)", () => { + const graph = new Map([ + ["SYSDBA", ["DBA", "RESOURCE", "PUBLIC", "SOI", "SYS_ADMIN"]], + ["DBA", ["VTI"]], + ["PUBLIC", ["SVI"]], + ]); + const entries = damengRolesClosure("SYSDBA", graph); + // BFS order: direct grants first, then nested roles in discovery order. + assert.deepEqual(entries.map((e) => e.role), ["DBA", "RESOURCE", "PUBLIC", "SOI", "VTI", "SVI"]); + const vti = entries.find((e) => e.role === "VTI")!; + assert.equal(vti.direct, false); + assert.equal(vti.via, "DBA"); + const svi = entries.find((e) => e.role === "SVI")!; + assert.equal(svi.direct, false); + assert.equal(svi.via, "PUBLIC"); + const dba = entries.find((e) => e.role === "DBA")!; + assert.equal(dba.direct, true); + // Hidden internal role SYS_ADMIN is excluded. + assert.equal(entries.some((e) => e.role === "SYS_ADMIN"), false); +}); + +test("damengRolesClosure handles nesting for regular users and cycles", () => { + // Note: parseDamengRoleGraph normalises keys to upper-case. + const graph = new Map([["ALICE", ["PUBLIC"]], ["PUBLIC", ["SVI"]], ["A", ["B"]], ["B", ["A"]]]); + const alice = damengRolesClosure("alice", graph); + assert.deepEqual(alice.map((e) => e.role), ["PUBLIC", "SVI"]); + assert.equal(alice[0].direct, true); + assert.equal(alice[1].direct, false); + assert.equal(alice[1].via, "PUBLIC"); + // Cycle A <-> B must terminate without infinite recursion. + const cycle = damengRolesClosure("A", graph); + assert.ok(cycle.length <= 3); + assert.ok(cycle.some((e) => e.role === "B")); +}); + +test("DAMENG_HIDDEN_ROLES contains the internal non-grantable role", () => { + assert.deepEqual([...DAMENG_HIDDEN_ROLES], ["SYS_ADMIN"]); +}); + +test("predefined DM roles are detected case-insensitively", () => { + assert.equal(isDamengPredefinedRole("DBA"), true); + assert.equal(isDamengPredefinedRole("public"), true); + assert.equal(isDamengPredefinedRole("DB_AUDIT_ADMIN"), true); + assert.equal(isDamengPredefinedRole("DB_POLICY_SVI"), true); + assert.equal(isDamengPredefinedRole("db_object_admin"), true); + assert.equal(isDamengPredefinedRole("APP_ROLE"), false); + assert.equal(isDamengPredefinedRole(undefined), false); + assert.equal(isDamengPredefinedRole(""), false); +}); + +test("system privilege map SQL reads SYSTEM_PRIVILEGE_MAP", () => { + const sql = damengSystemPrivilegeMapSql(); + assert.match(sql, /FROM\s+SYSTEM_PRIVILEGE_MAP/i); + assert.match(sql, /ORDER BY\s+NAME/i); +}); + +test("parseDamengSystemPrivilegeMap normalises names to upper case", () => { + const map = parseDamengSystemPrivilegeMap(queryResult(["PRIVILEGE", "NAME"], [[1, "CREATE TABLE"], [2, "select any table"], [3, ""]])); + assert.equal(map.has("CREATE TABLE"), true); + assert.equal(map.has("SELECT ANY TABLE"), true); + assert.equal(map.has(""), false); +}); + +test("damengAvailableSystemPrivileges filters the catalog by instance map", () => { + const available = damengAvailableSystemPrivileges(["CREATE SESSION", "CREATE TABLE", "ALTER ANY TABLE"], new Set(["CREATE SESSION", "CREATE TABLE"])); + assert.deepEqual(available, ["CREATE SESSION", "CREATE TABLE"]); + // Case-insensitive matching keeps the catalog's canonical spelling. + const lower = damengAvailableSystemPrivileges(["CREATE SESSION"], new Set(["create session"])); + assert.deepEqual(lower, ["CREATE SESSION"]); +}); + +test("ENABLE_DDL_ANY_PRIV SQL reads V$DM_INI parameter", () => { + const sql = damengEnableDdlAnyPrivSql(); + assert.match(sql, /FROM\s+V\$DM_INI/i); + assert.match(sql, /ENABLE_DDL_ANY_PRIV/i); +}); + +test("parseDamengEnableDdlAnyPriv reads the parameter value", () => { + assert.equal(parseDamengEnableDdlAnyPriv(queryResult(["PARA_VALUE"], [["0"]])), false); + assert.equal(parseDamengEnableDdlAnyPriv(queryResult(["PARA_VALUE"], [["1"]])), true); + assert.equal(parseDamengEnableDdlAnyPriv(queryResult(["PARA_VALUE"], [])), true); +}); + +test("isDamengAnyPrivilege detects ANY-type privileges case-insensitively", () => { + assert.equal(isDamengAnyPrivilege("CREATE ANY TABLE"), true); + assert.equal(isDamengAnyPrivilege("select any view"), true); + assert.equal(isDamengAnyPrivilege("GRANT ANY PRIVILEGE"), true); + assert.equal(isDamengAnyPrivilege("CREATE TABLE"), false); + assert.equal(isDamengAnyPrivilege("CREATE SESSION"), false); +}); + +test("create user SQL rejects empty username or password", () => { + assert.throws(() => damengCreateUserSql({ username: " ", password: "secret" }), /username must not be empty/i); + assert.throws(() => damengCreateUserSql({ username: "alice", password: "" }), /password must not be empty/i); + assert.throws(() => damengCreateUserSql({ username: "alice", password: " " }), /password must not be empty/i); +}); diff --git a/packages/app-tests/sidebarNodeOrdering.test.ts b/packages/app-tests/sidebarNodeOrdering.test.ts index d9fae9dc51..105bda2f00 100644 --- a/packages/app-tests/sidebarNodeOrdering.test.ts +++ b/packages/app-tests/sidebarNodeOrdering.test.ts @@ -102,3 +102,25 @@ test("keeps connection utility nodes in fixed positions", () => { ], ); }); + +test("dameng user/role admin nodes sort after regular children", () => { + const parent: Pick = { type: "connection" }; + const children: TreeNode[] = [ + { id: "conn:__dameng_roles", label: "tree.damengRoles", type: "dameng-roles" }, + { id: "conn:__dameng_users", label: "tree.damengUsers", type: "dameng-users" }, + { id: "conn:b", label: "b", type: "database" }, + { id: "conn:__dameng_jobs", label: "tree.damengJobAdmin", type: "dameng-job-admin" }, + ]; + + const sorted = sortSidebarTreeChildrenForParent(parent, children, "dameng"); + + assert.deepEqual( + sorted.map((child) => [child.type, child.label]), + [ + ["database", "b"], + ["dameng-job-admin", "tree.damengJobAdmin"], + ["dameng-roles", "tree.damengRoles"], + ["dameng-users", "tree.damengUsers"], + ], + ); +}); diff --git a/packages/app-tests/treeNodeClick.test.ts b/packages/app-tests/treeNodeClick.test.ts index 5753c2dff4..816f551c4b 100644 --- a/packages/app-tests/treeNodeClick.test.ts +++ b/packages/app-tests/treeNodeClick.test.ts @@ -22,6 +22,13 @@ test("extension rows open their metadata details", () => { assert.equal(treeNodeRowDoubleClickAction("extension", false, "double"), "open-extension-details"); }); +test("dameng user and role admin rows toggle their tabs", () => { + assert.equal(treeNodeRowAction("dameng-users", false), "toggle"); + assert.equal(treeNodeRowAction("dameng-roles", false), "toggle"); + assert.equal(treeNodeRowAction("dameng-users", true), "toggle"); + assert.equal(treeNodeRowAction("dameng-roles", true), "toggle"); +}); + test("double click navigation mode selects rows on single click", () => { assert.equal(treeNodeRowAction("table", true, "double"), "none"); assert.equal(treeNodeRowAction("view", true, "double"), "none");
{{ t("damengRoleAdmin.grantUserHint", { role: selectedRole?.role ?? "" }) }}
{{ t("damengRoleAdmin.anyPrivilegeDisabled") }}
{{ t("damengRoleAdmin.grantPrivilegeHint", { role: selectedRole?.role ?? "" }) }}
{{ pendingSql }}
{{ t("damengUserAdmin.grantRoleHint", { user: selectedUser?.username ?? "" }) }}
{{ t("damengUserAdmin.anyPrivilegeDisabled") }}
{{ t("damengUserAdmin.grantPrivilegeHint", { user: selectedUser?.username ?? "" }) }}