feat: Associate user policies with grouping policies. #30

This commit is contained in:
马登山
2025-12-08 13:05:06 +08:00
parent 19a55e7fe6
commit c51cd09afc
16 changed files with 241 additions and 37 deletions
+22
View File
@@ -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
+63 -2
View File
@@ -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)"
>
<Icon
name="ri:check-line"
@@ -112,6 +116,11 @@
:class="editForm.policies.includes(option.value) ? 'opacity-100' : 'opacity-0'"
/>
<span>{{ option.label }}</span>
<span
v-if="editForm.groupInheritedPolicies.includes(option.value)"
class="ml-2 text-xs text-muted-foreground opacity-70"
>({{ t('Inherited from group') }})</span
>
</CommandItem>
</CommandGroup>
</CommandList>
@@ -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 {
+55 -4
View File
@@ -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"
>
<Icon
name="ri:check-line"
@@ -46,6 +48,12 @@
:class="selectedPolicies.includes(option.value) ? 'opacity-100' : 'opacity-0'"
/>
<span>{{ option.label }}</span>
<span
v-if="inheritedPolicies.includes(option.value)"
class="ml-2 text-xs text-muted-foreground opacity-70 transition-opacity"
>
{{ t('(Inherited from group)') }}
</span>
</CommandItem>
</CommandGroup>
</CommandList>
@@ -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<string[]>([])
// 获取用户继承的策略
const loadInheritedPolicies = async () => {
if (!props.user?.accessKey) return
try {
const userPolicies = await getPolicyByUserName(props.user.accessKey)
if (userPolicies?.Statement) {
// 提取来自组的继承策略
const inheritedPolicyNames = new Set<string>()
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]
}
}
}
+59 -20
View File
@@ -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<string, string[]> = {}
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<string>()
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)
}
})
+1
View File
@@ -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.",
+1
View File
@@ -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.",
+2 -1
View File
@@ -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.",
+1
View File
@@ -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.",
+1
View File
@@ -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.",
+1
View File
@@ -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.": "バケット間でオブジェクトを同期するレプリケーションルールを追加します。",
+1
View File
@@ -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.": "버킷 간 객체를 동기화하는 복제 규칙을 추가합니다.",
+2 -1
View File
@@ -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.",
+1
View File
@@ -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.": "Добавьте правила репликации для синхронизации объектов между бакетами.",
+1
View File
@@ -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.",
+1
View File
@@ -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.": "添加复制规则以同步存储桶之间的对象。",
+29 -9
View File
@@ -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",