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:
seaislee1209 2026-03-28 22:45:04 +08:00
parent 9ffa13f44d
commit 4b8181b96a
5 changed files with 276 additions and 1 deletions

View File

@ -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)

View File

@ -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:

View File

@ -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') },

View File

@ -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

View 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>