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//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