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/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>/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/toggle-all/', views.iam_user_project_toggle_all_view),
|
||||
|
||||
|
||||
@ -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):
|
||||
"""移除关联项目:回收权限 + 移出监测"""
|
||||
|
||||
@ -204,14 +204,37 @@
|
||||
<el-switch :model-value="row.monitor_enabled" @change="val => handleToggleProject(row, val)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<el-table-column label="操作" width="140" align="center">
|
||||
<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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</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 -->
|
||||
<el-dialog v-model="historyVisible" :title="`${historyUser?.username} 额度划拨记录`" width="90%" style="max-width: 800px;">
|
||||
<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) {
|
||||
await ElMessageBox.confirm(`确定移除项目 "${row.project_name}" 吗?`, '确认', { type: 'warning' })
|
||||
try {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user