Skip to content

Commit c51cd09

Browse files
committed
feat: Associate user policies with grouping policies. #30
1 parent 19a55e7 commit c51cd09

16 files changed

Lines changed: 241 additions & 37 deletions

File tree

‎components/user/groups.vue‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,11 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
8383
import type { ColumnDef } from '@tanstack/vue-table'
8484
import { computed, h, onMounted, ref, watch } from 'vue'
8585
import { useI18n } from 'vue-i18n'
86+
import { usePolicies } from '@/composables/usePolicies'
8687
8788
const { t } = useI18n()
8889
const { listGroup, updateGroupMembers } = useGroups()
90+
const { listPolicies, getPolicyByUserName, setUserOrGroupPolicy } = usePolicies()
8991
const message = useMessage()
9092
9193
const props = defineProps<{
@@ -215,6 +217,26 @@ const changeMembers = async () => {
215217
),
216218
])
217219
220+
// After updating groups, update user policies to include inherited policies from groups
221+
const updatedUserPolicies = await getPolicyByUserName(props.user.accessKey)
222+
// Get all policy names from the combined policy document
223+
const policyNames =
224+
updatedUserPolicies?.Statement?.map((statement: { Sid?: string; Origin?: { policyName?: string } }) => {
225+
// This is a simplified approach - in reality, we need to extract policy names
226+
// For now, we'll assume the backend handles policy resolution correctly
227+
return statement.Origin?.policyName || statement.Sid || ''
228+
}).filter(Boolean) || []
229+
230+
// Remove duplicate policy names
231+
const uniquePolicyNames = Array.from(new Set(policyNames))
232+
233+
// Update user policies to include both direct and inherited policies
234+
await setUserOrGroupPolicy({
235+
policyName: uniquePolicyNames,
236+
userOrGroup: props.user.accessKey,
237+
isGroup: false,
238+
})
239+
218240
message.success(t('Update Success'))
219241
editStatus.value = false
220242
groupSelectorOpen.value = false

‎components/user/new-form.vue‎

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,22 @@
105105
:key="option.value"
106106
:value="option.label"
107107
@select="() => togglePolicy(option.value)"
108+
:class="{
109+
'opacity-70 cursor-not-allowed': editForm.groupInheritedPolicies.includes(option.value),
110+
}"
111+
:disabled="editForm.groupInheritedPolicies.includes(option.value)"
108112
>
109113
<Icon
110114
name="ri:check-line"
111115
class="mr-2 size-4"
112116
:class="editForm.policies.includes(option.value) ? 'opacity-100' : 'opacity-0'"
113117
/>
114118
<span>{{ option.label }}</span>
119+
<span
120+
v-if="editForm.groupInheritedPolicies.includes(option.value)"
121+
class="ml-2 text-xs text-muted-foreground opacity-70"
122+
>({{ t('Inherited from group') }})</span
123+
>
115124
</CommandItem>
116125
</CommandGroup>
117126
</CommandList>
@@ -147,13 +156,15 @@ import { Field, FieldContent, FieldDescription, FieldLabel } from '@/components/
147156
import { Input } from '@/components/ui/input'
148157
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
149158
import { useI18n } from 'vue-i18n'
159+
import { useNuxtApp } from '#app'
150160
import Modal from '~/components/modal.vue'
151161
152162
const { t } = useI18n()
153163
const message = useMessage()
154164
const { createUser } = useUsers()
155165
const { listPolicies, setUserOrGroupPolicy } = usePolicies()
156-
const { listGroup, updateGroupMembers } = useGroups()
166+
const { listGroup, updateGroupMembers, getGroup } = useGroups()
167+
const { $api } = useNuxtApp()
157168
158169
const emit = defineEmits<{
159170
(e: 'search'): void
@@ -167,6 +178,8 @@ const editForm = reactive({
167178
secretKey: '',
168179
groups: [] as string[],
169180
policies: [] as string[],
181+
// 存储从分组继承的策略,这些策略不可取消
182+
groupInheritedPolicies: [] as string[],
170183
})
171184
172185
const errors = reactive({
@@ -200,6 +213,7 @@ const resetForm = () => {
200213
editForm.secretKey = ''
201214
editForm.groups = []
202215
editForm.policies = []
216+
editForm.groupInheritedPolicies = []
203217
errors.accessKey = ''
204218
errors.secretKey = ''
205219
groupSelectorOpen.value = false
@@ -284,6 +298,43 @@ const submitForm = async () => {
284298
}
285299
}
286300
301+
// 获取分组的策略
302+
const getGroupPolicies = async (groupName: string) => {
303+
try {
304+
const groupInfo: { policy?: string } = await getGroup(groupName)
305+
return groupInfo.policy ? groupInfo.policy.split(',') : []
306+
} catch (error) {
307+
console.error('获取分组策略失败:', error)
308+
return []
309+
}
310+
}
311+
312+
// 更新分组继承的策略
313+
const updateGroupInheritedPolicies = async () => {
314+
// 清空当前的分组继承策略
315+
editForm.groupInheritedPolicies = []
316+
317+
// 获取所有选中分组的策略
318+
if (editForm.groups.length > 0) {
319+
const promises = editForm.groups.map(group => getGroupPolicies(group))
320+
const results = await Promise.all(promises)
321+
322+
// 合并所有分组的策略并去重
323+
const inheritedPolicies = Array.from(new Set(results.flat()))
324+
editForm.groupInheritedPolicies = inheritedPolicies
325+
}
326+
327+
// 更新策略列表,确保分组继承的策略被选中且不可取消
328+
updateSelectedPolicies()
329+
}
330+
331+
// 更新选中的策略,确保分组继承的策略被选中
332+
const updateSelectedPolicies = () => {
333+
// 合并用户手动选择的策略和分组继承的策略
334+
const allPolicies = Array.from(new Set([...editForm.policies, ...editForm.groupInheritedPolicies]))
335+
editForm.policies = allPolicies
336+
}
337+
287338
const getPoliciesList = async () => {
288339
const res = await listPolicies()
289340
policiesList.value = Object.keys(res ?? {})
@@ -302,15 +353,25 @@ const getGroupsList = async () => {
302353
}))
303354
}
304355
305-
const toggleGroup = (value: string) => {
356+
const toggleGroup = async (value: string) => {
306357
if (editForm.groups.includes(value)) {
358+
// 移除分组
307359
editForm.groups = editForm.groups.filter(item => item !== value)
308360
} else {
361+
// 添加分组
309362
editForm.groups = [...editForm.groups, value]
310363
}
364+
365+
// 更新分组继承的策略
366+
await updateGroupInheritedPolicies()
311367
}
312368
313369
const togglePolicy = (value: string) => {
370+
// 如果是分组继承的策略,不能取消选中
371+
if (editForm.groupInheritedPolicies.includes(value)) {
372+
return
373+
}
374+
314375
if (editForm.policies.includes(value)) {
315376
editForm.policies = editForm.policies.filter(item => item !== value)
316377
} else {

‎components/user/policies.vue‎

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,21 @@
3939
:key="option.value"
4040
:value="option.label"
4141
@select="() => togglePolicy(option.value)"
42+
:disabled="inheritedPolicies.includes(option.value) && selectedPolicies.includes(option.value)"
43+
class="group"
4244
>
4345
<Icon
4446
name="ri:check-line"
4547
class="mr-2 size-4"
4648
:class="selectedPolicies.includes(option.value) ? 'opacity-100' : 'opacity-0'"
4749
/>
4850
<span>{{ option.label }}</span>
51+
<span
52+
v-if="inheritedPolicies.includes(option.value)"
53+
class="ml-2 text-xs text-muted-foreground opacity-70 transition-opacity"
54+
>
55+
{{ t('(Inherited from group)') }}
56+
</span>
4957
</CommandItem>
5058
</CommandGroup>
5159
</CommandList>
@@ -83,11 +91,39 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
8391
import type { ColumnDef } from '@tanstack/vue-table'
8492
import { computed, h, onMounted, ref, watch } from 'vue'
8593
import { useI18n } from 'vue-i18n'
94+
import { useGroups } from '@/composables/useGroups'
8695
87-
const { listPolicies, setUserOrGroupPolicy } = usePolicies()
96+
const { listPolicies, setUserOrGroupPolicy, getPolicyByUserName } = usePolicies()
97+
const { listGroup } = useGroups()
8898
const { t } = useI18n()
8999
const message = useMessage()
90100
101+
// 存储用户继承的策略
102+
const inheritedPolicies = ref<string[]>([])
103+
104+
// 获取用户继承的策略
105+
const loadInheritedPolicies = async () => {
106+
if (!props.user?.accessKey) return
107+
try {
108+
const userPolicies = await getPolicyByUserName(props.user.accessKey)
109+
if (userPolicies?.Statement) {
110+
// 提取来自组的继承策略
111+
const inheritedPolicyNames = new Set<string>()
112+
userPolicies.Statement.forEach((statement: any) => {
113+
if (statement.Origin?.type === 'group' || statement.Origin?.type === 'both') {
114+
// 如果策略来自组或同时来自组和直接分配,将其添加到继承策略列表
115+
if (statement.Origin?.policyName) {
116+
inheritedPolicyNames.add(statement.Origin.policyName)
117+
}
118+
}
119+
})
120+
inheritedPolicies.value = Array.from(inheritedPolicyNames)
121+
}
122+
} catch (error) {
123+
console.error('Failed to load inherited policies:', error)
124+
}
125+
}
126+
91127
const props = defineProps<{
92128
user: {
93129
accessKey: string
@@ -153,9 +189,10 @@ const loadPolicies = async () => {
153189
}
154190
}
155191
156-
onMounted(() => {
157-
loadPolicies()
192+
onMounted(async () => {
193+
await loadPolicies()
158194
selectedPolicies.value = [...currentPolicies.value]
195+
await loadInheritedPolicies()
159196
})
160197
161198
watch(
@@ -168,6 +205,12 @@ watch(
168205
{ immediate: true }
169206
)
170207
208+
watch(editStatus, async val => {
209+
if (val) {
210+
await loadInheritedPolicies()
211+
}
212+
})
213+
171214
const startEditing = () => {
172215
selectedPolicies.value = [...currentPolicies.value]
173216
policySelectorOpen.value = false
@@ -182,9 +225,17 @@ const cancelEditing = () => {
182225
183226
const togglePolicy = (value: string) => {
184227
if (selectedPolicies.value.includes(value)) {
228+
// 如果是继承的策略,禁止删除
229+
if (inheritedPolicies.value.includes(value)) {
230+
// 不再显示弹出消息,而是通过UI禁用删除功能
231+
return
232+
}
185233
selectedPolicies.value = selectedPolicies.value.filter(item => item !== value)
186234
} else {
187-
selectedPolicies.value = [...selectedPolicies.value, value]
235+
// 确保添加的策略不重复
236+
if (!selectedPolicies.value.includes(value)) {
237+
selectedPolicies.value = [...selectedPolicies.value, value]
238+
}
188239
}
189240
}
190241

‎composables/usePolicies.ts‎

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,22 @@ export const usePolicies = () => {
6060
* @returns Result
6161
*/
6262
const setPolicyMultiple = async (data: any) => {
63+
// Add basic deduplication for policy names if applicable
64+
// This handles common batch policy assignment patterns
65+
if (data.policyName && Array.isArray(data.policyName)) {
66+
data.policyName = Array.from(new Set(data.policyName))
67+
}
68+
// Handle potential different structure where policies might be in a 'policies' array
69+
if (data.policies && Array.isArray(data.policies)) {
70+
// If policies array contains objects with policyName, deduplicate based on policyName
71+
const policyNameMap = new Map()
72+
data.policies.forEach((policy: any) => {
73+
if (policy.policyName && !policyNameMap.has(policy.policyName)) {
74+
policyNameMap.set(policy.policyName, policy)
75+
}
76+
})
77+
data.policies = Array.from(policyNameMap.values())
78+
}
6379
return await $api.put(`/set-policy-multi`, data)
6480
}
6581

@@ -69,6 +85,10 @@ export const usePolicies = () => {
6985
* @returns Result
7086
*/
7187
const setUserOrGroupPolicy = async (data: any) => {
88+
// Ensure policy names are unique before sending to API
89+
if (data.policyName && Array.isArray(data.policyName)) {
90+
data.policyName = Array.from(new Set(data.policyName))
91+
}
7292
return await $api.put(`/set-user-or-group-policy`, {}, { params: data })
7393
}
7494

@@ -80,37 +100,56 @@ export const usePolicies = () => {
80100
const getPolicyByUserName = async (userName: string) => {
81101
// Get user policy groups
82102
const userInfo = await $api.get(`/user-info?accessKey=${userName}`)
83-
const policyName = userInfo?.policyName?.split(',') || []
103+
const directPolicyNames = userInfo?.policyName?.split(',') || []
84104

85105
// Get user's group memberships
86-
const memberOf = userInfo?.memberOf
106+
const memberOf = userInfo?.memberOf || []
87107
// Get group policies
88-
if (memberOf && memberOf.length > 0) {
89-
const promises = memberOf.map(async (element: string) => {
90-
const groupInfo: { policy?: string } = await $api.get(`/group?group=${encodeURIComponent(element)}`)
91-
const groupPolicyName: string[] = groupInfo.policy ? groupInfo.policy.split(',') : []
92-
return groupPolicyName
93-
})
94-
const results = await Promise.all(promises)
95-
results.forEach(policyNames => {
96-
policyName.push(...policyNames)
108+
const groupPoliciesMap: Record<string, string[]> = {}
109+
if (memberOf.length > 0) {
110+
const promises = memberOf.map(async (groupName: string) => {
111+
const groupInfo: { policy?: string } = await $api.get(`/group?group=${encodeURIComponent(groupName)}`)
112+
const groupPolicyNames: string[] = groupInfo.policy ? groupInfo.policy.split(',') : []
113+
if (groupPolicyNames.length > 0) {
114+
groupPoliciesMap[groupName] = groupPolicyNames
115+
}
97116
})
117+
await Promise.all(promises)
98118
}
99119

100-
// Remove duplicates
101-
let uniquePolicyName: string[] = []
102-
if (policyName.length) {
103-
uniquePolicyName = Array.from(new Set(policyName))
104-
}
120+
// Collect all unique policy names
121+
const allPolicyNames = new Set<string>()
122+
directPolicyNames.forEach((policyName: string) => allPolicyNames.add(policyName))
123+
Object.values(groupPoliciesMap).forEach((policyNames: string[]) => {
124+
policyNames.forEach((policyName: string) => allPolicyNames.add(policyName))
125+
})
105126

106127
let policyStatement: any = []
107-
// Get all remaining policy documents
108-
if (uniquePolicyName.length) {
109-
const policyPromises = uniquePolicyName.map(async (element: any) => {
110-
const policyInfo = await getPolicy(element)
128+
// Get all policy documents
129+
if (allPolicyNames.size > 0) {
130+
const policyPromises = Array.from(allPolicyNames).map(async (policyName: string) => {
131+
const policyInfo = await getPolicy(policyName)
111132
// Format policy
112133
let policyRes = JSON.parse(policyInfo.policy)
113134
if (policyRes?.Statement) {
135+
// Add origin information to each statement
136+
policyRes.Statement = policyRes.Statement.map((statement: any) => {
137+
// Check if this policy is direct or inherited from groups
138+
const isDirect = directPolicyNames.includes(policyName)
139+
const groupOrigins = Object.entries(groupPoliciesMap)
140+
.filter(([_, policies]) => policies.includes(policyName))
141+
.map(([groupName]) => groupName)
142+
143+
return {
144+
...statement,
145+
Sid: statement.Sid || policyName, // Use policy name as Sid if not provided
146+
Origin: {
147+
type: isDirect && groupOrigins.length > 0 ? 'both' : isDirect ? 'direct' : 'group',
148+
groups: groupOrigins,
149+
policyName: policyName,
150+
},
151+
}
152+
})
114153
policyStatement.push(...policyRes.Statement)
115154
}
116155
})

‎i18n/locales/de-DE.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"Add Tier": "Tier hinzufügen",
3232
"Add User": "Benutzer hinzufügen",
3333
"Add User Group": "Benutzergruppe hinzufügen",
34+
"Inherited from group": "Vererbt von Gruppe",
3435
"Add failed": "Hinzufügen fehlgeschlagen",
3536
"Add group members": "Gruppenmitglieder hinzufügen",
3637
"Add replication rules to sync objects across buckets.": "Replikationsregeln hinzufügen, um Objekte zwischen Buckets zu synchronisieren.",

0 commit comments

Comments
 (0)