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