feat: unified policy overview page for sub-accounts
- New page: /iam-users/:id/policies shows all policies in one view - Separated into global policies and per-project policies sections - Each section has inline add/remove with disabled duplicates - Backend: new policies/overview/ endpoint returns global + project policies - Replaces old popup-based policy management Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9ffa13f44d
commit
4b8181b96a
@ -23,6 +23,7 @@ urlpatterns = [
|
||||
path('iam-users/<int:pk>/disable/', views.iam_user_disable_view),
|
||||
path('iam-users/<int:pk>/enable/', views.iam_user_enable_view),
|
||||
path('iam-users/<int:pk>/policies/', views.iam_user_policies_view),
|
||||
path('iam-users/<int:pk>/policies/overview/', views.iam_user_policies_overview_view),
|
||||
path('iam-users/<int:pk>/policies/attach/', views.iam_user_attach_policy_view),
|
||||
path('iam-users/<int:pk>/policies/detach/', views.iam_user_detach_policy_view),
|
||||
# IAM user projects (multi-project)
|
||||
|
||||
@ -695,6 +695,81 @@ def iam_user_enable_view(request, pk):
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@api_view(['GET'])
|
||||
def iam_user_policies_overview_view(request, pk):
|
||||
"""查看子账号的完整权限总览(全局 + 各项目)"""
|
||||
try:
|
||||
user = IAMUser.objects.get(pk=pk)
|
||||
except IAMUser.DoesNotExist:
|
||||
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
account, ak, sk = _get_volc_account(user.volc_account_id)
|
||||
if not ak:
|
||||
return Response({'error': 'no_credentials'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
svc = IAMService(ak, sk)
|
||||
|
||||
try:
|
||||
# Get all policies
|
||||
resp = svc.list_attached_user_policies(user.username)
|
||||
all_policies = resp.get("Result", {}).get("AttachedPolicyMetadata", [])
|
||||
|
||||
# Separate global vs project
|
||||
global_policies = []
|
||||
for p in all_policies:
|
||||
scopes = p.get('PolicyScope', [])
|
||||
is_global = not scopes or any(s.get('PolicyScopeType') == 'Global' for s in scopes)
|
||||
if is_global:
|
||||
global_policies.append({
|
||||
'name': p.get('PolicyName', ''),
|
||||
'type': p.get('PolicyType', ''),
|
||||
'description': p.get('Description', ''),
|
||||
})
|
||||
|
||||
# Get project-level policies for each associated project
|
||||
project_policies = []
|
||||
for proj in user.projects.all():
|
||||
try:
|
||||
resp2 = svc.client.call('ListAttachedUserPolicies', {
|
||||
'UserName': user.username,
|
||||
'ProjectName': proj.project_name,
|
||||
})
|
||||
proj_items = []
|
||||
for p in resp2.get('Result', {}).get('AttachedPolicyMetadata', []):
|
||||
scopes = p.get('PolicyScope', [])
|
||||
for s in scopes:
|
||||
if s.get('PolicyScopeType') == 'Project' and s.get('ProjectName') == proj.project_name:
|
||||
proj_items.append({
|
||||
'name': p.get('PolicyName', ''),
|
||||
'type': p.get('PolicyType', ''),
|
||||
'description': p.get('Description', ''),
|
||||
})
|
||||
break
|
||||
project_policies.append({
|
||||
'project_name': proj.project_name,
|
||||
'display_name': proj.display_name,
|
||||
'project_id': proj.id,
|
||||
'policies': proj_items,
|
||||
})
|
||||
except Exception:
|
||||
project_policies.append({
|
||||
'project_name': proj.project_name,
|
||||
'display_name': proj.display_name,
|
||||
'project_id': proj.id,
|
||||
'policies': [],
|
||||
})
|
||||
|
||||
return Response({
|
||||
'username': user.username,
|
||||
'display_name': user.display_name,
|
||||
'global_policies': global_policies,
|
||||
'project_policies': project_policies,
|
||||
})
|
||||
except VolcengineAPIError as e:
|
||||
return Response({'error': 'api_error', 'message': str(e)},
|
||||
status=status.HTTP_502_BAD_GATEWAY)
|
||||
|
||||
|
||||
def iam_user_policies_view(request, pk):
|
||||
"""查看子账号的权限策略"""
|
||||
try:
|
||||
|
||||
@ -21,6 +21,7 @@ const routes = [
|
||||
// Admin routes
|
||||
{ path: 'dashboard', name: 'Dashboard', component: () => import('../views/dashboard/DashboardView.vue') },
|
||||
{ path: 'iam-users', name: 'IAMUsers', component: () => import('../views/iam/IAMUserList.vue') },
|
||||
{ path: 'iam-users/:id/policies', name: 'UserPolicies', component: () => import('../views/iam/UserPoliciesView.vue'), props: true },
|
||||
{ path: 'billing', name: 'Billing', component: () => import('../views/billing/BillingView.vue') },
|
||||
{ path: 'alerts', name: 'Alerts', component: () => import('../views/alerts/AlertList.vue') },
|
||||
{ path: 'ark-keys', name: 'ArkKeys', component: () => import('../views/ark/ArkKeysView.vue') },
|
||||
|
||||
@ -82,7 +82,7 @@
|
||||
<el-dropdown-item @click="toggleVolcLogin(row)">
|
||||
{{ row.volc_login_allowed ? '🔓 关闭火山登录' : '🔒 开启火山登录' }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="openPolicies(row)">全局权限策略</el-dropdown-item>
|
||||
<el-dropdown-item @click="$router.push(`/iam-users/${row.id}/policies`)">权限管理</el-dropdown-item>
|
||||
<el-dropdown-item @click="openQuotaHistory(row)">划拨记录</el-dropdown-item>
|
||||
<el-dropdown-item @click="openSetLogin(row)">登录密码</el-dropdown-item>
|
||||
<el-dropdown-item v-if="row.status === 'active'" divided
|
||||
|
||||
198
frontend/src/views/iam/UserPoliciesView.vue
Normal file
198
frontend/src/views/iam/UserPoliciesView.vue
Normal file
@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<div style="max-width: 1200px; margin: 0 auto;">
|
||||
<div class="page-header">
|
||||
<h2>{{ overview.display_name || overview.username }} 权限总览</h2>
|
||||
<el-button @click="$router.push('/iam-users')">返回子账号列表</el-button>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading">
|
||||
<!-- Global Policies -->
|
||||
<el-card style="margin-bottom: 20px;">
|
||||
<template #header>
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<span style="font-weight:600; font-size:16px;">全局策略</span>
|
||||
<div style="display:flex; gap:8px;">
|
||||
<el-select v-model="globalPolicyToAdd" placeholder="添加全局策略" filterable size="small" style="width:280px;">
|
||||
<el-option v-for="opt in policyOptions" :key="opt.value"
|
||||
:value="opt.value" :label="opt.label"
|
||||
:disabled="overview.global_policies?.some(p => p.name === opt.value)" />
|
||||
</el-select>
|
||||
<el-button type="primary" size="small" @click="attachGlobal" :disabled="!globalPolicyToAdd">添加</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-alert type="info" :closable="false" style="margin-bottom:12px;">
|
||||
全局策略对所有项目生效。一般只放 Deny 策略(项目隔离),业务权限请加到项目级。
|
||||
</el-alert>
|
||||
<el-table :data="overview.global_policies || []" stripe empty-text="无全局策略">
|
||||
<el-table-column prop="name" label="策略名" min-width="220" />
|
||||
<el-table-column label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type === 'Custom' ? 'warning' : 'info'" size="small">
|
||||
{{ row.type === 'Custom' ? '自定义' : '系统' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="说明" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="danger" text @click="detachGlobal(row)">移除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- Project-level Policies -->
|
||||
<el-card v-for="proj in (overview.project_policies || [])" :key="proj.project_name" style="margin-bottom: 16px;">
|
||||
<template #header>
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<span>
|
||||
<el-tag type="success" size="small" style="margin-right:8px;">项目</el-tag>
|
||||
<span style="font-weight:600; font-size:15px;">{{ proj.project_name }}</span>
|
||||
<span v-if="proj.display_name" style="color:#999; margin-left:8px;">{{ proj.display_name }}</span>
|
||||
</span>
|
||||
<div style="display:flex; gap:8px;">
|
||||
<el-select v-model="projectPolicyToAdd[proj.project_name]" placeholder="添加策略" filterable size="small" style="width:280px;">
|
||||
<el-option v-for="opt in policyOptions" :key="opt.value"
|
||||
:value="opt.value" :label="opt.label"
|
||||
:disabled="proj.policies?.some(p => p.name === opt.value)" />
|
||||
</el-select>
|
||||
<el-button type="primary" size="small" @click="attachProject(proj)" :disabled="!projectPolicyToAdd[proj.project_name]">添加</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-alert type="info" :closable="false" style="margin-bottom:12px;">
|
||||
以下权限仅在 <strong>{{ proj.project_name }}</strong> 项目范围内生效。
|
||||
</el-alert>
|
||||
<el-table :data="proj.policies || []" stripe empty-text="无项目级策略">
|
||||
<el-table-column prop="name" label="策略名" min-width="220" />
|
||||
<el-table-column label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type === 'Custom' ? 'warning' : 'info'" size="small">
|
||||
{{ row.type === 'Custom' ? '自定义' : '系统' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="说明" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="danger" text @click="detachProject(proj, row)">移除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-empty v-if="!(overview.project_policies || []).length && !loading" description="暂无关联项目,请先在子账号管理中添加项目" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import api from '../../api'
|
||||
|
||||
const route = useRoute()
|
||||
const userId = route.params.id
|
||||
|
||||
const loading = ref(false)
|
||||
const overview = ref({})
|
||||
const globalPolicyToAdd = ref('')
|
||||
const projectPolicyToAdd = reactive({})
|
||||
|
||||
const policyOptions = [
|
||||
{ value: 'ArkFullAccess', label: 'ArkFullAccess(方舟/Seedance 完整权限)' },
|
||||
{ value: 'ArkExperienceAccess', label: 'ArkExperienceAccess(方舟体验权限)' },
|
||||
{ value: 'ArkReadOnlyAccess', label: 'ArkReadOnlyAccess(方舟只读)' },
|
||||
{ value: 'TOSFullAccess', label: 'TOSFullAccess(对象存储完整权限)' },
|
||||
{ value: 'TOSReadOnlyAccess', label: 'TOSReadOnlyAccess(对象存储只读)' },
|
||||
{ value: 'AccessKeySelfManageAccess', label: 'AccessKeySelfManageAccess(自管理密钥)' },
|
||||
]
|
||||
|
||||
async function loadOverview() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await api.get(`/api/v1/iam-users/${userId}/policies/overview/`)
|
||||
overview.value = data
|
||||
} catch (e) {
|
||||
ElMessage.error(e.response?.data?.message || '加载权限信息失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function attachGlobal() {
|
||||
if (!globalPolicyToAdd.value) return
|
||||
try {
|
||||
await api.post(`/api/v1/iam-users/${userId}/policies/attach/`, {
|
||||
policy_name: globalPolicyToAdd.value,
|
||||
policy_type: 'System',
|
||||
})
|
||||
ElMessage.success(`已添加全局策略 ${globalPolicyToAdd.value}`)
|
||||
globalPolicyToAdd.value = ''
|
||||
await loadOverview()
|
||||
} catch (e) {
|
||||
ElMessage.error(e.response?.data?.message || '添加失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function detachGlobal(row) {
|
||||
await ElMessageBox.confirm(`确定移除全局策略 "${row.name}" 吗?`, '确认移除', { type: 'warning' })
|
||||
try {
|
||||
await api.post(`/api/v1/iam-users/${userId}/policies/detach/`, {
|
||||
policy_name: row.name,
|
||||
policy_type: row.type,
|
||||
})
|
||||
ElMessage.success(`已移除 ${row.name}`)
|
||||
await loadOverview()
|
||||
} catch (e) {
|
||||
ElMessage.error(e.response?.data?.message || '移除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function attachProject(proj) {
|
||||
const policyName = projectPolicyToAdd[proj.project_name]
|
||||
if (!policyName) return
|
||||
try {
|
||||
// Use the project policies update endpoint
|
||||
const newPolicies = [...(proj.policies || []).map(p => p.name), policyName]
|
||||
await api.put(`/api/v1/iam-users/${userId}/projects/${proj.project_id}/policies/`, {
|
||||
policies: newPolicies,
|
||||
})
|
||||
ElMessage.success(`已添加 ${policyName} 到 ${proj.project_name}`)
|
||||
projectPolicyToAdd[proj.project_name] = ''
|
||||
await loadOverview()
|
||||
} catch (e) {
|
||||
ElMessage.error(e.response?.data?.message || '添加失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function detachProject(proj, row) {
|
||||
await ElMessageBox.confirm(
|
||||
`确定从 ${proj.project_name} 移除策略 "${row.name}" 吗?`,
|
||||
'确认移除', { type: 'warning' }
|
||||
)
|
||||
try {
|
||||
const newPolicies = (proj.policies || []).filter(p => p.name !== row.name).map(p => p.name)
|
||||
await api.put(`/api/v1/iam-users/${userId}/projects/${proj.project_id}/policies/`, {
|
||||
policies: newPolicies,
|
||||
})
|
||||
ElMessage.success(`已从 ${proj.project_name} 移除 ${row.name}`)
|
||||
await loadOverview()
|
||||
} catch (e) {
|
||||
ElMessage.error(e.response?.data?.message || '移除失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadOverview)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user