mirror of https://github.com/jumpserver/jumpserver
				
				
				
			perf: 修改 account (#10088)
* perf: 优化账号创建策略 * perf: 修改账号 * perf: 修改 account * perf: 修改 account * perf: 修改批量创建 * perf: 修改账号批量创建 * perf: 继续优化账号批量添加 * perf: 优化创建 accounts 的结果 * perf: 优化账号批量返回的格式 * perf: 优化账号 --------- Co-authored-by: ibuler <ibuler@qq.com>pull/10125/head
							parent
							
								
									4601bb9e58
								
							
						
					
					
						commit
						c5340b5adc
					
				|  | @ -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 | ||||
|  |  | |||
|  | @ -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') | ||||
|  |  | |||
|  | @ -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'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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') | ||||
|  |  | |||
|  | @ -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 = { | ||||
|  |  | |||
|  | @ -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')}, | ||||
|         } | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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'), | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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() | ||||
|         } | ||||
|  |  | |||
|  | @ -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'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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') | ||||
| 
 | ||||
|  |  | |||
|  | @ -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): | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 fit2bot
						fit2bot