feat(dameng): add user and role administration
This commit is contained in:
1 parent
9bacb752e4
commit
ec4f8582e6
25 files changed
+3435
-4
No files matched your search
@@ -0,0 +1,536 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { AlertTriangle, Loader2, Plus, RefreshCcw, ShieldCheck, Trash2, UserRoundPlus, UsersRound } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import type { ConnectionConfig } from "@/types/database";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import {
|
||||
DAMENG_SYSTEM_PRIVILEGES,
|
||||
damengCreateRoleSql,
|
||||
damengDropRoleSql,
|
||||
damengGrantRoleSql,
|
||||
damengGrantSystemPrivilegeSql,
|
||||
damengListRoleMembersSql,
|
||||
damengListRoleSysPrivsSql,
|
||||
damengListRolesSql,
|
||||
damengListUsersSql,
|
||||
damengRevokeRoleSql,
|
||||
damengRevokeSystemPrivilegeSql,
|
||||
damengSystemPrivilegeMapSql,
|
||||
damengEnableDdlAnyPrivSql,
|
||||
isDamengAnyPrivilege,
|
||||
parseDamengEnableDdlAnyPriv,
|
||||
isDamengSystemUser,
|
||||
isDamengPredefinedRole,
|
||||
parseDamengRolePrivs,
|
||||
parseDamengRoles,
|
||||
parseDamengSysPrivs,
|
||||
parseDamengUsers,
|
||||
parseDamengSystemPrivilegeMap,
|
||||
damengAvailableSystemPrivileges,
|
||||
type DamengGrant,
|
||||
type DamengRole,
|
||||
type DamengSysPrivilege,
|
||||
type DamengUser,
|
||||
} from "@/lib/database/damengPrincipalAdmin";
|
||||
|
||||
const props = defineProps<{
|
||||
connection: ConnectionConfig;
|
||||
}>();
|
||||
|
||||
type DetailTab = "members" | "privileges";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const connectionStore = useConnectionStore();
|
||||
|
||||
const roles = ref<DamengRole[]>([]);
|
||||
const allUsers = ref<DamengUser[]>([]);
|
||||
const selectedRoleName = ref("");
|
||||
const search = ref("");
|
||||
const loadingRoles = ref(false);
|
||||
const loadingDetails = ref(false);
|
||||
const applying = ref(false);
|
||||
const loadError = ref("");
|
||||
const detailError = ref("");
|
||||
const detailTab = ref<DetailTab>("members");
|
||||
const members = ref<DamengGrant[]>([]);
|
||||
const grantedPrivileges = ref<DamengSysPrivilege[]>([]);
|
||||
const systemPrivilegeMap = ref<Set<string> | null>(null);
|
||||
const enableDdlAnyPriv = ref(true);
|
||||
|
||||
const createDialogOpen = ref(false);
|
||||
const createRoleName = ref("");
|
||||
|
||||
const grantUserDialogOpen = ref(false);
|
||||
const grantUserValue = ref("");
|
||||
const grantPrivilegeDialogOpen = ref(false);
|
||||
const grantPrivilegeValue = ref("");
|
||||
|
||||
const previewDialogOpen = ref(false);
|
||||
const pendingSql = ref("");
|
||||
const pendingAfterApply = ref<(() => void | Promise<void>) | undefined>();
|
||||
const pendingDanger = ref(false);
|
||||
|
||||
const supported = computed(() => props.connection.db_type === "dameng");
|
||||
const executionDatabase = computed(() => (props.connection.db_type === "dameng" ? "" : props.connection.database || ""));
|
||||
const selectedRole = computed(() => roles.value.find((role) => role.role === selectedRoleName.value));
|
||||
const selectedIsBuiltinRole = computed(() => !!selectedRole.value && isDamengPredefinedRole(selectedRole.value.role));
|
||||
const filteredRoles = computed(() => {
|
||||
const query = search.value.trim().toLowerCase();
|
||||
if (!query) return roles.value;
|
||||
return roles.value.filter((role) => role.role.toLowerCase().includes(query));
|
||||
});
|
||||
const availableUsersForGrant = computed(() => {
|
||||
const granted = new Set(members.value.map((member) => member.grantee.toUpperCase()));
|
||||
// System users are protected: not offered for grant.
|
||||
return allUsers.value.filter((user) => !granted.has(user.username.toUpperCase()) && !isDamengSystemUser(user.username));
|
||||
});
|
||||
const availablePrivilegesForGrant = computed(() => {
|
||||
const granted = new Set(grantedPrivileges.value.map((grant) => grant.privilege.toUpperCase()));
|
||||
const catalog = systemPrivilegeMap.value ? damengAvailableSystemPrivileges(DAMENG_SYSTEM_PRIVILEGES, systemPrivilegeMap.value) : DAMENG_SYSTEM_PRIVILEGES;
|
||||
return catalog.filter((privilege) => !granted.has(privilege.toUpperCase()));
|
||||
});
|
||||
const canCreateRole = computed(() => createRoleName.value.trim() !== "");
|
||||
|
||||
async function ensureConnection() {
|
||||
await connectionStore.ensureConnected(props.connection.id);
|
||||
}
|
||||
|
||||
function isDdlAnyPrivDenied(message: string): boolean {
|
||||
// DM8 error -5567: "Grantor has no granted privilege" / "授权者没有此授权权限",
|
||||
// typically because ENABLE_DDL_ANY_PRIV=0 blocks granting ANY-type privileges.
|
||||
return /授权者没有此授权权限|Grantor no granted privilege|[-]?5567/i.test(message);
|
||||
}
|
||||
|
||||
function formatApplyFailedMessage(message: string): string {
|
||||
return isDdlAnyPrivDenied(message) ? `${message}\n${t("damengRoleAdmin.enableDdlAnyPrivHint")}` : message;
|
||||
}
|
||||
|
||||
function isPrivilegeGrantDisabled(privilege: string): boolean {
|
||||
return !enableDdlAnyPriv.value && isDamengAnyPrivilege(privilege);
|
||||
}
|
||||
|
||||
async function loadRoles() {
|
||||
if (!supported.value) return;
|
||||
loadingRoles.value = true;
|
||||
loadError.value = "";
|
||||
try {
|
||||
await ensureConnection();
|
||||
const [rolesResult, usersResult, privilegeMapResult, ddlAnyPrivResult] = await Promise.all([
|
||||
api.executeQuery(props.connection.id, executionDatabase.value, damengListRolesSql(), undefined, undefined, { maxRows: 5000 }),
|
||||
api.executeQuery(props.connection.id, executionDatabase.value, damengListUsersSql(), undefined, undefined, { maxRows: 5000 }),
|
||||
// SYSTEM_PRIVILEGE_MAP may be unreadable for non-privileged accounts; fall back to the full catalog.
|
||||
api.executeQuery(props.connection.id, executionDatabase.value, damengSystemPrivilegeMapSql(), undefined, undefined, { maxRows: 5000 }).catch(() => undefined),
|
||||
// DM8 default disables ANY-type privilege grants (ENABLE_DDL_ANY_PRIV=0); keep the default enabled on failure.
|
||||
api.executeQuery(props.connection.id, executionDatabase.value, damengEnableDdlAnyPrivSql(), undefined, undefined, { maxRows: 10 }).catch(() => undefined),
|
||||
]);
|
||||
roles.value = parseDamengRoles(rolesResult);
|
||||
allUsers.value = parseDamengUsers(usersResult);
|
||||
systemPrivilegeMap.value = privilegeMapResult ? parseDamengSystemPrivilegeMap(privilegeMapResult) : null;
|
||||
enableDdlAnyPriv.value = ddlAnyPrivResult ? parseDamengEnableDdlAnyPriv(ddlAnyPrivResult) : true;
|
||||
if (!selectedRole.value) selectedRoleName.value = roles.value[0]?.role || "";
|
||||
await loadDetails();
|
||||
} catch (error: any) {
|
||||
loadError.value = error?.message || String(error);
|
||||
roles.value = [];
|
||||
members.value = [];
|
||||
grantedPrivileges.value = [];
|
||||
} finally {
|
||||
loadingRoles.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
let detailRequestSeq = 0;
|
||||
|
||||
async function loadDetails() {
|
||||
const role = selectedRole.value;
|
||||
if (!role) {
|
||||
members.value = [];
|
||||
grantedPrivileges.value = [];
|
||||
return;
|
||||
}
|
||||
const seq = ++detailRequestSeq;
|
||||
loadingDetails.value = true;
|
||||
detailError.value = "";
|
||||
try {
|
||||
await ensureConnection();
|
||||
const [membersResult, privsResult] = await Promise.all([
|
||||
api.executeQuery(props.connection.id, executionDatabase.value, damengListRoleMembersSql(role.role), undefined, undefined, { maxRows: 1000 }),
|
||||
api.executeQuery(props.connection.id, executionDatabase.value, damengListRoleSysPrivsSql(role.role), undefined, undefined, { maxRows: 1000 }),
|
||||
]);
|
||||
if (seq !== detailRequestSeq) return; // stale response for a previous selection
|
||||
members.value = parseDamengRolePrivs(membersResult);
|
||||
grantedPrivileges.value = parseDamengSysPrivs(privsResult);
|
||||
} catch (error: any) {
|
||||
if (seq !== detailRequestSeq) return; // stale response for a previous selection
|
||||
detailError.value = error?.message || String(error);
|
||||
members.value = [];
|
||||
grantedPrivileges.value = [];
|
||||
} finally {
|
||||
if (seq === detailRequestSeq) loadingDetails.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function selectRole(role: DamengRole) {
|
||||
selectedRoleName.value = role.role;
|
||||
void loadDetails();
|
||||
}
|
||||
|
||||
function previewSql(sql: string, options: { danger?: boolean; afterApply?: () => void | Promise<void> } = {}) {
|
||||
pendingSql.value = sql;
|
||||
pendingDanger.value = !!options.danger;
|
||||
pendingAfterApply.value = options.afterApply;
|
||||
previewDialogOpen.value = true;
|
||||
}
|
||||
|
||||
async function applyPendingSql() {
|
||||
if (!pendingSql.value.trim()) return;
|
||||
applying.value = true;
|
||||
try {
|
||||
await ensureConnection();
|
||||
const results = await executeWithProductionSqlGuard({
|
||||
connection: props.connection,
|
||||
database: executionDatabase.value,
|
||||
sql: pendingSql.value,
|
||||
source: t("production.sourceAdmin"),
|
||||
execute: () =>
|
||||
api.executeMulti(props.connection.id, executionDatabase.value, pendingSql.value, undefined, undefined, {
|
||||
maxRows: 1000,
|
||||
useTransaction: true,
|
||||
}),
|
||||
});
|
||||
if (!results) return;
|
||||
// Per-statement errors are returned inside the result array (HTTP 200), so
|
||||
// they must be surfaced explicitly instead of being treated as success.
|
||||
const failed = results.find((result) => !!result.error);
|
||||
if (failed) {
|
||||
toast(t("damengRoleAdmin.applyFailed", { message: formatApplyFailedMessage(translateBackendError(t, failed.error)) }), 5000);
|
||||
// Statements before the error may already be committed, so refresh regardless.
|
||||
await loadRoles();
|
||||
return;
|
||||
}
|
||||
toast(t("damengRoleAdmin.applySuccess"), 2500);
|
||||
previewDialogOpen.value = false;
|
||||
await (pendingAfterApply.value?.() ?? Promise.resolve());
|
||||
await loadRoles();
|
||||
} catch (error: any) {
|
||||
toast(t("damengRoleAdmin.applyFailed", { message: formatApplyFailedMessage(error?.message || String(error)) }), 5000);
|
||||
} finally {
|
||||
applying.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
createRoleName.value = "";
|
||||
createDialogOpen.value = true;
|
||||
}
|
||||
|
||||
function previewCreateRole() {
|
||||
if (!canCreateRole.value) return;
|
||||
previewSql(damengCreateRoleSql(createRoleName.value), {
|
||||
afterApply: () => {
|
||||
createDialogOpen.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function previewDropRole() {
|
||||
const role = selectedRole.value;
|
||||
if (!role || selectedIsBuiltinRole.value) return;
|
||||
previewSql(damengDropRoleSql(role.role), { danger: true });
|
||||
}
|
||||
|
||||
function openGrantUserDialog() {
|
||||
grantUserValue.value = "";
|
||||
grantUserDialogOpen.value = true;
|
||||
}
|
||||
|
||||
function previewGrantUser() {
|
||||
const role = selectedRole.value;
|
||||
if (!role || !grantUserValue.value) return;
|
||||
previewSql(damengGrantRoleSql(grantUserValue.value, role.role), {
|
||||
afterApply: () => {
|
||||
grantUserDialogOpen.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function previewRevokeMember(member: DamengGrant) {
|
||||
const role = selectedRole.value;
|
||||
if (!role) return;
|
||||
previewSql(damengRevokeRoleSql(member.grantee, role.role), { danger: true });
|
||||
}
|
||||
|
||||
function openGrantPrivilegeDialog() {
|
||||
grantPrivilegeValue.value = "";
|
||||
grantPrivilegeDialogOpen.value = true;
|
||||
}
|
||||
|
||||
function previewGrantPrivilege() {
|
||||
const role = selectedRole.value;
|
||||
if (!role || !grantPrivilegeValue.value) return;
|
||||
previewSql(damengGrantSystemPrivilegeSql(role.role, grantPrivilegeValue.value), {
|
||||
afterApply: () => {
|
||||
grantPrivilegeDialogOpen.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function previewRevokePrivilege(grant: DamengSysPrivilege) {
|
||||
const role = selectedRole.value;
|
||||
if (!role) return;
|
||||
previewSql(damengRevokeSystemPrivilegeSql(role.role, grant.privilege), { danger: true });
|
||||
}
|
||||
|
||||
function isSystemMember(grantee: string): boolean {
|
||||
return isDamengSystemUser(grantee);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadRoles();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col bg-background">
|
||||
<div class="flex h-12 shrink-0 items-center gap-2 border-b px-3">
|
||||
<ShieldCheck class="h-4 w-4 text-primary" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm font-semibold">{{ t("damengRoleAdmin.title") }}</div>
|
||||
<div class="truncate text-[11px] text-muted-foreground">{{ connection.name }}</div>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" class="h-8 gap-1.5" :disabled="loadingRoles" @click="loadRoles">
|
||||
<Loader2 v-if="loadingRoles" class="h-3.5 w-3.5 animate-spin" />
|
||||
<RefreshCcw v-else class="h-3.5 w-3.5" />
|
||||
{{ t("contextMenu.refreshChildren") }}
|
||||
</Button>
|
||||
<Button size="sm" class="h-8 gap-1.5" @click="openCreateDialog">
|
||||
<Plus class="h-3.5 w-3.5" />
|
||||
{{ t("damengRoleAdmin.newRole") }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="!supported" class="m-4 rounded border border-dashed p-4 text-sm text-muted-foreground">
|
||||
{{ t("damengRoleAdmin.unsupported") }}
|
||||
</div>
|
||||
|
||||
<div v-else class="grid min-h-0 flex-1 grid-cols-[300px_minmax(0,1fr)]">
|
||||
<aside class="flex min-h-0 flex-col border-r">
|
||||
<div class="border-b p-2">
|
||||
<Input v-model="search" class="h-8 text-xs" :placeholder="t('damengRoleAdmin.searchRole')" />
|
||||
</div>
|
||||
<div v-if="loadError" class="m-2 rounded border border-destructive/30 bg-destructive/5 p-2 text-xs text-destructive">
|
||||
<div class="mb-1 flex items-center gap-1 font-medium"><AlertTriangle class="h-3.5 w-3.5" />{{ t("damengRoleAdmin.loadFailed") }}</div>
|
||||
<div class="break-all">{{ loadError }}</div>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-auto p-2">
|
||||
<button v-for="role in filteredRoles" :key="role.role" type="button" class="mb-1 w-full rounded border px-2 py-2 text-left text-xs transition hover:bg-accent" :class="selectedRoleName === role.role ? 'border-primary bg-primary/10' : 'border-transparent'" @click="selectRole(role)">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="min-w-0 flex-1 truncate font-medium">{{ role.role }}</span>
|
||||
<Badge v-if="role.passwordRequired === 'YES'" variant="outline" class="h-5 px-1.5 text-[10px]">PWD</Badge>
|
||||
<Badge v-if="isDamengPredefinedRole(role.role)" variant="outline" class="h-5 border-amber-500/50 px-1.5 text-[10px] text-amber-700 dark:text-amber-300">{{ t("damengRoleAdmin.builtinRole") }}</Badge>
|
||||
</div>
|
||||
</button>
|
||||
<div v-if="!loadingRoles && filteredRoles.length === 0" class="p-6 text-center text-xs text-muted-foreground">
|
||||
{{ t("damengRoleAdmin.emptyRoles") }}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="flex min-h-0 flex-col">
|
||||
<div v-if="selectedRole" class="flex shrink-0 flex-wrap items-center gap-2 border-b p-3">
|
||||
<ShieldCheck class="h-4 w-4 text-primary" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="truncate text-sm font-semibold">{{ selectedRole.role }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" variant="destructive" class="h-8 gap-1.5" :disabled="selectedIsBuiltinRole" :title="selectedIsBuiltinRole ? t('damengRoleAdmin.builtinRoleProtected') : ''" @click="previewDropRole">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
{{ t("damengRoleAdmin.dropRole") }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedRole" class="flex h-10 shrink-0 items-center gap-1 border-b px-3">
|
||||
<Button size="sm" :variant="detailTab === 'members' ? 'secondary' : 'ghost'" class="h-7 px-2 text-xs" @click="detailTab = 'members'">{{ t("damengRoleAdmin.members") }}</Button>
|
||||
<Button size="sm" :variant="detailTab === 'privileges' ? 'secondary' : 'ghost'" class="h-7 px-2 text-xs" @click="detailTab = 'privileges'">{{ t("damengRoleAdmin.systemPrivileges") }}</Button>
|
||||
<Loader2 v-if="loadingDetails" class="ml-auto h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-auto p-3">
|
||||
<div v-if="!selectedRole" class="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
{{ t("damengRoleAdmin.selectRole") }}
|
||||
</div>
|
||||
<div v-else-if="detailError" class="rounded border border-destructive/30 bg-destructive/5 p-3 text-xs text-destructive">
|
||||
<div class="mb-1 flex items-center gap-1 font-medium"><AlertTriangle class="h-3.5 w-3.5" />{{ t("damengRoleAdmin.loadFailed") }}</div>
|
||||
<div class="break-all">{{ detailError }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="detailTab === 'members'">
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<span class="text-xs font-medium">{{ t("damengRoleAdmin.grantedToUsers") }} ({{ members.length }})</span>
|
||||
<Button size="sm" variant="outline" class="ml-auto h-7 gap-1 px-2 text-xs" :disabled="availableUsersForGrant.length === 0" :title="availableUsersForGrant.length === 0 ? t('damengRoleAdmin.noUsersToGrant') : ''" @click="openGrantUserDialog">
|
||||
<UserRoundPlus class="h-3.5 w-3.5" />
|
||||
{{ t("damengRoleAdmin.grantToUser") }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="grid gap-1.5">
|
||||
<div v-for="member in members" :key="member.grantee" class="flex items-center gap-2 rounded border bg-muted/20 px-3 py-2">
|
||||
<UsersRound class="h-3.5 w-3.5 shrink-0 text-primary" />
|
||||
<span class="min-w-0 flex-1 truncate font-mono text-xs">{{ member.grantee }}</span>
|
||||
<Badge v-if="isSystemMember(member.grantee)" variant="outline" class="h-5 border-amber-500/50 px-1.5 text-[10px] text-amber-700 dark:text-amber-300">{{ t("damengRoleAdmin.systemUser") }}</Badge>
|
||||
<Badge v-if="isDamengPredefinedRole(member.grantee)" variant="outline" class="h-5 border-amber-500/50 px-1.5 text-[10px] text-amber-700 dark:text-amber-300">{{ t("damengRoleAdmin.builtinRole") }}</Badge>
|
||||
<Badge v-if="member.adminOption" variant="outline" class="h-5 px-1.5 text-[10px]">ADMIN OPTION</Badge>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
class="h-6 gap-1 px-2 text-xs text-destructive hover:text-destructive"
|
||||
:disabled="isSystemMember(member.grantee) || (selectedIsBuiltinRole && isDamengPredefinedRole(member.grantee))"
|
||||
:title="isSystemMember(member.grantee) ? t('damengRoleAdmin.systemUserProtected') : selectedIsBuiltinRole && isDamengPredefinedRole(member.grantee) ? t('damengRoleAdmin.builtinRoleProtected') : ''"
|
||||
@click="previewRevokeMember(member)"
|
||||
>
|
||||
<Trash2 class="h-3 w-3" />
|
||||
{{ t("damengRoleAdmin.revokeFromUser") }}
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="members.length === 0" class="rounded border border-dashed p-6 text-center text-xs text-muted-foreground">
|
||||
{{ t("damengRoleAdmin.noMembers") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<span class="text-xs font-medium">{{ t("damengRoleAdmin.grantedPrivileges") }} ({{ grantedPrivileges.length }})</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="ml-auto h-7 gap-1 px-2 text-xs"
|
||||
:disabled="selectedIsBuiltinRole || availablePrivilegesForGrant.length === 0"
|
||||
:title="selectedIsBuiltinRole ? t('damengRoleAdmin.builtinRoleProtected') : availablePrivilegesForGrant.length === 0 ? t('damengRoleAdmin.noPrivilegesToGrant') : ''"
|
||||
@click="openGrantPrivilegeDialog"
|
||||
>
|
||||
<UserRoundPlus class="h-3.5 w-3.5" />
|
||||
{{ t("damengRoleAdmin.grantPrivilege") }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="grid gap-1.5">
|
||||
<div v-for="grant in grantedPrivileges" :key="grant.privilege" class="flex items-center gap-2 rounded border bg-muted/20 px-3 py-2">
|
||||
<ShieldCheck class="h-3.5 w-3.5 shrink-0 text-primary" />
|
||||
<span class="min-w-0 flex-1 truncate font-mono text-xs">{{ grant.privilege }}</span>
|
||||
<Badge v-if="grant.adminOption" variant="outline" class="h-5 px-1.5 text-[10px]">ADMIN OPTION</Badge>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
class="h-6 gap-1 px-2 text-xs text-destructive hover:text-destructive"
|
||||
:disabled="selectedIsBuiltinRole || isPrivilegeGrantDisabled(grant.privilege)"
|
||||
:title="selectedIsBuiltinRole ? t('damengRoleAdmin.builtinRoleProtected') : isPrivilegeGrantDisabled(grant.privilege) ? t('damengRoleAdmin.anyPrivilegeDisabled') : ''"
|
||||
@click="previewRevokePrivilege(grant)"
|
||||
>
|
||||
<Trash2 class="h-3 w-3" />
|
||||
{{ t("damengRoleAdmin.revokePrivilege") }}
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="grantedPrivileges.length === 0" class="rounded border border-dashed p-6 text-center text-xs text-muted-foreground">
|
||||
{{ t("damengRoleAdmin.noGrantedPrivileges") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<Dialog v-model:open="createDialogOpen">
|
||||
<DialogContent class="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("damengRoleAdmin.newRole") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-3 text-xs">
|
||||
<label class="grid gap-1">
|
||||
<span>{{ t("damengRoleAdmin.roleName") }}</span>
|
||||
<Input v-model="createRoleName" class="h-8 text-xs" :placeholder="t('damengRoleAdmin.roleNamePlaceholder')" />
|
||||
</label>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="createDialogOpen = false">{{ t("common.cancel") }}</Button>
|
||||
<Button :disabled="!canCreateRole" @click="previewCreateRole">{{ t("damengRoleAdmin.previewSql") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="grantUserDialogOpen">
|
||||
<DialogContent class="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("damengRoleAdmin.grantToUser") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-3 text-xs">
|
||||
<label class="grid gap-1">
|
||||
<span>{{ t("damengRoleAdmin.userToGrant") }}</span>
|
||||
<Select v-model="grantUserValue">
|
||||
<SelectTrigger class="h-8 text-xs"><SelectValue :placeholder="t('damengRoleAdmin.selectUser')" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="user in availableUsersForGrant" :key="user.username" :value="user.username">{{ user.username }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</label>
|
||||
<p class="text-[11px] text-muted-foreground">{{ t("damengRoleAdmin.grantUserHint", { role: selectedRole?.role ?? "" }) }}</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="grantUserDialogOpen = false">{{ t("common.cancel") }}</Button>
|
||||
<Button :disabled="!grantUserValue" @click="previewGrantUser">{{ t("damengRoleAdmin.previewSql") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="grantPrivilegeDialogOpen">
|
||||
<DialogContent class="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("damengRoleAdmin.grantPrivilege") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-3 text-xs">
|
||||
<label class="grid gap-1">
|
||||
<span>{{ t("damengRoleAdmin.privilegeToGrant") }}</span>
|
||||
<Select v-model="grantPrivilegeValue">
|
||||
<SelectTrigger class="h-8 text-xs"><SelectValue :placeholder="t('damengRoleAdmin.selectPrivilege')" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="privilege in availablePrivilegesForGrant" :key="privilege" :value="privilege" :disabled="isPrivilegeGrantDisabled(privilege)">{{ privilege }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</label>
|
||||
<p v-if="!enableDdlAnyPriv" class="text-[11px] text-amber-600 dark:text-amber-400">{{ t("damengRoleAdmin.anyPrivilegeDisabled") }}</p>
|
||||
<p class="text-[11px] text-muted-foreground">{{ t("damengRoleAdmin.grantPrivilegeHint", { role: selectedRole?.role ?? "" }) }}</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="grantPrivilegeDialogOpen = false">{{ t("common.cancel") }}</Button>
|
||||
<Button :disabled="!grantPrivilegeValue" @click="previewGrantPrivilege">{{ t("damengRoleAdmin.previewSql") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="previewDialogOpen">
|
||||
<DialogContent class="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("damengRoleAdmin.sqlPreview") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<pre class="max-h-[50vh] overflow-auto rounded-md border bg-muted/30 p-3 text-xs"><code>{{ pendingSql }}</code></pre>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="previewDialogOpen = false">{{ t("common.cancel") }}</Button>
|
||||
<Button :variant="pendingDanger ? 'destructive' : 'default'" :disabled="applying" @click="applyPendingSql">
|
||||
<Loader2 v-if="applying" class="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
{{ t("damengRoleAdmin.applySql") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,787 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { AlertTriangle, ChevronRight, Folder, KeyRound, Loader2, Lock, LockOpen, Plus, RefreshCcw, ShieldCheck, Trash2, UserRound, UserRoundPlus, UsersRound } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import PasswordInput from "@/components/ui/PasswordInput.vue";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import type { ConnectionConfig } from "@/types/database";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import {
|
||||
DAMENG_USER_GROUPS,
|
||||
DAMENG_SYSTEM_PRIVILEGES,
|
||||
damengAlterUserPasswordSql,
|
||||
damengCreateUserSql,
|
||||
damengDropUserSql,
|
||||
damengGrantRoleSql,
|
||||
damengGrantSystemPrivilegeSql,
|
||||
damengListRolesSql,
|
||||
damengListTablespacesSql,
|
||||
damengListUserRolesSql,
|
||||
damengListUserSysPrivsSql,
|
||||
damengListUsersSql,
|
||||
damengLockUserSql,
|
||||
damengRevokeRoleSql,
|
||||
damengRevokeSystemPrivilegeSql,
|
||||
damengSystemPrivilegeMapSql,
|
||||
damengRoleGraphSql,
|
||||
damengRolesClosure,
|
||||
damengUnlockUserSql,
|
||||
damengUserTypeGrantSqls,
|
||||
canDamengAlterUserPassword,
|
||||
canDamengRevokeRoleGrant,
|
||||
isDamengSystemUser,
|
||||
isDamengPredefinedRole,
|
||||
DAMENG_HIDDEN_ROLES,
|
||||
damengUserGroup,
|
||||
damengSystemUserCategory,
|
||||
parseDamengRoles,
|
||||
parseDamengRoleGraph,
|
||||
parseDamengRolePrivs,
|
||||
parseDamengSysPrivs,
|
||||
parseDamengTablespaces,
|
||||
parseDamengSystemPrivilegeMap,
|
||||
damengAvailableSystemPrivileges,
|
||||
parseDamengRoleGrants,
|
||||
parseDamengUsers,
|
||||
type DamengGrant,
|
||||
type DamengRole,
|
||||
type DamengSysPrivilege,
|
||||
type DamengUser,
|
||||
type DamengUserGroup,
|
||||
damengEnableDdlAnyPrivSql,
|
||||
isDamengAnyPrivilege,
|
||||
parseDamengEnableDdlAnyPriv,
|
||||
} from "@/lib/database/damengPrincipalAdmin";
|
||||
|
||||
const props = defineProps<{
|
||||
connection: ConnectionConfig;
|
||||
}>();
|
||||
|
||||
type DetailTab = "roles" | "privileges";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const connectionStore = useConnectionStore();
|
||||
|
||||
const users = ref<DamengUser[]>([]);
|
||||
const allRoles = ref<DamengRole[]>([]);
|
||||
const tablespaces = ref<string[]>([]);
|
||||
const selectedUsername = ref("");
|
||||
const search = ref("");
|
||||
const loadingUsers = ref(false);
|
||||
const loadingRoles = ref(false);
|
||||
const loadingDetails = ref(false);
|
||||
const applying = ref(false);
|
||||
const loadError = ref("");
|
||||
const detailError = ref("");
|
||||
const detailTab = ref<DetailTab>("roles");
|
||||
const grantedRoles = ref<DamengGrant[]>([]);
|
||||
const grantedPrivileges = ref<DamengSysPrivilege[]>([]);
|
||||
const systemPrivilegeMap = ref<Set<string> | null>(null);
|
||||
const enableDdlAnyPriv = ref(true);
|
||||
|
||||
const createDialogOpen = ref(false);
|
||||
const createUsername = ref("");
|
||||
const createPassword = ref("");
|
||||
const createTablespace = ref("");
|
||||
const createLocked = ref(false);
|
||||
const createUserType = ref<Exclude<DamengUserGroup, "other">>("admin");
|
||||
const createdUsername = ref("");
|
||||
const userRoleGrants = ref<Map<string, Set<string>>>(new Map());
|
||||
const collapsedGroups = ref<Record<string, boolean>>({});
|
||||
|
||||
function groupLabel(group: DamengUserGroup): string {
|
||||
if (group === "other") return t("damengUserAdmin.groupOther");
|
||||
const key = "damengUserAdmin.systemUser" + group.charAt(0).toUpperCase() + group.slice(1);
|
||||
return t(key);
|
||||
}
|
||||
|
||||
function usersInGroup(group: DamengUserGroup): DamengUser[] {
|
||||
return filteredUsers.value.filter((user) => damengUserGroup(user.username, userRoleGrants.value.get(user.username.trim().toUpperCase())) === group);
|
||||
}
|
||||
|
||||
function toggleGroup(group: DamengUserGroup) {
|
||||
collapsedGroups.value[group] = !collapsedGroups.value[group];
|
||||
}
|
||||
|
||||
const passwordDialogOpen = ref(false);
|
||||
const passwordValue = ref("");
|
||||
const passwordConfirm = ref("");
|
||||
|
||||
const grantRoleDialogOpen = ref(false);
|
||||
const grantRoleValue = ref("");
|
||||
const grantPrivilegeDialogOpen = ref(false);
|
||||
const grantPrivilegeValue = ref("");
|
||||
|
||||
const previewDialogOpen = ref(false);
|
||||
const pendingSql = ref("");
|
||||
const pendingAfterApply = ref<(() => void | Promise<void>) | undefined>();
|
||||
const pendingDanger = ref(false);
|
||||
|
||||
const supported = computed(() => props.connection.db_type === "dameng");
|
||||
const executionDatabase = computed(() => (props.connection.db_type === "dameng" ? "" : props.connection.database || ""));
|
||||
const selectedUser = computed(() => users.value.find((user) => user.username === selectedUsername.value));
|
||||
const filteredUsers = computed(() => {
|
||||
const query = search.value.trim().toLowerCase();
|
||||
if (!query) return users.value;
|
||||
return users.value.filter((user) => `${user.username} ${user.defaultTablespace}`.toLowerCase().includes(query));
|
||||
});
|
||||
const selectedIsSystemUser = computed(() => !!selectedUser.value && isDamengSystemUser(selectedUser.value.username));
|
||||
|
||||
/**
|
||||
* Whether the current connection user is allowed to change the selected
|
||||
* user's password, following the DM security model (see canDamengAlterUserPassword).
|
||||
*/
|
||||
const canAlterPasswordForSelected = computed(() => canDamengAlterUserPassword(props.connection.username, selectedUser.value?.username));
|
||||
|
||||
const selectedUserCategory = computed(() => (selectedUser.value ? damengSystemUserCategory(selectedUser.value.username) : undefined));
|
||||
const selectedUserCategoryLabel = computed(() => {
|
||||
if (!selectedUserCategory.value) return "";
|
||||
const key = "damengUserAdmin.systemUser" + selectedUserCategory.value.charAt(0).toUpperCase() + selectedUserCategory.value.slice(1);
|
||||
return t(key);
|
||||
});
|
||||
|
||||
function userCategoryLabel(username: string): string {
|
||||
const category = damengSystemUserCategory(username);
|
||||
if (!category) return "";
|
||||
const key = "damengUserAdmin.systemUser" + category.charAt(0).toUpperCase() + category.slice(1);
|
||||
return t(key);
|
||||
}
|
||||
const availableRolesForGrant = computed(() => {
|
||||
// Roles already held (directly or via nesting) plus hidden internal roles
|
||||
// that cannot be granted (e.g. SYS_ADMIN) are not offered.
|
||||
const owned = new Set(grantedRoles.value.map((grant) => grant.grantedRole.toUpperCase()));
|
||||
const hidden = DAMENG_HIDDEN_ROLES.map((role) => role.toUpperCase());
|
||||
return allRoles.value.filter((role) => !owned.has(role.role.toUpperCase()) && !hidden.includes(role.role.toUpperCase()));
|
||||
});
|
||||
const availablePrivilegesForGrant = computed(() => {
|
||||
const granted = new Set(grantedPrivileges.value.map((grant) => grant.privilege.toUpperCase()));
|
||||
const catalog = systemPrivilegeMap.value ? damengAvailableSystemPrivileges(DAMENG_SYSTEM_PRIVILEGES, systemPrivilegeMap.value) : DAMENG_SYSTEM_PRIVILEGES;
|
||||
return catalog.filter((privilege) => !granted.has(privilege.toUpperCase()));
|
||||
});
|
||||
const canCreateUser = computed(() => createUsername.value.trim() !== "" && createPassword.value !== "");
|
||||
const canChangePassword = computed(() => passwordValue.value !== "" && passwordValue.value === passwordConfirm.value);
|
||||
|
||||
async function ensureConnection() {
|
||||
await connectionStore.ensureConnected(props.connection.id);
|
||||
}
|
||||
|
||||
async function refreshConnectionTree() {
|
||||
try {
|
||||
// DM users map 1:1 to schemas, so reload the sidebar database/schema tree
|
||||
// after user DDL. Best-effort: the in-page user list is already refreshed.
|
||||
await connectionStore.loadDatabases(props.connection.id, { force: true });
|
||||
} catch {
|
||||
// ignore tree refresh failures
|
||||
}
|
||||
}
|
||||
|
||||
function isDdlAnyPrivDenied(message: string): boolean {
|
||||
// DM8 error -5567: "Grantor has no granted privilege" / "授权者没有此授权权限",
|
||||
// typically because ENABLE_DDL_ANY_PRIV=0 blocks granting ANY-type privileges.
|
||||
return /授权者没有此授权权限|Grantor no granted privilege|[-]?5567/i.test(message);
|
||||
}
|
||||
|
||||
function formatApplyFailedMessage(message: string): string {
|
||||
return isDdlAnyPrivDenied(message) ? `${message}\n${t("damengUserAdmin.enableDdlAnyPrivHint")}` : message;
|
||||
}
|
||||
|
||||
function isPrivilegeGrantDisabled(privilege: string): boolean {
|
||||
return !enableDdlAnyPriv.value && isDamengAnyPrivilege(privilege);
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
if (!supported.value) return;
|
||||
loadingUsers.value = true;
|
||||
loadError.value = "";
|
||||
try {
|
||||
await ensureConnection();
|
||||
// Load the full role-grant map once so list grouping can place regular users
|
||||
// holding management roles (DBA/DB_AUDIT_ADMIN/DB_POLICY_ADMIN) into the
|
||||
// matching category. Falls back to an empty map when the view is unreadable.
|
||||
const [result, roleGrantsResult] = await Promise.all([
|
||||
api.executeQuery(props.connection.id, executionDatabase.value, damengListUsersSql(), undefined, undefined, { maxRows: 5000 }),
|
||||
api.executeQuery(props.connection.id, executionDatabase.value, damengRoleGraphSql(), undefined, undefined, { maxRows: 5000 }).catch(() => undefined),
|
||||
]);
|
||||
users.value = parseDamengUsers(result);
|
||||
userRoleGrants.value = roleGrantsResult ? parseDamengRoleGrants(roleGrantsResult) : new Map();
|
||||
// Auto-select a user just created through the dialog.
|
||||
if (createdUsername.value) {
|
||||
const created = users.value.find((user) => user.username.toLowerCase() === createdUsername.value.toLowerCase());
|
||||
if (created) {
|
||||
selectedUsername.value = created.username;
|
||||
createdUsername.value = "";
|
||||
}
|
||||
}
|
||||
if (!selectedUser.value) selectedUsername.value = users.value[0]?.username || "";
|
||||
await loadDetails();
|
||||
} catch (error: any) {
|
||||
loadError.value = error?.message || String(error);
|
||||
users.value = [];
|
||||
grantedRoles.value = [];
|
||||
grantedPrivileges.value = [];
|
||||
} finally {
|
||||
loadingUsers.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRolesAndTablespaces() {
|
||||
if (!supported.value) return;
|
||||
loadingRoles.value = true;
|
||||
try {
|
||||
await ensureConnection();
|
||||
const [rolesResult, tablespaceResult, privilegeMapResult, ddlAnyPrivResult] = await Promise.all([
|
||||
api.executeQuery(props.connection.id, executionDatabase.value, damengListRolesSql(), undefined, undefined, { maxRows: 5000 }),
|
||||
api.executeQuery(props.connection.id, executionDatabase.value, damengListTablespacesSql(), undefined, undefined, { maxRows: 1000 }),
|
||||
// SYSTEM_PRIVILEGE_MAP may be unreadable for non-privileged accounts; fall back to the full catalog.
|
||||
api.executeQuery(props.connection.id, executionDatabase.value, damengSystemPrivilegeMapSql(), undefined, undefined, { maxRows: 5000 }).catch(() => undefined),
|
||||
// DM8 default disables ANY-type privilege grants (ENABLE_DDL_ANY_PRIV=0); keep the default enabled on failure.
|
||||
api.executeQuery(props.connection.id, executionDatabase.value, damengEnableDdlAnyPrivSql(), undefined, undefined, { maxRows: 10 }).catch(() => undefined),
|
||||
]);
|
||||
allRoles.value = parseDamengRoles(rolesResult);
|
||||
tablespaces.value = parseDamengTablespaces(tablespaceResult);
|
||||
systemPrivilegeMap.value = privilegeMapResult ? parseDamengSystemPrivilegeMap(privilegeMapResult) : null;
|
||||
enableDdlAnyPriv.value = ddlAnyPrivResult ? parseDamengEnableDdlAnyPriv(ddlAnyPrivResult) : true;
|
||||
} catch {
|
||||
allRoles.value = [];
|
||||
tablespaces.value = [];
|
||||
systemPrivilegeMap.value = null;
|
||||
} finally {
|
||||
loadingRoles.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
let detailRequestSeq = 0;
|
||||
|
||||
async function loadDetails() {
|
||||
const user = selectedUser.value;
|
||||
if (!user) {
|
||||
grantedRoles.value = [];
|
||||
grantedPrivileges.value = [];
|
||||
return;
|
||||
}
|
||||
const seq = ++detailRequestSeq;
|
||||
loadingDetails.value = true;
|
||||
detailError.value = "";
|
||||
try {
|
||||
await ensureConnection();
|
||||
const [rolesResult, privsResult, graphResult] = await Promise.all([
|
||||
api.executeQuery(props.connection.id, executionDatabase.value, damengListUserRolesSql(user.username), undefined, undefined, { maxRows: 1000 }),
|
||||
api.executeQuery(props.connection.id, executionDatabase.value, damengListUserSysPrivsSql(user.username), undefined, undefined, { maxRows: 1000 }),
|
||||
api.executeQuery(props.connection.id, executionDatabase.value, damengRoleGraphSql(), undefined, undefined, { maxRows: 5000 }),
|
||||
]);
|
||||
if (seq !== detailRequestSeq) return; // stale response for a previous selection
|
||||
const directRoles = parseDamengRolePrivs(rolesResult);
|
||||
const graph = parseDamengRoleGraph(graphResult);
|
||||
// Official DM tool shows the transitive closure of roles (e.g. SYSDBA also
|
||||
// has SVI via PUBLIC and VTI via DBA), excluding hidden internal roles.
|
||||
grantedRoles.value = damengRolesClosure(user.username, graph).map((entry) => {
|
||||
const direct = directRoles.find((grant) => grant.grantedRole.toUpperCase() === entry.role);
|
||||
return direct ?? { grantee: user.username, grantedRole: entry.role, adminOption: false, defaultRole: false, inherited: !entry.direct, via: entry.via };
|
||||
});
|
||||
grantedPrivileges.value = parseDamengSysPrivs(privsResult);
|
||||
} catch (error: any) {
|
||||
if (seq !== detailRequestSeq) return; // stale response for a previous selection
|
||||
detailError.value = error?.message || String(error);
|
||||
grantedRoles.value = [];
|
||||
grantedPrivileges.value = [];
|
||||
} finally {
|
||||
if (seq === detailRequestSeq) loadingDetails.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function selectUser(user: DamengUser) {
|
||||
selectedUsername.value = user.username;
|
||||
void loadDetails();
|
||||
}
|
||||
|
||||
function previewSql(sql: string, options: { danger?: boolean; afterApply?: () => void | Promise<void> } = {}) {
|
||||
pendingSql.value = sql;
|
||||
pendingDanger.value = !!options.danger;
|
||||
pendingAfterApply.value = options.afterApply;
|
||||
previewDialogOpen.value = true;
|
||||
}
|
||||
|
||||
async function applyPendingSql() {
|
||||
if (!pendingSql.value.trim()) return;
|
||||
applying.value = true;
|
||||
try {
|
||||
await ensureConnection();
|
||||
const results = await executeWithProductionSqlGuard({
|
||||
connection: props.connection,
|
||||
database: executionDatabase.value,
|
||||
sql: pendingSql.value,
|
||||
source: t("production.sourceAdmin"),
|
||||
execute: () =>
|
||||
api.executeMulti(props.connection.id, executionDatabase.value, pendingSql.value, undefined, undefined, {
|
||||
maxRows: 1000,
|
||||
useTransaction: true,
|
||||
}),
|
||||
});
|
||||
if (!results) return;
|
||||
// Per-statement errors are returned inside the result array (HTTP 200), so
|
||||
// they must be surfaced explicitly instead of being treated as success.
|
||||
const failed = results.find((result) => !!result.error);
|
||||
if (failed) {
|
||||
toast(t("damengUserAdmin.applyFailed", { message: formatApplyFailedMessage(translateBackendError(t, failed.error)) }), 5000);
|
||||
// Statements before the error may already be committed, so refresh regardless.
|
||||
await loadUsers();
|
||||
// Some statements may already be committed (per-statement autocommit), so keep the sidebar tree in sync too.
|
||||
await refreshConnectionTree();
|
||||
return;
|
||||
}
|
||||
toast(t("damengUserAdmin.applySuccess"), 2500);
|
||||
previewDialogOpen.value = false;
|
||||
await (pendingAfterApply.value?.() ?? Promise.resolve());
|
||||
await loadUsers();
|
||||
// Users map 1:1 to schemas on DM; keep the sidebar connection tree in sync after create/drop/etc.
|
||||
await refreshConnectionTree();
|
||||
} catch (error: any) {
|
||||
toast(t("damengUserAdmin.applyFailed", { message: formatApplyFailedMessage(error?.message || String(error)) }), 5000);
|
||||
} finally {
|
||||
applying.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
createUsername.value = "";
|
||||
createPassword.value = "";
|
||||
createTablespace.value = tablespaces.value[0] ?? "";
|
||||
createLocked.value = false;
|
||||
createDialogOpen.value = true;
|
||||
}
|
||||
|
||||
function previewCreateUser() {
|
||||
if (!canCreateUser.value) return;
|
||||
const statements = [
|
||||
damengCreateUserSql({
|
||||
username: createUsername.value,
|
||||
password: createPassword.value,
|
||||
tablespace: createTablespace.value,
|
||||
locked: createLocked.value,
|
||||
}),
|
||||
...damengUserTypeGrantSqls(createUserType.value, createUsername.value),
|
||||
];
|
||||
previewSql(statements.join("\n"), {
|
||||
afterApply: () => {
|
||||
createDialogOpen.value = false;
|
||||
createdUsername.value = createUsername.value.trim();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function openChangePasswordDialog() {
|
||||
passwordValue.value = "";
|
||||
passwordConfirm.value = "";
|
||||
passwordDialogOpen.value = true;
|
||||
}
|
||||
|
||||
function previewChangePassword() {
|
||||
const user = selectedUser.value;
|
||||
if (!user || !canAlterPasswordForSelected.value || !canChangePassword.value) return;
|
||||
previewSql(damengAlterUserPasswordSql(user.username, passwordValue.value), {
|
||||
afterApply: () => {
|
||||
passwordDialogOpen.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function previewLockUser(locked: boolean) {
|
||||
const user = selectedUser.value;
|
||||
if (!user) return;
|
||||
previewSql(locked ? damengLockUserSql(user.username) : damengUnlockUserSql(user.username));
|
||||
}
|
||||
|
||||
function previewDropUser() {
|
||||
const user = selectedUser.value;
|
||||
if (!user) return;
|
||||
previewSql(damengDropUserSql(user.username), { danger: true });
|
||||
}
|
||||
|
||||
function openGrantRoleDialog() {
|
||||
grantRoleValue.value = "";
|
||||
grantRoleDialogOpen.value = true;
|
||||
}
|
||||
|
||||
function previewGrantRole() {
|
||||
const user = selectedUser.value;
|
||||
if (!user || !grantRoleValue.value) return;
|
||||
previewSql(damengGrantRoleSql(user.username, grantRoleValue.value), {
|
||||
afterApply: () => {
|
||||
grantRoleDialogOpen.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function previewRevokeRole(grant: DamengGrant) {
|
||||
const user = selectedUser.value;
|
||||
if (!user || !canDamengRevokeRoleGrant(user.username, grant)) return;
|
||||
previewSql(damengRevokeRoleSql(user.username, grant.grantedRole), { danger: true });
|
||||
}
|
||||
|
||||
function openGrantPrivilegeDialog() {
|
||||
grantPrivilegeValue.value = "";
|
||||
grantPrivilegeDialogOpen.value = true;
|
||||
}
|
||||
|
||||
function previewGrantPrivilege() {
|
||||
const user = selectedUser.value;
|
||||
if (!user || !grantPrivilegeValue.value) return;
|
||||
previewSql(damengGrantSystemPrivilegeSql(user.username, grantPrivilegeValue.value), {
|
||||
afterApply: () => {
|
||||
grantPrivilegeDialogOpen.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function previewRevokePrivilege(grant: DamengSysPrivilege) {
|
||||
const user = selectedUser.value;
|
||||
if (!user) return;
|
||||
previewSql(damengRevokeSystemPrivilegeSql(user.username, grant.privilege), { danger: true });
|
||||
}
|
||||
|
||||
function accountStatusBadgeVariant(user: DamengUser): "default" | "secondary" | "destructive" | "outline" {
|
||||
if (!user.locked) return "default";
|
||||
return /EXPIRED/i.test(user.accountStatus) ? "destructive" : "secondary";
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadUsers();
|
||||
void loadRolesAndTablespaces();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col bg-background">
|
||||
<div class="flex h-12 shrink-0 items-center gap-2 border-b px-3">
|
||||
<UsersRound class="h-4 w-4 text-primary" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm font-semibold">{{ t("damengUserAdmin.title") }}</div>
|
||||
<div class="truncate text-[11px] text-muted-foreground">{{ connection.name }}</div>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" class="h-8 gap-1.5" :disabled="loadingUsers" @click="loadUsers">
|
||||
<Loader2 v-if="loadingUsers" class="h-3.5 w-3.5 animate-spin" />
|
||||
<RefreshCcw v-else class="h-3.5 w-3.5" />
|
||||
{{ t("contextMenu.refreshChildren") }}
|
||||
</Button>
|
||||
<Button size="sm" class="h-8 gap-1.5" @click="openCreateDialog">
|
||||
<Plus class="h-3.5 w-3.5" />
|
||||
{{ t("damengUserAdmin.newUser") }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="!supported" class="m-4 rounded border border-dashed p-4 text-sm text-muted-foreground">
|
||||
{{ t("damengUserAdmin.unsupported") }}
|
||||
</div>
|
||||
|
||||
<div v-else class="grid min-h-0 flex-1 grid-cols-[300px_minmax(0,1fr)]">
|
||||
<aside class="flex min-h-0 flex-col border-r">
|
||||
<div class="border-b p-2">
|
||||
<Input v-model="search" class="h-8 text-xs" :placeholder="t('damengUserAdmin.searchUser')" />
|
||||
</div>
|
||||
<div v-if="loadError" class="m-2 rounded border border-destructive/30 bg-destructive/5 p-2 text-xs text-destructive">
|
||||
<div class="mb-1 flex items-center gap-1 font-medium"><AlertTriangle class="h-3.5 w-3.5" />{{ t("damengUserAdmin.loadFailed") }}</div>
|
||||
<div class="break-all">{{ loadError }}</div>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-auto p-2">
|
||||
<template v-for="group in DAMENG_USER_GROUPS" :key="group">
|
||||
<div v-if="usersInGroup(group).length > 0" class="mb-0.5 mt-1 first:mt-0">
|
||||
<button type="button" class="flex w-full items-center gap-1 rounded px-1 py-1 text-[11px] font-medium text-muted-foreground hover:bg-accent" @click="toggleGroup(group)">
|
||||
<ChevronRight class="h-3 w-3 transition-transform" :class="collapsedGroups[group] ? '' : 'rotate-90'" />
|
||||
<Folder class="h-3 w-3" />
|
||||
<span class="truncate">{{ groupLabel(group) }}</span>
|
||||
<span class="ml-auto text-[10px] opacity-70">{{ usersInGroup(group).length }}</span>
|
||||
</button>
|
||||
<div v-if="!collapsedGroups[group]">
|
||||
<button
|
||||
v-for="user in usersInGroup(group)"
|
||||
:key="user.username"
|
||||
type="button"
|
||||
class="mb-1 w-full rounded border px-2 py-2 text-left text-xs transition hover:bg-accent"
|
||||
:class="selectedUsername === user.username ? 'border-primary bg-primary/10' : 'border-transparent'"
|
||||
@click="selectUser(user)"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="min-w-0 flex-1 truncate font-medium">{{ user.username }}</span>
|
||||
<Badge v-if="userCategoryLabel(user.username)" variant="outline" class="h-5 border-amber-500/50 px-1.5 text-[10px] text-amber-700 dark:text-amber-300">{{ userCategoryLabel(user.username) }}</Badge>
|
||||
<Badge v-if="user.locked" :variant="accountStatusBadgeVariant(user)" class="h-5 px-1.5 text-[10px]">{{ user.accountStatus }}</Badge>
|
||||
</div>
|
||||
<div class="mt-1 truncate text-[11px] text-muted-foreground">{{ user.defaultTablespace || "-" }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="!loadingUsers && filteredUsers.length === 0" class="p-6 text-center text-xs text-muted-foreground">
|
||||
{{ t("damengUserAdmin.emptyUsers") }}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="flex min-h-0 flex-col">
|
||||
<div v-if="selectedUser" class="flex shrink-0 flex-wrap items-center gap-2 border-b p-3">
|
||||
<UserRound class="h-4 w-4 text-primary" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="truncate text-sm font-semibold">{{ selectedUser.username }}</span>
|
||||
<Badge v-if="selectedUser.locked" :variant="accountStatusBadgeVariant(selectedUser)" class="h-5 px-1.5 text-[10px]">{{ selectedUser.accountStatus }}</Badge>
|
||||
<Badge v-else variant="outline" class="h-5 px-1.5 text-[10px]">{{ t("damengUserAdmin.open") }}</Badge>
|
||||
<Badge v-if="selectedUserCategoryLabel" variant="outline" class="h-5 border-amber-500/50 px-1.5 text-[10px] text-amber-700 dark:text-amber-300">{{ selectedUserCategoryLabel }}</Badge>
|
||||
</div>
|
||||
<div class="truncate text-xs text-muted-foreground">{{ t("damengUserAdmin.defaultTablespace") }}: {{ selectedUser.defaultTablespace || "-" }} · {{ t("damengUserAdmin.created") }}: {{ selectedUser.created || "-" }}</div>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" class="h-8 gap-1.5" :disabled="!canAlterPasswordForSelected" :title="canAlterPasswordForSelected ? '' : t('damengUserAdmin.cannotChangePassword')" @click="openChangePasswordDialog">
|
||||
<KeyRound class="h-3.5 w-3.5" />
|
||||
{{ t("damengUserAdmin.changePassword") }}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" class="h-8 gap-1.5" :disabled="selectedIsSystemUser" :title="selectedIsSystemUser ? t('damengUserAdmin.systemUserProtected') : ''" @click="previewLockUser(!selectedUser.locked)">
|
||||
<LockOpen v-if="selectedUser.locked" class="h-3.5 w-3.5" />
|
||||
<Lock v-else class="h-3.5 w-3.5" />
|
||||
{{ selectedUser.locked ? t("damengUserAdmin.unlock") : t("damengUserAdmin.lock") }}
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive" class="h-8 gap-1.5" :disabled="selectedIsSystemUser" :title="selectedIsSystemUser ? t('damengUserAdmin.systemUserProtected') : ''" @click="previewDropUser">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
{{ t("damengUserAdmin.dropUser") }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedUser" class="flex h-10 shrink-0 items-center gap-1 border-b px-3">
|
||||
<Button size="sm" :variant="detailTab === 'roles' ? 'secondary' : 'ghost'" class="h-7 px-2 text-xs" @click="detailTab = 'roles'">{{ t("damengUserAdmin.roles") }}</Button>
|
||||
<Button size="sm" :variant="detailTab === 'privileges' ? 'secondary' : 'ghost'" class="h-7 px-2 text-xs" @click="detailTab = 'privileges'">{{ t("damengUserAdmin.systemPrivileges") }}</Button>
|
||||
<Loader2 v-if="loadingDetails" class="ml-auto h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-auto p-3">
|
||||
<div v-if="!selectedUser" class="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
{{ t("damengUserAdmin.selectUser") }}
|
||||
</div>
|
||||
<div v-else-if="detailError" class="rounded border border-destructive/30 bg-destructive/5 p-3 text-xs text-destructive">
|
||||
<div class="mb-1 flex items-center gap-1 font-medium"><AlertTriangle class="h-3.5 w-3.5" />{{ t("damengUserAdmin.loadFailed") }}</div>
|
||||
<div class="break-all">{{ detailError }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="detailTab === 'roles'">
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<span class="text-xs font-medium">{{ t("damengUserAdmin.grantedRoles") }} ({{ grantedRoles.length }})</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="ml-auto h-7 gap-1 px-2 text-xs"
|
||||
:disabled="selectedIsSystemUser || availableRolesForGrant.length === 0"
|
||||
:title="selectedIsSystemUser ? t('damengUserAdmin.systemUserProtected') : availableRolesForGrant.length === 0 ? t('damengUserAdmin.noRolesToGrant') : ''"
|
||||
@click="openGrantRoleDialog"
|
||||
>
|
||||
<UserRoundPlus class="h-3.5 w-3.5" />
|
||||
{{ t("damengUserAdmin.grantRole") }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="grid gap-1.5">
|
||||
<div v-for="grant in grantedRoles" :key="grant.grantedRole" class="flex items-center gap-2 rounded border bg-muted/20 px-3 py-2">
|
||||
<ShieldCheck class="h-3.5 w-3.5 shrink-0 text-primary" />
|
||||
<span class="min-w-0 flex-1 truncate font-mono text-xs">{{ grant.grantedRole }}</span>
|
||||
<Badge v-if="grant.adminOption" variant="outline" class="h-5 px-1.5 text-[10px]">ADMIN OPTION</Badge>
|
||||
<Badge v-if="isDamengPredefinedRole(grant.grantedRole)" variant="outline" class="h-5 border-amber-500/50 px-1.5 text-[10px] text-amber-700 dark:text-amber-300">{{ t("damengUserAdmin.builtinRole") }}</Badge>
|
||||
<Badge v-if="grant.inherited" variant="secondary" class="h-5 px-1.5 text-[10px]" :title="grant.via">{{ t("damengUserAdmin.inheritedFrom", { via: grant.via ?? "" }) }}</Badge>
|
||||
<Button
|
||||
v-if="!grant.inherited"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
class="h-6 gap-1 px-2 text-xs text-destructive hover:text-destructive"
|
||||
:disabled="!canDamengRevokeRoleGrant(selectedUser.username, grant)"
|
||||
:title="selectedIsSystemUser ? t('damengUserAdmin.systemUserProtected') : ''"
|
||||
@click="previewRevokeRole(grant)"
|
||||
>
|
||||
<Trash2 class="h-3 w-3" />
|
||||
{{ t("damengUserAdmin.revokeRole") }}
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="grantedRoles.length === 0" class="rounded border border-dashed p-6 text-center text-xs text-muted-foreground">
|
||||
{{ t("damengUserAdmin.noGrantedRoles") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<span class="text-xs font-medium">{{ t("damengUserAdmin.grantedPrivileges") }} ({{ grantedPrivileges.length }})</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="ml-auto h-7 gap-1 px-2 text-xs"
|
||||
:disabled="selectedIsSystemUser || availablePrivilegesForGrant.length === 0"
|
||||
:title="selectedIsSystemUser ? t('damengUserAdmin.systemUserProtected') : availablePrivilegesForGrant.length === 0 ? t('damengUserAdmin.noPrivilegesToGrant') : ''"
|
||||
@click="openGrantPrivilegeDialog"
|
||||
>
|
||||
<UserRoundPlus class="h-3.5 w-3.5" />
|
||||
{{ t("damengUserAdmin.grantPrivilege") }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="grid gap-1.5">
|
||||
<div v-for="grant in grantedPrivileges" :key="grant.privilege" class="flex items-center gap-2 rounded border bg-muted/20 px-3 py-2">
|
||||
<ShieldCheck class="h-3.5 w-3.5 shrink-0 text-primary" />
|
||||
<span class="min-w-0 flex-1 truncate font-mono text-xs">{{ grant.privilege }}</span>
|
||||
<Badge v-if="grant.adminOption" variant="outline" class="h-5 px-1.5 text-[10px]">ADMIN OPTION</Badge>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
class="h-6 gap-1 px-2 text-xs text-destructive hover:text-destructive"
|
||||
:disabled="selectedIsSystemUser || isPrivilegeGrantDisabled(grant.privilege)"
|
||||
:title="selectedIsSystemUser ? t('damengUserAdmin.systemUserProtected') : isPrivilegeGrantDisabled(grant.privilege) ? t('damengUserAdmin.anyPrivilegeDisabled') : ''"
|
||||
@click="previewRevokePrivilege(grant)"
|
||||
>
|
||||
<Trash2 class="h-3 w-3" />
|
||||
{{ t("damengUserAdmin.revokePrivilege") }}
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="grantedPrivileges.length === 0" class="rounded border border-dashed p-6 text-center text-xs text-muted-foreground">
|
||||
{{ t("damengUserAdmin.noGrantedPrivileges") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<Dialog v-model:open="createDialogOpen">
|
||||
<DialogContent class="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("damengUserAdmin.newUser") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-3 text-xs">
|
||||
<label class="grid gap-1">
|
||||
<span>{{ t("damengUserAdmin.username") }}</span>
|
||||
<Input v-model="createUsername" class="h-8 text-xs" :placeholder="t('damengUserAdmin.usernamePlaceholder')" />
|
||||
</label>
|
||||
<label class="grid gap-1">
|
||||
<span>{{ t("damengUserAdmin.userType") }}</span>
|
||||
<Select v-model="createUserType">
|
||||
<SelectTrigger class="h-8 text-xs"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="group in DAMENG_USER_GROUPS.slice(0, 3)" :key="group" :value="group">{{ groupLabel(group) }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span class="text-[10px] text-muted-foreground">{{ t("damengUserAdmin.userTypeHint") }}</span>
|
||||
</label>
|
||||
<label class="grid gap-1">
|
||||
<span>{{ t("damengUserAdmin.password") }}</span>
|
||||
<PasswordInput v-model="createPassword" class="h-8 text-xs" />
|
||||
<span :class="createPassword.length > 0 && createPassword.length < 9 ? 'text-[10px] text-amber-600 dark:text-amber-400' : 'text-[10px] text-muted-foreground'">{{ t("damengUserAdmin.passwordPolicyHint") }}</span>
|
||||
</label>
|
||||
<label class="grid gap-1">
|
||||
<span>{{ t("damengUserAdmin.tablespace") }}</span>
|
||||
<Select v-if="tablespaces.length > 0" v-model="createTablespace">
|
||||
<SelectTrigger class="h-8 text-xs"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="space in tablespaces" :key="space" :value="space">{{ space }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input v-else v-model="createTablespace" class="h-8 text-xs" :placeholder="t('damengUserAdmin.tablespacePlaceholder')" />
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input v-model="createLocked" type="checkbox" class="h-3.5 w-3.5 accent-primary" />
|
||||
<span>{{ t("damengUserAdmin.locked") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="createDialogOpen = false">{{ t("common.cancel") }}</Button>
|
||||
<Button :disabled="!canCreateUser" @click="previewCreateUser">{{ t("damengUserAdmin.previewSql") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="passwordDialogOpen">
|
||||
<DialogContent class="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("damengUserAdmin.changePassword") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-3 text-xs">
|
||||
<label class="grid gap-1">
|
||||
<span>{{ t("damengUserAdmin.newPassword") }}</span>
|
||||
<PasswordInput v-model="passwordValue" class="h-8 text-xs" />
|
||||
<span :class="passwordValue.length > 0 && passwordValue.length < 9 ? 'text-[10px] text-amber-600 dark:text-amber-400' : 'text-[10px] text-muted-foreground'">{{ t("damengUserAdmin.passwordPolicyHint") }}</span>
|
||||
</label>
|
||||
<label class="grid gap-1">
|
||||
<span>{{ t("damengUserAdmin.confirmPassword") }}</span>
|
||||
<PasswordInput v-model="passwordConfirm" class="h-8 text-xs" />
|
||||
</label>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="passwordDialogOpen = false">{{ t("common.cancel") }}</Button>
|
||||
<Button :disabled="!canChangePassword" @click="previewChangePassword">{{ t("damengUserAdmin.previewSql") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="grantRoleDialogOpen">
|
||||
<DialogContent class="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("damengUserAdmin.grantRole") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-3 text-xs">
|
||||
<label class="grid gap-1">
|
||||
<span>{{ t("damengUserAdmin.roleToGrant") }}</span>
|
||||
<Select v-model="grantRoleValue">
|
||||
<SelectTrigger class="h-8 text-xs"><SelectValue :placeholder="t('damengUserAdmin.selectRole')" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="role in availableRolesForGrant" :key="role.role" :value="role.role">{{ role.role }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</label>
|
||||
<p class="text-[11px] text-muted-foreground">{{ t("damengUserAdmin.grantRoleHint", { user: selectedUser?.username ?? "" }) }}</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="grantRoleDialogOpen = false">{{ t("common.cancel") }}</Button>
|
||||
<Button :disabled="!grantRoleValue" @click="previewGrantRole">{{ t("damengUserAdmin.previewSql") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="grantPrivilegeDialogOpen">
|
||||
<DialogContent class="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("damengUserAdmin.grantPrivilege") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-3 text-xs">
|
||||
<label class="grid gap-1">
|
||||
<span>{{ t("damengUserAdmin.privilegeToGrant") }}</span>
|
||||
<Select v-model="grantPrivilegeValue">
|
||||
<SelectTrigger class="h-8 text-xs"><SelectValue :placeholder="t('damengUserAdmin.selectPrivilege')" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="privilege in availablePrivilegesForGrant" :key="privilege" :value="privilege" :disabled="isPrivilegeGrantDisabled(privilege)">{{ privilege }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</label>
|
||||
<p v-if="!enableDdlAnyPriv" class="text-[11px] text-amber-600 dark:text-amber-400">{{ t("damengUserAdmin.anyPrivilegeDisabled") }}</p>
|
||||
<p class="text-[11px] text-muted-foreground">{{ t("damengUserAdmin.grantPrivilegeHint", { user: selectedUser?.username ?? "" }) }}</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="grantPrivilegeDialogOpen = false">{{ t("common.cancel") }}</Button>
|
||||
<Button :disabled="!grantPrivilegeValue" @click="previewGrantPrivilege">{{ t("damengUserAdmin.previewSql") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="previewDialogOpen">
|
||||
<DialogContent class="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("damengUserAdmin.sqlPreview") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<pre class="max-h-[50vh] overflow-auto rounded-md border bg-muted/30 p-3 text-xs"><code>{{ pendingSql }}</code></pre>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="previewDialogOpen = false">{{ t("common.cancel") }}</Button>
|
||||
<Button :variant="pendingDanger ? 'destructive' : 'default'" :disabled="applying" @click="applyPendingSql">
|
||||
<Loader2 v-if="applying" class="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
{{ t("damengUserAdmin.applySql") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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
|
||||
<template v-else-if="activeTab.mode === 'dameng-jobs' && activeConnection">
|
||||
<DamengJobAdmin :key="activeTab.id" :connection="activeConnection" />
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeTab.mode === 'dameng-users' && activeConnection">
|
||||
<DamengUserAdmin :key="activeTab.id" :connection="activeConnection" />
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeTab.mode === 'dameng-roles' && activeConnection">
|
||||
<DamengRoleAdmin :key="activeTab.id" :connection="activeConnection" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "テーブル",
|
||||
|
||||
@@ -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: "테이블",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "表",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
}
|
||||
|
||||
export interface DamengRole {
|
||||
role: string;
|
||||
passwordRequired: string;
|
||||
raw: Record<string, unknown>;
|
||||
}
|
||||
|
||||
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<string, string> = {
|
||||
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<string>): 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<DamengGrant, "grantedRole" | "inherited"> | 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<string, readonly string[]> = {
|
||||
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<string, Set<string>> {
|
||||
const grants = new Map<string, Set<string>>();
|
||||
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<string> {
|
||||
const names = new Set<string>();
|
||||
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>): 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<string, string[]> {
|
||||
const graph = new Map<string, string[]>();
|
||||
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<string, string[]>): DamengRoleEntry[] {
|
||||
const start = username.trim().toUpperCase();
|
||||
const seen = new Set<string>();
|
||||
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<string, unknown> {
|
||||
const object: Record<string, unknown> = {};
|
||||
columns.forEach((column, index) => {
|
||||
object[column] = row[index] ?? null;
|
||||
});
|
||||
return object;
|
||||
}
|
||||
|
||||
function valueAt(row: Record<string, unknown>, 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);
|
||||
}
|
||||
@@ -36,8 +36,8 @@ export function sortSidebarTreeChildrenForParent(parent: Pick<TreeNode, "type">,
|
||||
|
||||
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") {
|
||||
|
||||
@@ -28,6 +28,8 @@ const leafTypes: Set<TreeNodeType> = new Set([
|
||||
"vector-collection",
|
||||
"elasticsearch-index",
|
||||
"user-admin",
|
||||
"dameng-users",
|
||||
"dameng-roles",
|
||||
"saved-sql-file",
|
||||
"table-search-control",
|
||||
"load-more",
|
||||
|
||||
@@ -25,6 +25,8 @@ const toggleLeafNodeTypes = new Set<TreeNodeType>([
|
||||
"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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user