From c5340b5adcf77583c6e1563769e6e06c9000c96c Mon Sep 17 00:00:00 2001
From: fit2bot <68588906+fit2bot@users.noreply.github.com>
Date: Mon, 3 Apr 2023 18:18:31 +0800
Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=20account=20(#10088)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* perf: 优化账号创建策略

* perf: 修改账号

* perf: 修改 account

* perf: 修改 account

* perf: 修改批量创建

* perf: 修改账号批量创建

* perf: 继续优化账号批量添加

* perf: 优化创建 accounts 的结果

* perf: 优化账号批量返回的格式

* perf: 优化账号

---------

Co-authored-by: ibuler <ibuler@qq.com>
---
 apps/accounts/api/account/account.py          |  18 +-
 apps/accounts/const/account.py                |   2 +-
 .../migrations/0010_account_source_id.py      |  18 +
 apps/accounts/models/account.py               |   1 +
 apps/accounts/serializers/account/account.py  | 378 ++++++++++++++----
 apps/accounts/serializers/account/base.py     |   3 +-
 apps/accounts/signal_handlers.py              |   4 +-
 apps/accounts/urls.py                         |   1 +
 apps/accounts/validator.py                    | 101 -----
 apps/assets/const/protocol.py                 |   8 +
 .../0112_platformprotocol_public.py           |  18 +
 apps/assets/models/platform.py                |   1 +
 apps/assets/serializers/asset/common.py       |  56 +--
 apps/terminal/models/applet/applet.py         |   6 +-
 14 files changed, 378 insertions(+), 237 deletions(-)
 create mode 100644 apps/accounts/migrations/0010_account_source_id.py
 delete mode 100644 apps/accounts/validator.py
 create mode 100644 apps/assets/migrations/0112_platformprotocol_public.py

diff --git a/apps/accounts/api/account/account.py b/apps/accounts/api/account/account.py
index 0cfddd88c..7b9991988 100644
--- a/apps/accounts/api/account/account.py
+++ b/apps/accounts/api/account/account.py
@@ -1,6 +1,6 @@
 from django.shortcuts import get_object_or_404
 from rest_framework.decorators import action
-from rest_framework.generics import ListAPIView
+from rest_framework.generics import ListAPIView, CreateAPIView
 from rest_framework.response import Response
 from rest_framework.status import HTTP_200_OK
 
@@ -15,7 +15,7 @@ from rbac.permissions import RBACPermission
 
 __all__ = [
     'AccountViewSet', 'AccountSecretsViewSet',
-    'AccountHistoriesSecretAPI'
+    'AccountHistoriesSecretAPI', 'AssetAccountBulkCreateApi',
 ]
 
 
@@ -97,6 +97,20 @@ class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
     }
 
 
+class AssetAccountBulkCreateApi(CreateAPIView):
+    serializer_class = serializers.AssetAccountBulkSerializer
+    rbac_perms = {
+        'POST': 'accounts.add_account',
+    }
+
+    def create(self, request, *args, **kwargs):
+        serializer = self.get_serializer(data=request.data)
+        serializer.is_valid(raise_exception=True)
+        data = serializer.create(serializer.validated_data)
+        serializer = serializers.AssetAccountBulkSerializerResultSerializer(data, many=True)
+        return Response(data=serializer.data, status=HTTP_200_OK)
+
+
 class AccountHistoriesSecretAPI(RecordViewLogMixin, ListAPIView):
     model = Account.history.model
     serializer_class = serializers.AccountHistorySerializer
diff --git a/apps/accounts/const/account.py b/apps/accounts/const/account.py
index b86e9400b..29185b233 100644
--- a/apps/accounts/const/account.py
+++ b/apps/accounts/const/account.py
@@ -20,7 +20,7 @@ class Source(TextChoices):
     COLLECTED = 'collected', _('Collected')
 
 
-class BulkCreateStrategy(TextChoices):
+class AccountInvalidPolicy(TextChoices):
     SKIP = 'skip', _('Skip')
     UPDATE = 'update', _('Update')
     ERROR = 'error', _('Failed')
diff --git a/apps/accounts/migrations/0010_account_source_id.py b/apps/accounts/migrations/0010_account_source_id.py
new file mode 100644
index 000000000..dc1a5563c
--- /dev/null
+++ b/apps/accounts/migrations/0010_account_source_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.17 on 2023-03-23 07:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('accounts', '0009_account_usernames_to_ids'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='account',
+            name='source_id',
+            field=models.CharField(max_length=128, null=True, blank=True, verbose_name='Source ID'),
+        ),
+    ]
diff --git a/apps/accounts/models/account.py b/apps/accounts/models/account.py
index 008318c7e..4094018e1 100644
--- a/apps/accounts/models/account.py
+++ b/apps/accounts/models/account.py
@@ -53,6 +53,7 @@ class Account(AbsConnectivity, BaseAccount):
     version = models.IntegerField(default=0, verbose_name=_('Version'))
     history = AccountHistoricalRecords(included_fields=['id', 'secret', 'secret_type', 'version'])
     source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
+    source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
 
     class Meta:
         verbose_name = _('Account')
diff --git a/apps/accounts/serializers/account/account.py b/apps/accounts/serializers/account/account.py
index b8a5d84c0..97bbbfaa7 100644
--- a/apps/accounts/serializers/account/account.py
+++ b/apps/accounts/serializers/account/account.py
@@ -1,15 +1,18 @@
+import uuid
+from collections import defaultdict
+
+from django.db import IntegrityError
+from django.db.models import Q
 from django.utils.translation import ugettext_lazy as _
 from rest_framework import serializers
-from rest_framework.generics import get_object_or_404
 from rest_framework.validators import UniqueTogetherValidator
 
-from accounts import validator
-from accounts.const import SecretType, Source, BulkCreateStrategy
+from accounts.const import SecretType, Source, AccountInvalidPolicy
 from accounts.models import Account, AccountTemplate
 from accounts.tasks import push_accounts_to_assets_task
-from assets.const import Category, AllTypes
+from assets.const import Category, AllTypes, Protocol
 from assets.models import Asset
-from common.serializers import SecretReadableMixin, BulkModelSerializer
+from common.serializers import SecretReadableMixin
 from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
 from common.utils import get_logger
 from .base import BaseAccountSerializer
@@ -17,74 +20,134 @@ from .base import BaseAccountSerializer
 logger = get_logger(__name__)
 
 
-class AccountSerializerCreateValidateMixin:
-    from_id: str
-    template: bool
-    push_now: bool
-    replace_attrs: callable
+class AccountCreateUpdateSerializerMixin(serializers.Serializer):
+    template = serializers.PrimaryKeyRelatedField(
+        queryset=AccountTemplate.objects,
+        required=False, label=_("Template"), write_only=True
+    )
+    push_now = serializers.BooleanField(
+        default=False, label=_("Push now"), write_only=True
+    )
+    on_invalid = LabeledChoiceField(
+        choices=AccountInvalidPolicy.choices, default=AccountInvalidPolicy.ERROR,
+        write_only=True, label=_('Exist policy')
+    )
+
+    class Meta:
+        fields = ['template', 'push_now', 'on_invalid']
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.set_initial_value()
+
+    def set_initial_value(self):
+        if not getattr(self, 'initial_data', None):
+            return
+        if isinstance(self.initial_data, dict):
+            initial_data = [self.initial_data]
+        else:
+            initial_data = self.initial_data
+
+        for data in initial_data:
+            if not data.get('asset') and not self.instance:
+                raise serializers.ValidationError({'asset': 'Asset is required'})
+            asset = data.get('asset') or self.instance.asset
+            self.from_template_if_need(data)
+            self.set_uniq_name_if_need(data, asset)
 
-    def to_internal_value(self, data):
-        from_id = data.pop('id', None)
-        ret = super().to_internal_value(data)
-        self.from_id = from_id
-        return ret
     @staticmethod
-    def related_template_values(template: AccountTemplate, attrs):
-        ignore_fields = ['id', 'date_created', 'date_updated', 'org_id']
+    def set_uniq_name_if_need(initial_data, asset):
+        name = initial_data.get('name')
+        if not name:
+            name = initial_data.get('username')
+        if Account.objects.filter(name=name, asset=asset).exists():
+            name = name + '_' + uuid.uuid4().hex[:4]
+        initial_data['name'] = name
+
+    @staticmethod
+    def from_template_if_need(initial_data):
+        template_id = initial_data.pop('template', None)
+        if not template_id:
+            return
+        if isinstance(template_id, (str, uuid.UUID)):
+            template = AccountTemplate.objects.filter(id=template_id).first()
+        else:
+            template = template_id
+        if not template:
+            raise serializers.ValidationError({'template': 'Template not found'})
+
+        # Set initial data from template
+        ignore_fields = ['id', 'name', 'date_created', 'date_updated', 'org_id']
         field_names = [
             field.name for field in template._meta.fields
             if field.name not in ignore_fields
         ]
+        attrs = {'source': 'template', 'source_id': template.id}
         for name in field_names:
-            attrs[name] = attrs.get(name) or getattr(template, name)
-
-    def set_secret(self, attrs):
-        _id = self.from_id
-        template = attrs.pop('template', None)
-
-        if _id and template:
-            account_template = get_object_or_404(AccountTemplate, id=_id)
-            self.related_template_values(account_template, attrs)
-        elif _id and not template:
-            account = get_object_or_404(Account, id=_id)
-            attrs['secret'] = account.secret
-        return attrs
-
-    def validate(self, attrs):
-        attrs = super().validate(attrs)
-        return self.set_secret(attrs)
+            value = getattr(template, name, None)
+            if value is None:
+                continue
+            attrs[name] = value
+        initial_data.update(attrs)
 
     @staticmethod
-    def push_account(instance, push_now):
-        if not push_now:
+    def push_account_if_need(instance, push_now, stat):
+        if not push_now or stat != 'created':
             return
         push_accounts_to_assets_task.delay([str(instance.id)])
 
+    def get_validators(self):
+        _validators = super().get_validators()
+        if getattr(self, 'initial_data', None) is None:
+            return _validators
+        on_invalid = self.initial_data.get('on_invalid')
+        if on_invalid == AccountInvalidPolicy.ERROR:
+            return _validators
+        _validators = [v for v in _validators if not isinstance(v, UniqueTogetherValidator)]
+        return _validators
+
+    @staticmethod
+    def do_create(vd):
+        on_invalid = vd.pop('on_invalid', None)
+
+        q = Q()
+        if vd.get('name'):
+            q |= Q(name=vd['name'])
+        if vd.get('username'):
+            q |= Q(username=vd['username'], secret_type=vd.get('secret_type'))
+
+        instance = Account.objects.filter(asset=vd['asset']).filter(q).first()
+        # 不存在这个资产,不用关系策略
+        if not instance:
+            instance = Account.objects.create(**vd)
+            return instance, 'created'
+
+        if on_invalid == AccountInvalidPolicy.SKIP:
+            return instance, 'skipped'
+        elif on_invalid == AccountInvalidPolicy.UPDATE:
+            for k, v in vd.items():
+                setattr(instance, k, v)
+            instance.save()
+            return instance, 'updated'
+        else:
+            raise serializers.ValidationError('Account already exists')
+
     def create(self, validated_data):
         push_now = validated_data.pop('push_now', None)
-        instance = super().create(validated_data)
-        self.push_account(instance, push_now)
+        instance, stat = self.do_create(validated_data)
+        self.push_account_if_need(instance, push_now, stat)
         return instance
 
     def update(self, instance, validated_data):
         # account cannot be modified
         validated_data.pop('username', None)
+        validated_data.pop('on_invalid', None)
         push_now = validated_data.pop('push_now', None)
         instance = super().update(instance, validated_data)
-        self.push_account(instance, push_now)
+        self.push_account_if_need(instance, push_now, 'updated')
         return instance
 
 
-class AccountSerializerCreateMixin(AccountSerializerCreateValidateMixin, BulkModelSerializer):
-    template = serializers.BooleanField(
-        default=False, label=_("Template"), write_only=True
-    )
-    push_now = serializers.BooleanField(
-        default=False, label=_("Push now"), write_only=True
-    )
-    has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
-
-
 class AccountAssetSerializer(serializers.ModelSerializer):
     platform = ObjectRelatedField(read_only=True)
     category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
@@ -106,62 +169,207 @@ class AccountAssetSerializer(serializers.ModelSerializer):
             raise serializers.ValidationError(_('Asset not found'))
 
 
-class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer):
+class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer):
     asset = AccountAssetSerializer(label=_('Asset'))
     source = LabeledChoiceField(choices=Source.choices, label=_("Source"), read_only=True)
+    has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
     su_from = ObjectRelatedField(
         required=False, queryset=Account.objects, allow_null=True, allow_empty=True,
         label=_('Su from'), attrs=('id', 'name', 'username')
     )
-    strategy = LabeledChoiceField(
-        choices=BulkCreateStrategy.choices, default=BulkCreateStrategy.SKIP,
-        write_only=True, label=_('Account policy')
-    )
 
     class Meta(BaseAccountSerializer.Meta):
         model = Account
         fields = BaseAccountSerializer.Meta.fields + [
-            'su_from', 'asset', 'template', 'version',
-            'push_now', 'source', 'connectivity', 'strategy'
+            'su_from', 'asset', 'version',
+            'source', 'source_id', 'connectivity',
+        ] + AccountCreateUpdateSerializerMixin.Meta.fields
+        read_only_fields = BaseAccountSerializer.Meta.read_only_fields + [
+            'source', 'source_id', 'connectivity'
         ]
         extra_kwargs = {
             **BaseAccountSerializer.Meta.extra_kwargs,
-            'name': {'required': False, 'allow_null': True},
+            'name': {'required': False},
         }
 
-    def validate_name(self, value):
-        if not value:
-            value = self.initial_data.get('username')
-        return value
-
     @classmethod
     def setup_eager_loading(cls, queryset):
         """ Perform necessary eager loading of data. """
-        queryset = queryset \
-            .prefetch_related('asset', 'asset__platform', 'asset__platform__automation')
+        queryset = queryset.prefetch_related(
+            'asset', 'asset__platform',
+            'asset__platform__automation'
+        )
         return queryset
 
-    def get_validators(self):
-        ignore = False
-        validators = [validator.AccountSecretTypeValidator(fields=('secret_type',))]
-        view = self.context.get('view')
-        request = self.context.get('request')
-        if request and view:
-            data = request.data
-            action = view.action
-            ignore = action == 'create' and isinstance(data, list)
 
-        _validators = super().get_validators()
-        for v in _validators:
-            if ignore and isinstance(v, UniqueTogetherValidator):
-                v = validator.AccountUniqueTogetherValidator(v.queryset, v.fields)
-            validators.append(v)
-        return validators
+class AssetAccountBulkSerializerResultSerializer(serializers.Serializer):
+    asset = serializers.CharField(read_only=True, label=_('Asset'))
+    state = serializers.CharField(read_only=True, label=_('State'))
+    error = serializers.CharField(read_only=True, label=_('Error'))
+    changed = serializers.BooleanField(read_only=True, label=_('Changed'))
 
-    def validate(self, attrs):
-        attrs = super().validate(attrs)
-        attrs.pop('strategy', None)
-        return attrs
+
+class AssetAccountBulkSerializer(AccountCreateUpdateSerializerMixin, serializers.ModelSerializer):
+    assets = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, many=True, label=_('Assets'))
+
+    class Meta:
+        model = Account
+        fields = [
+            'name', 'username', 'secret', 'secret_type',
+            'privileged', 'is_active', 'comment', 'template',
+            'on_invalid', 'push_now', 'assets',
+        ]
+        extra_kwargs = {
+            'name': {'required': False},
+            'secret_type': {'required': False},
+        }
+
+    def set_initial_value(self):
+        if not getattr(self, 'initial_data', None):
+            return
+        initial_data = self.initial_data
+        self.from_template_if_need(initial_data)
+
+    @staticmethod
+    def _get_valid_secret_type_assets(assets, secret_type):
+        if isinstance(assets, list):
+            asset_ids = [a.id for a in assets]
+            assets = Asset.objects.filter(id__in=asset_ids)
+
+        asset_protocol = assets.prefetch_related('protocols').values_list('id', 'protocols__name')
+        protocol_secret_types_map = Protocol.protocol_secret_types()
+        asset_secret_types_mapp = defaultdict(set)
+
+        for asset_id, protocol in asset_protocol:
+            secret_types = set(protocol_secret_types_map.get(protocol, []))
+            asset_secret_types_mapp[asset_id].update(secret_types)
+
+        return [
+            asset for asset in assets
+            if secret_type in asset_secret_types_mapp.get(asset.id, [])
+        ]
+
+    @staticmethod
+    def get_filter_lookup(vd):
+        return {
+            'username': vd['username'],
+            'secret_type': vd['secret_type'],
+            'asset': vd['asset'],
+        }
+
+    @staticmethod
+    def get_uniq_name(vd):
+        return vd['name'] + '-' + uuid.uuid4().hex[:4]
+
+    @staticmethod
+    def _handle_update_create(vd, lookup):
+        ori = Account.objects.filter(**lookup).first()
+        if ori and ori.secret == vd['secret']:
+            return ori, False, 'skipped'
+
+        instance, value = Account.objects.update_or_create(defaults=vd, **lookup)
+        state = 'created' if value else 'updated'
+        return instance, True, state
+
+    @staticmethod
+    def _handle_skip_create(vd, lookup):
+        instance, value = Account.objects.get_or_create(defaults=vd, **lookup)
+        state = 'created' if value else 'skipped'
+        return instance, value, state
+
+    @staticmethod
+    def _handle_err_create(vd, lookup):
+        instance, value = Account.objects.get_or_create(defaults=vd, **lookup)
+        if not value:
+            raise serializers.ValidationError(_('Account already exists'))
+        return instance, True, 'created'
+
+    def perform_create(self, vd, handler):
+        lookup = self.get_filter_lookup(vd)
+        try:
+            instance, changed, state = handler(vd, lookup)
+        except IntegrityError:
+            vd['name'] = self.get_uniq_name(vd)
+            instance, changed, state = handler(vd, lookup)
+        return instance, changed, state
+
+    def get_create_handler(self, on_invalid):
+        if on_invalid == 'update':
+            handler = self._handle_update_create
+        elif on_invalid == 'skip':
+            handler = self._handle_skip_create
+        else:
+            handler = self._handle_err_create
+        return handler
+
+    def perform_bulk_create(self, vd):
+        assets = vd.pop('assets')
+        on_invalid = vd.pop('on_invalid', 'skip')
+        secret_type = vd.get('secret_type', 'password')
+
+        if not vd.get('name'):
+            vd['name'] = vd.get('username')
+
+        create_handler = self.get_create_handler(on_invalid)
+        secret_type_supports = self._get_valid_secret_type_assets(assets, secret_type)
+
+        _results = {}
+        for asset in assets:
+            if asset not in secret_type_supports:
+                _results[asset] = {
+                    'error': _('Asset does not support this secret type: %s') % secret_type,
+                    'state': 'error',
+                }
+                continue
+
+            vd = vd.copy()
+            vd['asset'] = asset
+            try:
+                instance, changed, state = self.perform_create(vd, create_handler)
+                _results[asset] = {
+                    'changed': changed, 'instance': instance.id, 'state': state
+                }
+            except serializers.ValidationError as e:
+                _results[asset] = {'error': e.detail[0], 'state': 'error'}
+            except Exception as e:
+                logger.exception(e)
+                _results[asset] = {'error': str(e), 'state': 'error'}
+
+        results = [{'asset': asset, **result} for asset, result in _results.items()]
+        state_score = {'created': 3, 'updated': 2, 'skipped': 1, 'error': 0}
+        results = sorted(results, key=lambda x: state_score.get(x['state'], 4))
+
+        if on_invalid != 'error':
+            return results
+
+        errors = []
+        errors.extend([result for result in results if result['state'] == 'error'])
+        for result in results:
+            if result['state'] != 'skipped':
+                continue
+            errors.append({
+                'error': _('Account has exist'),
+                'state': 'error',
+                'asset': str(result['asset'])
+            })
+        if errors:
+            raise serializers.ValidationError(errors)
+        return results
+
+    @staticmethod
+    def push_accounts_if_need(results, push_now):
+        if not push_now:
+            return
+        accounts = [str(v['instance']) for v in results if v.get('instance')]
+        push_accounts_to_assets_task.delay(accounts)
+
+    def create(self, validated_data):
+        push_now = validated_data.pop('push_now', False)
+        results = self.perform_bulk_create(validated_data)
+        self.push_accounts_if_need(results, push_now)
+        for res in results:
+            res['asset'] = str(res['asset'])
+        return results
 
 
 class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
@@ -177,8 +385,8 @@ class AccountHistorySerializer(serializers.ModelSerializer):
     class Meta:
         model = Account.history.model
         fields = [
-            'id', 'secret', 'secret_type', 'version', 'history_date',
-            'history_user'
+            'id', 'secret', 'secret_type', 'version',
+            'history_date', 'history_user'
         ]
         read_only_fields = fields
         extra_kwargs = {
diff --git a/apps/accounts/serializers/account/base.py b/apps/accounts/serializers/account/base.py
index 88239c98e..4e2b1a1df 100644
--- a/apps/accounts/serializers/account/base.py
+++ b/apps/accounts/serializers/account/base.py
@@ -13,7 +13,7 @@ __all__ = ['AuthValidateMixin', 'BaseAccountSerializer']
 
 class AuthValidateMixin(serializers.Serializer):
     secret_type = LabeledChoiceField(
-        choices=SecretType.choices, required=True, label=_('Secret type')
+        choices=SecretType.choices, label=_('Secret type'), default='password'
     )
     secret = EncryptedField(
         label=_('Secret'), required=False, max_length=40960, allow_blank=True,
@@ -77,6 +77,5 @@ class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
             'date_verified', 'created_by', 'date_created',
         ]
         extra_kwargs = {
-            'name': {'required': True},
             'spec_info': {'label': _('Spec info')},
         }
diff --git a/apps/accounts/signal_handlers.py b/apps/accounts/signal_handlers.py
index b47588192..cf09842cc 100644
--- a/apps/accounts/signal_handlers.py
+++ b/apps/accounts/signal_handlers.py
@@ -8,8 +8,8 @@ logger = get_logger(__name__)
 
 
 @receiver(pre_save, sender=Account)
-def on_account_pre_save(sender, instance, created=False, **kwargs):
-    if created:
+def on_account_pre_save(sender, instance, **kwargs):
+    if instance.version == 0:
         instance.version = 1
     else:
         instance.version = instance.history.count()
diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py
index 59a70b3cf..5c57ad67b 100644
--- a/apps/accounts/urls.py
+++ b/apps/accounts/urls.py
@@ -25,6 +25,7 @@ router.register(r'push-account-executions', api.PushAccountExecutionViewSet, 'pu
 router.register(r'push-account-records', api.PushAccountRecordViewSet, 'push-account-record')
 
 urlpatterns = [
+    path('accounts/bulk/', api.AssetAccountBulkCreateApi.as_view(), name='account-bulk-create'),
     path('accounts/tasks/', api.AccountsTaskCreateAPI.as_view(), name='account-task-create'),
     path('account-secrets/<uuid:pk>/histories/', api.AccountHistoriesSecretAPI.as_view(),
          name='account-secret-history'),
diff --git a/apps/accounts/validator.py b/apps/accounts/validator.py
deleted file mode 100644
index b8c49896d..000000000
--- a/apps/accounts/validator.py
+++ /dev/null
@@ -1,101 +0,0 @@
-from functools import reduce
-
-from django.utils.translation import ugettext_lazy as _
-from rest_framework.validators import (
-    UniqueTogetherValidator, ValidationError
-)
-
-from accounts.const import BulkCreateStrategy
-from accounts.models import Account
-from assets.const import Protocol
-
-__all__ = ['AccountUniqueTogetherValidator', 'AccountSecretTypeValidator']
-
-
-class ValidatorStrategyMixin:
-
-    @staticmethod
-    def get_strategy(attrs):
-        return attrs.get('strategy', BulkCreateStrategy.SKIP)
-
-    def __call__(self, attrs, serializer):
-        message = None
-        try:
-            super().__call__(attrs, serializer)
-        except ValidationError as e:
-            message = e.detail[0]
-        strategy = self.get_strategy(attrs)
-        if not message:
-            return
-        if strategy == BulkCreateStrategy.ERROR:
-            raise ValidationError(message, code='error')
-        elif strategy in [BulkCreateStrategy.SKIP, BulkCreateStrategy.UPDATE]:
-            raise ValidationError({})
-        else:
-            return
-
-
-class SecretTypeValidator:
-    requires_context = True
-    protocol_settings = Protocol.settings()
-    message = _('{field_name} not a legal option')
-
-    def __init__(self, fields):
-        self.fields = fields
-
-    def __call__(self, attrs, serializer):
-        secret_types = set()
-        if serializer.instance:
-            asset = serializer.instance.asset
-        else:
-            asset = attrs['asset']
-        secret_type = attrs['secret_type']
-        platform_protocols_dict = {
-            name: self.protocol_settings.get(name, {}).get('secret_types', [])
-            for name in asset.platform.protocols.values_list('name', flat=True)
-        }
-
-        for name in asset.protocols.values_list('name', flat=True):
-            if name in platform_protocols_dict:
-                secret_types |= set(platform_protocols_dict[name])
-        if secret_type not in secret_types:
-            message = self.message.format(field_name=secret_type)
-            raise ValidationError(message, code='error')
-
-
-class UpdateAccountMixin:
-    fields: tuple
-    get_strategy: callable
-
-    def update(self, attrs):
-        unique_together = Account._meta.unique_together
-        unique_together_fields = reduce(lambda x, y: set(x) | set(y), unique_together)
-        query = {field_name: attrs[field_name] for field_name in unique_together_fields}
-        account = Account.objects.filter(**query).first()
-        if not account:
-            query = {field_name: attrs[field_name] for field_name in self.fields}
-            account = Account.objects.filter(**query).first()
-
-        for k, v in attrs.items():
-            setattr(account, k, v)
-        account.save()
-
-    def __call__(self, attrs, serializer):
-        try:
-            super().__call__(attrs, serializer)
-        except ValidationError as e:
-            strategy = self.get_strategy(attrs)
-            if strategy == BulkCreateStrategy.UPDATE:
-                self.update(attrs)
-            message = e.detail[0]
-            raise ValidationError(message, code='unique')
-
-
-class AccountUniqueTogetherValidator(
-    ValidatorStrategyMixin, UpdateAccountMixin, UniqueTogetherValidator
-):
-    pass
-
-
-class AccountSecretTypeValidator(ValidatorStrategyMixin, SecretTypeValidator):
-    pass
diff --git a/apps/assets/const/protocol.py b/apps/assets/const/protocol.py
index 6523c5dcc..39d3f3112 100644
--- a/apps/assets/const/protocol.py
+++ b/apps/assets/const/protocol.py
@@ -128,3 +128,11 @@ class Protocol(ChoicesMixin, models.TextChoices):
             **cls.database_protocols(),
             **cls.cloud_protocols()
         }
+
+    @classmethod
+    def protocol_secret_types(cls):
+        settings = cls.settings()
+        return {
+            protocol: settings[protocol]['secret_types']
+            for protocol in cls.settings()
+        }
diff --git a/apps/assets/migrations/0112_platformprotocol_public.py b/apps/assets/migrations/0112_platformprotocol_public.py
new file mode 100644
index 000000000..917686bc6
--- /dev/null
+++ b/apps/assets/migrations/0112_platformprotocol_public.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.17 on 2023-03-24 03:11
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('assets', '0111_auto_20230321_1633'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='platformprotocol',
+            name='public',
+            field=models.BooleanField(default=True, verbose_name='Public'),
+        ),
+    ]
diff --git a/apps/assets/models/platform.py b/apps/assets/models/platform.py
index e2262af04..7f57c01d7 100644
--- a/apps/assets/models/platform.py
+++ b/apps/assets/models/platform.py
@@ -15,6 +15,7 @@ class PlatformProtocol(models.Model):
     primary = models.BooleanField(default=False, verbose_name=_('Primary'))
     required = models.BooleanField(default=False, verbose_name=_('Required'))
     default = models.BooleanField(default=False, verbose_name=_('Default'))
+    public = models.BooleanField(default=True, verbose_name=_('Public'))
     setting = models.JSONField(verbose_name=_('Setting'), default=dict)
     platform = models.ForeignKey('Platform', on_delete=models.CASCADE, related_name='protocols')
 
diff --git a/apps/assets/serializers/asset/common.py b/apps/assets/serializers/asset/common.py
index 0a544a7b2..22329b22a 100644
--- a/apps/assets/serializers/asset/common.py
+++ b/apps/assets/serializers/asset/common.py
@@ -6,9 +6,8 @@ from django.db.transaction import atomic
 from django.utils.translation import ugettext_lazy as _
 from rest_framework import serializers
 
-from accounts.const import SecretType
 from accounts.models import Account
-from accounts.serializers import AuthValidateMixin, AccountSerializerCreateValidateMixin
+from accounts.serializers import AccountSerializer
 from common.serializers import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer
 from common.serializers.fields import LabeledChoiceField
 from orgs.mixins.serializers import BulkOrgResourceModelSerializer
@@ -59,49 +58,19 @@ class AssetPlatformSerializer(serializers.ModelSerializer):
         }
 
 
-class AssetAccountSerializer(
-    AuthValidateMixin,
-    AccountSerializerCreateValidateMixin,
-    CommonModelSerializer
-):
+class AssetAccountSerializer(AccountSerializer):
     add_org_fields = False
-    push_now = serializers.BooleanField(
-        default=False, label=_("Push now"), write_only=True
-    )
-    template = serializers.BooleanField(
-        default=False, label=_("Template"), write_only=True
-    )
-    name = serializers.CharField(max_length=128, required=False, label=_("Name"))
-    secret_type = LabeledChoiceField(
-        choices=SecretType.choices, default=SecretType.PASSWORD,
-        required=False, label=_('Secret type')
-    )
+    asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, required=False, write_only=True)
 
-    class Meta:
-        model = Account
-        fields_mini = [
-            'id', 'name', 'username', 'privileged',
-            'is_active', 'version', 'secret_type',
+    class Meta(AccountSerializer.Meta):
+        fields = [
+            f for f in AccountSerializer.Meta.fields
+            if f not in ['spec_info']
         ]
-        fields_write_only = [
-            'secret', 'passphrase', 'push_now', 'template'
-        ]
-        fields = fields_mini + fields_write_only
         extra_kwargs = {
-            'secret': {'write_only': True},
+            **AccountSerializer.Meta.extra_kwargs,
         }
 
-    def validate_push_now(self, value):
-        request = self.context['request']
-        if not request.user.has_perms('accounts.push_account'):
-            return False
-        return value
-
-    def validate_name(self, value):
-        if not value:
-            value = self.initial_data.get('username')
-        return value
-
 
 class AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer):
     class Meta:
@@ -132,7 +101,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
     type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))
     labels = AssetLabelSerializer(many=True, required=False, label=_('Label'))
     protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
-    accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Account'))
+    accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, label=_('Account'))
     nodes_display = serializers.ListField(read_only=False, required=False, label=_("Node path"))
 
     class Meta:
@@ -280,8 +249,11 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
         if not accounts_data:
             return
         for data in accounts_data:
-            data['asset'] = asset
-            AssetAccountSerializer().create(data)
+            data['asset'] = asset.id
+
+        s = AssetAccountSerializer(data=accounts_data, many=True)
+        s.is_valid(raise_exception=True)
+        s.save()
 
     @atomic
     def create(self, validated_data):
diff --git a/apps/terminal/models/applet/applet.py b/apps/terminal/models/applet/applet.py
index 22afe39e8..bb6e11179 100644
--- a/apps/terminal/models/applet/applet.py
+++ b/apps/terminal/models/applet/applet.py
@@ -112,8 +112,10 @@ class Applet(JMSBaseModel):
 
     def select_host_account(self):
         # 选择激活的发布机
-        hosts = [item for item in self.hosts.filter(is_active=True).all()
-                 if item.load != 'offline']
+        hosts = [
+            host for host in self.hosts.filter(is_active=True)
+            if host.load != 'offline'
+        ]
 
         if not hosts:
             return None