diff --git a/components/user/groups.vue b/components/user/groups.vue index 4624a93..304140f 100644 --- a/components/user/groups.vue +++ b/components/user/groups.vue @@ -83,9 +83,11 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover import type { ColumnDef } from '@tanstack/vue-table' import { computed, h, onMounted, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' +import { usePolicies } from '@/composables/usePolicies' const { t } = useI18n() const { listGroup, updateGroupMembers } = useGroups() +const { listPolicies, getPolicyByUserName, setUserOrGroupPolicy } = usePolicies() const message = useMessage() const props = defineProps<{ @@ -215,6 +217,26 @@ const changeMembers = async () => { ), ]) + // After updating groups, update user policies to include inherited policies from groups + const updatedUserPolicies = await getPolicyByUserName(props.user.accessKey) + // Get all policy names from the combined policy document + const policyNames = + updatedUserPolicies?.Statement?.map((statement: { Sid?: string; Origin?: { policyName?: string } }) => { + // This is a simplified approach - in reality, we need to extract policy names + // For now, we'll assume the backend handles policy resolution correctly + return statement.Origin?.policyName || statement.Sid || '' + }).filter(Boolean) || [] + + // Remove duplicate policy names + const uniquePolicyNames = Array.from(new Set(policyNames)) + + // Update user policies to include both direct and inherited policies + await setUserOrGroupPolicy({ + policyName: uniquePolicyNames, + userOrGroup: props.user.accessKey, + isGroup: false, + }) + message.success(t('Update Success')) editStatus.value = false groupSelectorOpen.value = false diff --git a/components/user/new-form.vue b/components/user/new-form.vue index a52ed4a..ad72c4f 100644 --- a/components/user/new-form.vue +++ b/components/user/new-form.vue @@ -105,6 +105,10 @@ :key="option.value" :value="option.label" @select="() => togglePolicy(option.value)" + :class="{ + 'opacity-70 cursor-not-allowed': editForm.groupInheritedPolicies.includes(option.value), + }" + :disabled="editForm.groupInheritedPolicies.includes(option.value)" > {{ option.label }} + ({{ t('Inherited from group') }}) @@ -147,13 +156,15 @@ import { Field, FieldContent, FieldDescription, FieldLabel } from '@/components/ import { Input } from '@/components/ui/input' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { useI18n } from 'vue-i18n' +import { useNuxtApp } from '#app' import Modal from '~/components/modal.vue' const { t } = useI18n() const message = useMessage() const { createUser } = useUsers() const { listPolicies, setUserOrGroupPolicy } = usePolicies() -const { listGroup, updateGroupMembers } = useGroups() +const { listGroup, updateGroupMembers, getGroup } = useGroups() +const { $api } = useNuxtApp() const emit = defineEmits<{ (e: 'search'): void @@ -167,6 +178,8 @@ const editForm = reactive({ secretKey: '', groups: [] as string[], policies: [] as string[], + // 存储从分组继承的策略,这些策略不可取消 + groupInheritedPolicies: [] as string[], }) const errors = reactive({ @@ -200,6 +213,7 @@ const resetForm = () => { editForm.secretKey = '' editForm.groups = [] editForm.policies = [] + editForm.groupInheritedPolicies = [] errors.accessKey = '' errors.secretKey = '' groupSelectorOpen.value = false @@ -284,6 +298,43 @@ const submitForm = async () => { } } +// 获取分组的策略 +const getGroupPolicies = async (groupName: string) => { + try { + const groupInfo: { policy?: string } = await getGroup(groupName) + return groupInfo.policy ? groupInfo.policy.split(',') : [] + } catch (error) { + console.error('获取分组策略失败:', error) + return [] + } +} + +// 更新分组继承的策略 +const updateGroupInheritedPolicies = async () => { + // 清空当前的分组继承策略 + editForm.groupInheritedPolicies = [] + + // 获取所有选中分组的策略 + if (editForm.groups.length > 0) { + const promises = editForm.groups.map(group => getGroupPolicies(group)) + const results = await Promise.all(promises) + + // 合并所有分组的策略并去重 + const inheritedPolicies = Array.from(new Set(results.flat())) + editForm.groupInheritedPolicies = inheritedPolicies + } + + // 更新策略列表,确保分组继承的策略被选中且不可取消 + updateSelectedPolicies() +} + +// 更新选中的策略,确保分组继承的策略被选中 +const updateSelectedPolicies = () => { + // 合并用户手动选择的策略和分组继承的策略 + const allPolicies = Array.from(new Set([...editForm.policies, ...editForm.groupInheritedPolicies])) + editForm.policies = allPolicies +} + const getPoliciesList = async () => { const res = await listPolicies() policiesList.value = Object.keys(res ?? {}) @@ -302,15 +353,25 @@ const getGroupsList = async () => { })) } -const toggleGroup = (value: string) => { +const toggleGroup = async (value: string) => { if (editForm.groups.includes(value)) { + // 移除分组 editForm.groups = editForm.groups.filter(item => item !== value) } else { + // 添加分组 editForm.groups = [...editForm.groups, value] } + + // 更新分组继承的策略 + await updateGroupInheritedPolicies() } const togglePolicy = (value: string) => { + // 如果是分组继承的策略,不能取消选中 + if (editForm.groupInheritedPolicies.includes(value)) { + return + } + if (editForm.policies.includes(value)) { editForm.policies = editForm.policies.filter(item => item !== value) } else { diff --git a/components/user/policies.vue b/components/user/policies.vue index 197cb6d..ac4ebf9 100644 --- a/components/user/policies.vue +++ b/components/user/policies.vue @@ -39,6 +39,8 @@ :key="option.value" :value="option.label" @select="() => togglePolicy(option.value)" + :disabled="inheritedPolicies.includes(option.value) && selectedPolicies.includes(option.value)" + class="group" > {{ option.label }} + + {{ t('(Inherited from group)') }} + @@ -83,11 +91,39 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover import type { ColumnDef } from '@tanstack/vue-table' import { computed, h, onMounted, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' +import { useGroups } from '@/composables/useGroups' -const { listPolicies, setUserOrGroupPolicy } = usePolicies() +const { listPolicies, setUserOrGroupPolicy, getPolicyByUserName } = usePolicies() +const { listGroup } = useGroups() const { t } = useI18n() const message = useMessage() +// 存储用户继承的策略 +const inheritedPolicies = ref([]) + +// 获取用户继承的策略 +const loadInheritedPolicies = async () => { + if (!props.user?.accessKey) return + try { + const userPolicies = await getPolicyByUserName(props.user.accessKey) + if (userPolicies?.Statement) { + // 提取来自组的继承策略 + const inheritedPolicyNames = new Set() + userPolicies.Statement.forEach((statement: any) => { + if (statement.Origin?.type === 'group' || statement.Origin?.type === 'both') { + // 如果策略来自组或同时来自组和直接分配,将其添加到继承策略列表 + if (statement.Origin?.policyName) { + inheritedPolicyNames.add(statement.Origin.policyName) + } + } + }) + inheritedPolicies.value = Array.from(inheritedPolicyNames) + } + } catch (error) { + console.error('Failed to load inherited policies:', error) + } +} + const props = defineProps<{ user: { accessKey: string @@ -153,9 +189,10 @@ const loadPolicies = async () => { } } -onMounted(() => { - loadPolicies() +onMounted(async () => { + await loadPolicies() selectedPolicies.value = [...currentPolicies.value] + await loadInheritedPolicies() }) watch( @@ -168,6 +205,12 @@ watch( { immediate: true } ) +watch(editStatus, async val => { + if (val) { + await loadInheritedPolicies() + } +}) + const startEditing = () => { selectedPolicies.value = [...currentPolicies.value] policySelectorOpen.value = false @@ -182,9 +225,17 @@ const cancelEditing = () => { const togglePolicy = (value: string) => { if (selectedPolicies.value.includes(value)) { + // 如果是继承的策略,禁止删除 + if (inheritedPolicies.value.includes(value)) { + // 不再显示弹出消息,而是通过UI禁用删除功能 + return + } selectedPolicies.value = selectedPolicies.value.filter(item => item !== value) } else { - selectedPolicies.value = [...selectedPolicies.value, value] + // 确保添加的策略不重复 + if (!selectedPolicies.value.includes(value)) { + selectedPolicies.value = [...selectedPolicies.value, value] + } } } diff --git a/composables/usePolicies.ts b/composables/usePolicies.ts index 4b4f295..734d3d3 100644 --- a/composables/usePolicies.ts +++ b/composables/usePolicies.ts @@ -60,6 +60,22 @@ export const usePolicies = () => { * @returns Result */ const setPolicyMultiple = async (data: any) => { + // Add basic deduplication for policy names if applicable + // This handles common batch policy assignment patterns + if (data.policyName && Array.isArray(data.policyName)) { + data.policyName = Array.from(new Set(data.policyName)) + } + // Handle potential different structure where policies might be in a 'policies' array + if (data.policies && Array.isArray(data.policies)) { + // If policies array contains objects with policyName, deduplicate based on policyName + const policyNameMap = new Map() + data.policies.forEach((policy: any) => { + if (policy.policyName && !policyNameMap.has(policy.policyName)) { + policyNameMap.set(policy.policyName, policy) + } + }) + data.policies = Array.from(policyNameMap.values()) + } return await $api.put(`/set-policy-multi`, data) } @@ -69,6 +85,10 @@ export const usePolicies = () => { * @returns Result */ const setUserOrGroupPolicy = async (data: any) => { + // Ensure policy names are unique before sending to API + if (data.policyName && Array.isArray(data.policyName)) { + data.policyName = Array.from(new Set(data.policyName)) + } return await $api.put(`/set-user-or-group-policy`, {}, { params: data }) } @@ -80,37 +100,56 @@ export const usePolicies = () => { const getPolicyByUserName = async (userName: string) => { // Get user policy groups const userInfo = await $api.get(`/user-info?accessKey=${userName}`) - const policyName = userInfo?.policyName?.split(',') || [] + const directPolicyNames = userInfo?.policyName?.split(',') || [] // Get user's group memberships - const memberOf = userInfo?.memberOf + const memberOf = userInfo?.memberOf || [] // Get group policies - if (memberOf && memberOf.length > 0) { - const promises = memberOf.map(async (element: string) => { - const groupInfo: { policy?: string } = await $api.get(`/group?group=${encodeURIComponent(element)}`) - const groupPolicyName: string[] = groupInfo.policy ? groupInfo.policy.split(',') : [] - return groupPolicyName - }) - const results = await Promise.all(promises) - results.forEach(policyNames => { - policyName.push(...policyNames) + const groupPoliciesMap: Record = {} + if (memberOf.length > 0) { + const promises = memberOf.map(async (groupName: string) => { + const groupInfo: { policy?: string } = await $api.get(`/group?group=${encodeURIComponent(groupName)}`) + const groupPolicyNames: string[] = groupInfo.policy ? groupInfo.policy.split(',') : [] + if (groupPolicyNames.length > 0) { + groupPoliciesMap[groupName] = groupPolicyNames + } }) + await Promise.all(promises) } - // Remove duplicates - let uniquePolicyName: string[] = [] - if (policyName.length) { - uniquePolicyName = Array.from(new Set(policyName)) - } + // Collect all unique policy names + const allPolicyNames = new Set() + directPolicyNames.forEach((policyName: string) => allPolicyNames.add(policyName)) + Object.values(groupPoliciesMap).forEach((policyNames: string[]) => { + policyNames.forEach((policyName: string) => allPolicyNames.add(policyName)) + }) let policyStatement: any = [] - // Get all remaining policy documents - if (uniquePolicyName.length) { - const policyPromises = uniquePolicyName.map(async (element: any) => { - const policyInfo = await getPolicy(element) + // Get all policy documents + if (allPolicyNames.size > 0) { + const policyPromises = Array.from(allPolicyNames).map(async (policyName: string) => { + const policyInfo = await getPolicy(policyName) // Format policy let policyRes = JSON.parse(policyInfo.policy) if (policyRes?.Statement) { + // Add origin information to each statement + policyRes.Statement = policyRes.Statement.map((statement: any) => { + // Check if this policy is direct or inherited from groups + const isDirect = directPolicyNames.includes(policyName) + const groupOrigins = Object.entries(groupPoliciesMap) + .filter(([_, policies]) => policies.includes(policyName)) + .map(([groupName]) => groupName) + + return { + ...statement, + Sid: statement.Sid || policyName, // Use policy name as Sid if not provided + Origin: { + type: isDirect && groupOrigins.length > 0 ? 'both' : isDirect ? 'direct' : 'group', + groups: groupOrigins, + policyName: policyName, + }, + } + }) policyStatement.push(...policyRes.Statement) } }) diff --git a/i18n/locales/de-DE.json b/i18n/locales/de-DE.json index af0385c..c5af61b 100644 --- a/i18n/locales/de-DE.json +++ b/i18n/locales/de-DE.json @@ -31,6 +31,7 @@ "Add Tier": "Tier hinzufügen", "Add User": "Benutzer hinzufügen", "Add User Group": "Benutzergruppe hinzufügen", + "Inherited from group": "Vererbt von Gruppe", "Add failed": "Hinzufügen fehlgeschlagen", "Add group members": "Gruppenmitglieder hinzufügen", "Add replication rules to sync objects across buckets.": "Replikationsregeln hinzufügen, um Objekte zwischen Buckets zu synchronisieren.", diff --git a/i18n/locales/en-US.json b/i18n/locales/en-US.json index b224f51..d2672c2 100644 --- a/i18n/locales/en-US.json +++ b/i18n/locales/en-US.json @@ -31,6 +31,7 @@ "Add Tier": "Add Tier", "Add User": "Add User", "Add User Group": "Add User Group", + "Inherited from group": "Inherited from group", "Add failed": "Add failed", "Add group members": "Add group members", "Add replication rules to sync objects across buckets.": "Add replication rules to sync objects across buckets.", diff --git a/i18n/locales/es-ES.json b/i18n/locales/es-ES.json index e9523e0..620ce01 100644 --- a/i18n/locales/es-ES.json +++ b/i18n/locales/es-ES.json @@ -30,7 +30,8 @@ "Add Tag": "Añadir Etiqueta", "Add Tier": "Añadir Nivel", "Add User": "Añadir Usuario", - "Add User Group": "Añadir Grupo de Usuarios", + "Add User Group": "Añadir Grupo de Usuario", + "Inherited from group": "Herencia de Grupo", "Add failed": "Error al añadir", "Add group members": "Añadir miembros del grupo", "Add replication rules to sync objects across buckets.": "Añade reglas de replicación para sincronizar objetos entre buckets.", diff --git a/i18n/locales/fr-FR.json b/i18n/locales/fr-FR.json index 636e8de..d5e8ded 100644 --- a/i18n/locales/fr-FR.json +++ b/i18n/locales/fr-FR.json @@ -31,6 +31,7 @@ "Add Tier": "Ajouter un niveau", "Add User": "Ajouter un utilisateur", "Add User Group": "Ajouter un groupe d'utilisateurs", + "Inherited from group": "Hérité de groupe", "Add failed": "Échec de l'ajout", "Add group members": "Ajouter des membres au groupe", "Add replication rules to sync objects across buckets.": "Ajoutez des règles de réplication pour synchroniser les objets entre les compartiments.", diff --git a/i18n/locales/it-IT.json b/i18n/locales/it-IT.json index e4a0bef..b95261a 100644 --- a/i18n/locales/it-IT.json +++ b/i18n/locales/it-IT.json @@ -31,6 +31,7 @@ "Add Tier": "Aggiungi tier", "Add User": "Aggiungi utente", "Add User Group": "Aggiungi gruppo utenti", + "Inherited from group": "Ereditato da gruppo", "Add failed": "Aggiunta non riuscita", "Add group members": "Aggiungi membri del gruppo", "Add replication rules to sync objects across buckets.": "Aggiungi regole di replica per sincronizzare gli oggetti tra i bucket.", diff --git a/i18n/locales/ja-JP.json b/i18n/locales/ja-JP.json index 9ceab4c..160837c 100644 --- a/i18n/locales/ja-JP.json +++ b/i18n/locales/ja-JP.json @@ -31,6 +31,7 @@ "Add Tier": "ティアを追加", "Add User": "ユーザーを追加", "Add User Group": "ユーザーグループを追加", + "Inherited from group": "グループから継承", "Add failed": "追加に失敗しました", "Add group members": "グループメンバーを追加", "Add replication rules to sync objects across buckets.": "バケット間でオブジェクトを同期するレプリケーションルールを追加します。", diff --git a/i18n/locales/ko-KR.json b/i18n/locales/ko-KR.json index 9fa855d..ea54980 100644 --- a/i18n/locales/ko-KR.json +++ b/i18n/locales/ko-KR.json @@ -31,6 +31,7 @@ "Add Tier": "티어 추가", "Add User": "사용자 추가", "Add User Group": "사용자 그룹 추가", + "Inherited from group": "그룹 상속", "Add failed": "추가 실패", "Add group members": "그룹 멤버 추가", "Add replication rules to sync objects across buckets.": "버킷 간 객체를 동기화하는 복제 규칙을 추가합니다.", diff --git a/i18n/locales/pt-BR.json b/i18n/locales/pt-BR.json index b7ee677..bfd0b44 100644 --- a/i18n/locales/pt-BR.json +++ b/i18n/locales/pt-BR.json @@ -30,7 +30,8 @@ "Add Tag": "Adicionar Tag", "Add Tier": "Adicionar Tier", "Add User": "Adicionar Usuário", - "Add User Group": "Adicionar Grupo de Usuários", + "Add User Group": "Adicionar Grupo de Usuário", + "Inherited from group": "Herança de Grupo", "Add failed": "Falha ao adicionar", "Add group members": "Adicionar membros do grupo", "Add replication rules to sync objects across buckets.": "Adicione regras de replicação para sincronizar objetos entre buckets.", diff --git a/i18n/locales/ru-RU.json b/i18n/locales/ru-RU.json index b0d7445..674351a 100644 --- a/i18n/locales/ru-RU.json +++ b/i18n/locales/ru-RU.json @@ -31,6 +31,7 @@ "Add Tier": "Добавить уровень", "Add User": "Добавить пользователя", "Add User Group": "Добавить группу пользователей", + "Inherited from group": "Наследовано от группы", "Add failed": "Ошибка добавления", "Add group members": "Добавить участников группы", "Add replication rules to sync objects across buckets.": "Добавьте правила репликации для синхронизации объектов между бакетами.", diff --git a/i18n/locales/tr-TR.json b/i18n/locales/tr-TR.json index dcd999b..bfa1ee5 100644 --- a/i18n/locales/tr-TR.json +++ b/i18n/locales/tr-TR.json @@ -31,6 +31,7 @@ "Add Tier": "Katman Ekle", "Add User": "Kullanıcı Ekle", "Add User Group": "Kullanıcı Grubu Ekle", + "Inherited from group": "Kalıtım Grubu", "Add failed": "Ekleme başarısız", "Add group members": "Grup üyeleri ekle", "Add replication rules to sync objects across buckets.": "Add replication rules to sync objects across buckets.", diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json index 9f4dacb..4b45008 100644 --- a/i18n/locales/zh-CN.json +++ b/i18n/locales/zh-CN.json @@ -31,6 +31,7 @@ "Add Tier": "添加存储层", "Add User": "新增用户", "Add User Group": "新增用户组", + "Inherited from group": "继承自分组", "Add failed": "添加失败", "Add group members": "添加组成员", "Add replication rules to sync objects across buckets.": "添加复制规则以同步存储桶之间的对象。", diff --git a/package-lock.json b/package-lock.json index e2b07c1..316a84e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "jszip": "^3.10.1", "lucide-vue-next": "^0.487.0", "motion-v": "^1.5.0", - "node-forge": "^1.3.1", + "node-forge": "^1.3.2", "nuxt": "^4.2.0", "pinia": "^2.3.1", "pnpm": "^10.19.0", @@ -67,6 +67,7 @@ "@types/aws4": "^1.11.6", "@types/lodash": "^4.17.20", "@types/node-forge": "^1.3.12", + "@vitalets/google-translate-api": "^9.2.1", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "@vueuse/core": "^13.5.0", @@ -80,6 +81,7 @@ "prettier": "^3.2.5", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", + "tar": "^7.5.2", "tw-animate-css": "^1.4.0", "typescript": "^5.8.3", "vitest": "^3.2.4", @@ -7372,6 +7374,12 @@ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, + "node_modules/@types/http-errors": { + "version": "1.8.2", + "resolved": "https://registry.npmmirror.com/@types/http-errors/-/http-errors-1.8.2.tgz", + "integrity": "sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==", + "dev": true + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmmirror.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -7837,6 +7845,20 @@ "node": ">=8" } }, + "node_modules/@vitalets/google-translate-api": { + "version": "9.2.1", + "resolved": "https://registry.npmmirror.com/@vitalets/google-translate-api/-/google-translate-api-9.2.1.tgz", + "integrity": "sha512-zlwQWSjXUZhbZQ6qwtIQ7GdYXFQmJ4wYqzcrYJUxtvzQQwUP+uKUb/SRJaBOQuBntjBjzcdcJoLFrpCKUbIkOg==", + "dev": true, + "dependencies": { + "@types/http-errors": "^1.8.2", + "http-errors": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@vitejs/plugin-vue": { "version": "6.0.1", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz", @@ -15077,10 +15099,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmmirror.com/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "license": "(BSD-3-Clause OR GPL-2.0)", + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", "engines": { "node": ">= 6.13.0" } @@ -18819,10 +18840,9 @@ } }, "node_modules/tar": { - "version": "7.5.1", - "resolved": "https://registry.npmmirror.com/tar/-/tar-7.5.1.tgz", - "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", - "license": "ISC", + "version": "7.5.2", + "resolved": "https://registry.npmmirror.com/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0",