From c58fe56d89502263695e197310b59541232bdd1d Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Fri, 20 Mar 2026 19:28:14 +0800 Subject: [PATCH] feat: add project-level policy management (add/remove per project) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- backend/apps/monitor/urls.py | 1 + backend/apps/monitor/views.py | 66 ++++++++++++++++++++++++-- frontend/src/views/iam/IAMUserList.vue | 54 ++++++++++++++++++++- 3 files changed, 115 insertions(+), 6 deletions(-) diff --git a/backend/apps/monitor/urls.py b/backend/apps/monitor/urls.py index 7acc38f..07a72ca 100644 --- a/backend/apps/monitor/urls.py +++ b/backend/apps/monitor/urls.py @@ -26,6 +26,7 @@ urlpatterns = [ path('iam-users//projects/', views.iam_user_project_list_view), path('iam-users//projects/add/', views.iam_user_project_add_view), path('iam-users//projects//', views.iam_user_project_update_view), + path('iam-users//projects//policies/', views.iam_user_project_policies_view), path('iam-users//projects//delete/', views.iam_user_project_delete_view), path('iam-users//projects/toggle-all/', views.iam_user_project_toggle_all_view), diff --git a/backend/apps/monitor/views.py b/backend/apps/monitor/views.py index c0bbf24..b202823 100644 --- a/backend/apps/monitor/views.py +++ b/backend/apps/monitor/views.py @@ -610,11 +610,7 @@ def iam_user_project_add_view(request, pk): d['project_name']) attached.append(policy_name) 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.save(update_fields=['attached_policies']) @@ -652,6 +648,66 @@ def iam_user_project_update_view(request, pk, pid): 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']) def iam_user_project_delete_view(request, pk, pid): """移除关联项目:回收权限 + 移出监测""" diff --git a/frontend/src/views/iam/IAMUserList.vue b/frontend/src/views/iam/IAMUserList.vue index 0e888b9..64e3414 100644 --- a/frontend/src/views/iam/IAMUserList.vue +++ b/frontend/src/views/iam/IAMUserList.vue @@ -204,14 +204,37 @@ - + + + +

+ 子账号 {{ projectsUser?.username }} 在此项目下的权限: +

+ +
+ 方舟/Seedance 完整权限 + 方舟只读 + 对象存储完整权限 + 对象存储只读 + 自管理密钥 +
+
+ +
+ @@ -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) { await ElMessageBox.confirm(`确定移除项目 "${row.project_name}" 吗?`, '确认', { type: 'warning' }) try {