feat: add project-level policy management (add/remove per project)
- Add "授权" button on each linked project row - New dialog to select/deselect policies per project - Backend does incremental diff: only attach new, detach removed - Handle PolicyAttachConflict gracefully Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6dd3ac5c0d
commit
c58fe56d89
@ -26,6 +26,7 @@ urlpatterns = [
|
|||||||
path('iam-users/<int:pk>/projects/', views.iam_user_project_list_view),
|
path('iam-users/<int:pk>/projects/', views.iam_user_project_list_view),
|
||||||
path('iam-users/<int:pk>/projects/add/', views.iam_user_project_add_view),
|
path('iam-users/<int:pk>/projects/add/', views.iam_user_project_add_view),
|
||||||
path('iam-users/<int:pk>/projects/<int:pid>/', views.iam_user_project_update_view),
|
path('iam-users/<int:pk>/projects/<int:pid>/', views.iam_user_project_update_view),
|
||||||
|
path('iam-users/<int:pk>/projects/<int:pid>/policies/', views.iam_user_project_policies_view),
|
||||||
path('iam-users/<int:pk>/projects/<int:pid>/delete/', views.iam_user_project_delete_view),
|
path('iam-users/<int:pk>/projects/<int:pid>/delete/', views.iam_user_project_delete_view),
|
||||||
path('iam-users/<int:pk>/projects/toggle-all/', views.iam_user_project_toggle_all_view),
|
path('iam-users/<int:pk>/projects/toggle-all/', views.iam_user_project_toggle_all_view),
|
||||||
|
|
||||||
|
|||||||
@ -610,10 +610,6 @@ def iam_user_project_add_view(request, pk):
|
|||||||
d['project_name'])
|
d['project_name'])
|
||||||
attached.append(policy_name)
|
attached.append(policy_name)
|
||||||
except VolcengineAPIError as e:
|
except VolcengineAPIError as e:
|
||||||
if 'PolicyAttachConflict' in str(e):
|
|
||||||
# 全局已有此策略,项目级无需重复附加,视为成功
|
|
||||||
attached.append(policy_name)
|
|
||||||
else:
|
|
||||||
auth_errors.append(f"{policy_name}: {e}")
|
auth_errors.append(f"{policy_name}: {e}")
|
||||||
|
|
||||||
obj.attached_policies = attached
|
obj.attached_policies = attached
|
||||||
@ -652,6 +648,66 @@ def iam_user_project_update_view(request, pk, pid):
|
|||||||
return Response(IAMUserProjectSerializer(project).data)
|
return Response(IAMUserProjectSerializer(project).data)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['PUT'])
|
||||||
|
def iam_user_project_policies_view(request, pk, pid):
|
||||||
|
"""更新项目级授权策略(增量对比:移除旧的、添加新的)"""
|
||||||
|
try:
|
||||||
|
project = IAMUserProject.objects.get(pk=pid, iam_user_id=pk)
|
||||||
|
user = project.iam_user
|
||||||
|
except IAMUserProject.DoesNotExist:
|
||||||
|
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
new_policies = request.data.get('policies', [])
|
||||||
|
old_policies = project.attached_policies or []
|
||||||
|
|
||||||
|
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)
|
||||||
|
attached = []
|
||||||
|
detached = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# Remove policies that were removed
|
||||||
|
to_remove = [p for p in old_policies if p not in new_policies]
|
||||||
|
for policy_name in to_remove:
|
||||||
|
try:
|
||||||
|
svc.detach_policy_in_project(user.username, policy_name, project.project_name)
|
||||||
|
detached.append(policy_name)
|
||||||
|
except VolcengineAPIError as e:
|
||||||
|
errors.append(f"移除 {policy_name}: {e}")
|
||||||
|
|
||||||
|
# Add policies that are new
|
||||||
|
to_add = [p for p in new_policies if p not in old_policies]
|
||||||
|
for policy_name in to_add:
|
||||||
|
try:
|
||||||
|
svc.attach_policy_in_project(user.username, policy_name, project.project_name)
|
||||||
|
attached.append(policy_name)
|
||||||
|
except VolcengineAPIError as e:
|
||||||
|
if 'PolicyAttachConflict' in str(e):
|
||||||
|
attached.append(policy_name)
|
||||||
|
else:
|
||||||
|
errors.append(f"添加 {policy_name}: {e}")
|
||||||
|
|
||||||
|
project.attached_policies = new_policies
|
||||||
|
project.save(update_fields=['attached_policies'])
|
||||||
|
|
||||||
|
AlertRecord.objects.create(
|
||||||
|
iam_user=user,
|
||||||
|
alert_type=AlertRecord.AlertType.MANUAL,
|
||||||
|
title=f"更新项目 {project.project_name} 授权策略",
|
||||||
|
content=f"操作人: {request.user.username},添加: {attached},移除: {detached}"
|
||||||
|
+ (f",失败: {errors}" if errors else ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = {'message': f'已更新,添加 {len(attached)} 个、移除 {len(detached)} 个策略',
|
||||||
|
'project': IAMUserProjectSerializer(project).data}
|
||||||
|
if errors:
|
||||||
|
result['warnings'] = errors
|
||||||
|
return Response(result)
|
||||||
|
|
||||||
|
|
||||||
@api_view(['DELETE'])
|
@api_view(['DELETE'])
|
||||||
def iam_user_project_delete_view(request, pk, pid):
|
def iam_user_project_delete_view(request, pk, pid):
|
||||||
"""移除关联项目:回收权限 + 移出监测"""
|
"""移除关联项目:回收权限 + 移出监测"""
|
||||||
|
|||||||
@ -204,14 +204,37 @@
|
|||||||
<el-switch :model-value="row.monitor_enabled" @change="val => handleToggleProject(row, val)" />
|
<el-switch :model-value="row.monitor_enabled" @change="val => handleToggleProject(row, val)" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="80" align="center">
|
<el-table-column label="操作" width="140" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" type="primary" text @click="openProjectPolicies(row)">授权</el-button>
|
||||||
<el-button size="small" type="danger" text @click="handleRemoveProject(row)">移除</el-button>
|
<el-button size="small" type="danger" text @click="handleRemoveProject(row)">移除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- Project Policies Dialog -->
|
||||||
|
<el-dialog v-model="projectPolicyVisible"
|
||||||
|
:title="`${projectPolicyProject?.project_name} 项目授权`"
|
||||||
|
width="90%" style="max-width: 550px;">
|
||||||
|
<p style="margin-bottom:12px; color:#606266; font-size:13px;">
|
||||||
|
子账号 <strong>{{ projectsUser?.username }}</strong> 在此项目下的权限:
|
||||||
|
</p>
|
||||||
|
<el-checkbox-group v-model="projectPolicySelected">
|
||||||
|
<div style="display:flex; flex-direction:column; gap:8px;">
|
||||||
|
<el-checkbox label="ArkFullAccess">方舟/Seedance 完整权限</el-checkbox>
|
||||||
|
<el-checkbox label="ArkReadOnlyAccess">方舟只读</el-checkbox>
|
||||||
|
<el-checkbox label="TOSFullAccess">对象存储完整权限</el-checkbox>
|
||||||
|
<el-checkbox label="TOSReadOnlyAccess">对象存储只读</el-checkbox>
|
||||||
|
<el-checkbox label="AccessKeySelfManageAccess">自管理密钥</el-checkbox>
|
||||||
|
</div>
|
||||||
|
</el-checkbox-group>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="projectPolicyVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSaveProjectPolicies" :loading="projectPolicySaving">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- Quota History Dialog -->
|
<!-- Quota History Dialog -->
|
||||||
<el-dialog v-model="historyVisible" :title="`${historyUser?.username} 额度划拨记录`" width="90%" style="max-width: 800px;">
|
<el-dialog v-model="historyVisible" :title="`${historyUser?.username} 额度划拨记录`" width="90%" style="max-width: 800px;">
|
||||||
<el-table :data="quotaHistory" stripe v-loading="historyLoading" empty-text="暂无划拨记录">
|
<el-table :data="quotaHistory" stripe v-loading="historyLoading" empty-text="暂无划拨记录">
|
||||||
@ -538,6 +561,35 @@ async function handleToggleProject(row, val) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Project Policies ===
|
||||||
|
const projectPolicyVisible = ref(false)
|
||||||
|
const projectPolicyProject = ref(null)
|
||||||
|
const projectPolicySelected = ref([])
|
||||||
|
const projectPolicySaving = ref(false)
|
||||||
|
|
||||||
|
function openProjectPolicies(row) {
|
||||||
|
projectPolicyProject.value = row
|
||||||
|
projectPolicySelected.value = [...(row.attached_policies || [])]
|
||||||
|
projectPolicyVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveProjectPolicies() {
|
||||||
|
projectPolicySaving.value = true
|
||||||
|
try {
|
||||||
|
const { data } = await api.put(
|
||||||
|
`/api/v1/iam-users/${projectsUser.value.id}/projects/${projectPolicyProject.value.id}/policies/`,
|
||||||
|
{ policies: projectPolicySelected.value }
|
||||||
|
)
|
||||||
|
ElMessage.success(data.message || '已更新')
|
||||||
|
projectPolicyVisible.value = false
|
||||||
|
await loadUserProjects(projectsUser.value.id)
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.response?.data?.message || '更新失败')
|
||||||
|
} finally {
|
||||||
|
projectPolicySaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleRemoveProject(row) {
|
async function handleRemoveProject(row) {
|
||||||
await ElMessageBox.confirm(`确定移除项目 "${row.project_name}" 吗?`, '确认', { type: 'warning' })
|
await ElMessageBox.confirm(`确定移除项目 "${row.project_name}" 吗?`, '确认', { type: 'warning' })
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user