diff --git a/backend/apps/monitor/migrations/0006_iamuser_saved_policies_on_disable.py b/backend/apps/monitor/migrations/0006_iamuser_saved_policies_on_disable.py
new file mode 100644
index 0000000..54e55a7
--- /dev/null
+++ b/backend/apps/monitor/migrations/0006_iamuser_saved_policies_on_disable.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.21 on 2026-03-20 06:56
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('monitor', '0005_globalconfig_default_project_policies_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='iamuser',
+ name='saved_policies_on_disable',
+ field=models.JSONField(blank=True, default=list, help_text='停用时自动移除的策略列表,恢复时加回', verbose_name='停用时保存的策略'),
+ ),
+ ]
diff --git a/backend/apps/monitor/models.py b/backend/apps/monitor/models.py
index c60f9c1..fd1ae68 100644
--- a/backend/apps/monitor/models.py
+++ b/backend/apps/monitor/models.py
@@ -56,6 +56,10 @@ class IAMUser(models.Model):
triggered_alerts = models.JSONField('已触发的告警阈值', default=list, blank=True,
help_text='记录已通知过的百分比,划拨新额度时自动重置')
+ # --- 停用时保存的策略快照(恢复时自动加回) ---
+ saved_policies_on_disable = models.JSONField('停用时保存的策略', default=list, blank=True,
+ help_text='停用时自动移除的策略列表,恢复时加回')
+
remark = models.TextField('备注', blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
diff --git a/backend/apps/monitor/serializers.py b/backend/apps/monitor/serializers.py
index fba26cc..fb9bab5 100644
--- a/backend/apps/monitor/serializers.py
+++ b/backend/apps/monitor/serializers.py
@@ -57,11 +57,11 @@ class IAMUserSerializer(serializers.ModelSerializer):
class IAMUserCreateSerializer(serializers.Serializer):
username = serializers.CharField(max_length=200)
- display_name = serializers.CharField(max_length=200, required=False, default='')
- email = serializers.EmailField(required=False, default='')
- phone = serializers.CharField(max_length=20, required=False, default='')
- password = serializers.CharField(write_only=True, required=False, default='')
- project_name = serializers.CharField(max_length=200, required=False, default='',
+ display_name = serializers.CharField(max_length=200, required=False, default='', allow_blank=True)
+ email = serializers.CharField(max_length=200, required=False, default='', allow_blank=True)
+ phone = serializers.CharField(max_length=20, required=False, default='', allow_blank=True)
+ password = serializers.CharField(write_only=True, required=False, default='', allow_blank=True)
+ project_name = serializers.CharField(max_length=200, required=False, default='', allow_blank=True,
help_text='可选,创建后自动关联此项目')
@@ -80,8 +80,13 @@ class IAMUserConfigSerializer(serializers.Serializer):
class IAMUserProjectAddSerializer(serializers.Serializer):
project_name = serializers.CharField(max_length=200)
- display_name = serializers.CharField(max_length=200, required=False, default='')
+ display_name = serializers.CharField(max_length=200, required=False, default='', allow_blank=True)
monitor_enabled = serializers.BooleanField(required=False, default=True)
+ policies = serializers.ListField(
+ child=serializers.CharField(max_length=200),
+ required=False, default=list,
+ help_text='要在项目范围内授权的策略名列表,如 ["ArkFullAccess"]'
+ )
class IAMUserProjectUpdateSerializer(serializers.Serializer):
diff --git a/backend/apps/monitor/views.py b/backend/apps/monitor/views.py
index 4434f21..31f1199 100644
--- a/backend/apps/monitor/views.py
+++ b/backend/apps/monitor/views.py
@@ -234,6 +234,10 @@ def iam_user_create_view(request):
phone=d.get('phone', ''),
)
except VolcengineAPIError as e:
+ if 'UserAlreadyExists' in e.code or 'EntityAlreadyExists' in e.code:
+ return Response({'error': 'user_exists',
+ 'message': f"火山引擎上已存在用户 {d['username']},请使用「同步已有用户」导入"},
+ status=status.HTTP_409_CONFLICT)
return Response({'error': 'create_failed', 'message': f'创建用户失败: {e}'},
status=status.HTTP_502_BAD_GATEWAY)
@@ -377,16 +381,43 @@ def iam_user_disable_view(request, pk):
svc = IAMService(ak, sk)
try:
+ # 1. 停用控制台 + API 密钥
svc.disable_user(user.username)
+
+ # 2. 移除所有权限策略并保存快照(恢复时加回)
+ saved_policies = []
+ detach_errors = []
+ try:
+ resp = svc.list_attached_user_policies(user.username)
+ policies = resp.get("Result", {}).get("AttachedPolicyMetadata", [])
+ for p in policies:
+ pname = p.get("PolicyName", "")
+ ptype = p.get("PolicyType", "")
+ try:
+ svc.detach_user_policy(user.username, pname, ptype)
+ saved_policies.append({"name": pname, "type": ptype})
+ except VolcengineAPIError as detach_err:
+ detach_errors.append(f"{pname}: {detach_err}")
+ except VolcengineAPIError:
+ pass
+
user.status = IAMUser.Status.DISABLED
- user.save(update_fields=['status'])
+ user.saved_policies_on_disable = saved_policies
+ user.save(update_fields=['status', 'saved_policies_on_disable'])
+
+ policy_count = len(saved_policies)
+ error_info = f",移除失败: {detach_errors}" if detach_errors else ""
AlertRecord.objects.create(
iam_user=user,
alert_type=AlertRecord.AlertType.MANUAL,
title=f"手动停用子账号 {user.username}",
- content=f"操作人: {request.user.username}",
+ content=f"操作人: {request.user.username},已移除 {policy_count} 个权限策略{error_info}",
)
- return Response({'message': f'用户 {user.username} 已停用'})
+ msg = f'用户 {user.username} 已停用,{policy_count} 个权限策略已移除'
+ result = {'message': msg}
+ if detach_errors:
+ result['warnings'] = detach_errors
+ return Response(result)
except VolcengineAPIError as e:
return Response({'error': 'api_error', 'message': str(e)},
status=status.HTTP_502_BAD_GATEWAY)
@@ -406,16 +437,36 @@ def iam_user_enable_view(request, pk):
svc = IAMService(ak, sk)
try:
+ # 1. 恢复控制台 + API 密钥
svc.enable_user(user.username)
+
+ # 2. 重新附加停用时保存的策略
+ restored_count = 0
+ restore_errors = []
+ saved_policies = user.saved_policies_on_disable or []
+ for p in saved_policies:
+ try:
+ svc.attach_user_policy(user.username, p["name"], p["type"])
+ restored_count += 1
+ except VolcengineAPIError as restore_err:
+ restore_errors.append(f"{p['name']}: {restore_err}")
+
user.status = IAMUser.Status.ACTIVE
- user.save(update_fields=['status'])
+ user.saved_policies_on_disable = []
+ user.save(update_fields=['status', 'saved_policies_on_disable'])
+
+ error_info = f",恢复失败: {restore_errors}" if restore_errors else ""
AlertRecord.objects.create(
iam_user=user,
alert_type=AlertRecord.AlertType.MANUAL,
title=f"手动恢复子账号 {user.username}",
- content=f"操作人: {request.user.username}",
+ content=f"操作人: {request.user.username},已恢复 {restored_count} 个权限策略{error_info}",
)
- return Response({'message': f'用户 {user.username} 已恢复'})
+ msg = f'用户 {user.username} 已恢复,{restored_count} 个权限策略已恢复'
+ result = {'message': msg}
+ if restore_errors:
+ result['warnings'] = restore_errors
+ return Response(result)
except VolcengineAPIError as e:
return Response({'error': 'api_error', 'message': str(e)},
status=status.HTTP_502_BAD_GATEWAY)
@@ -546,15 +597,14 @@ def iam_user_project_add_view(request, pk):
return Response({'error': 'duplicate', 'message': f'项目 {d["project_name"]} 已关联'},
status=status.HTTP_409_CONFLICT)
- # 自动在项目范围内授权默认策略
+ # 在项目范围内授权前端指定的策略(如果传入了)
account, ak, sk = _get_volc_account(user.volc_account_id)
attached = []
auth_errors = []
- if ak:
+ policies_to_attach = d.get('policies', [])
+ if ak and policies_to_attach:
svc = IAMService(ak, sk)
- config = GlobalConfig.get_solo()
- policies = config.default_project_policies or ['ArkFullAccess', 'TOSFullAccess']
- for policy_name in policies:
+ for policy_name in policies_to_attach:
try:
svc.attach_policy_in_project(user.username, policy_name,
d['project_name'])
diff --git a/frontend/src/views/iam/IAMUserList.vue b/frontend/src/views/iam/IAMUserList.vue
index 85ca0d4..0e888b9 100644
--- a/frontend/src/views/iam/IAMUserList.vue
+++ b/frontend/src/views/iam/IAMUserList.vue
@@ -165,11 +165,21 @@
:loading="volcProjectsLoading">