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 django.shortcuts import get_object_or_404
|
||||||
from rest_framework.decorators import action
|
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.response import Response
|
||||||
from rest_framework.status import HTTP_200_OK
|
from rest_framework.status import HTTP_200_OK
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ from rbac.permissions import RBACPermission
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'AccountViewSet', 'AccountSecretsViewSet',
|
'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):
|
class AccountHistoriesSecretAPI(RecordViewLogMixin, ListAPIView):
|
||||||
model = Account.history.model
|
model = Account.history.model
|
||||||
serializer_class = serializers.AccountHistorySerializer
|
serializer_class = serializers.AccountHistorySerializer
|
||||||
|
|
|
@ -20,7 +20,7 @@ class Source(TextChoices):
|
||||||
COLLECTED = 'collected', _('Collected')
|
COLLECTED = 'collected', _('Collected')
|
||||||
|
|
||||||
|
|
||||||
class BulkCreateStrategy(TextChoices):
|
class AccountInvalidPolicy(TextChoices):
|
||||||
SKIP = 'skip', _('Skip')
|
SKIP = 'skip', _('Skip')
|
||||||
UPDATE = 'update', _('Update')
|
UPDATE = 'update', _('Update')
|
||||||
ERROR = 'error', _('Failed')
|
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'))
|
version = models.IntegerField(default=0, verbose_name=_('Version'))
|
||||||
history = AccountHistoricalRecords(included_fields=['id', 'secret', 'secret_type', 'version'])
|
history = AccountHistoricalRecords(included_fields=['id', 'secret', 'secret_type', 'version'])
|
||||||
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
|
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:
|
class Meta:
|
||||||
verbose_name = _('Account')
|
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 django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.generics import get_object_or_404
|
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
|
|
||||||
from accounts import validator
|
from accounts.const import SecretType, Source, AccountInvalidPolicy
|
||||||
from accounts.const import SecretType, Source, BulkCreateStrategy
|
|
||||||
from accounts.models import Account, AccountTemplate
|
from accounts.models import Account, AccountTemplate
|
||||||
from accounts.tasks import push_accounts_to_assets_task
|
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 assets.models import Asset
|
||||||
from common.serializers import SecretReadableMixin, BulkModelSerializer
|
from common.serializers import SecretReadableMixin
|
||||||
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
|
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from .base import BaseAccountSerializer
|
from .base import BaseAccountSerializer
|
||||||
|
@ -17,74 +20,134 @@ from .base import BaseAccountSerializer
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AccountSerializerCreateValidateMixin:
|
class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||||
from_id: str
|
template = serializers.PrimaryKeyRelatedField(
|
||||||
template: bool
|
queryset=AccountTemplate.objects,
|
||||||
push_now: bool
|
required=False, label=_("Template"), write_only=True
|
||||||
replace_attrs: callable
|
)
|
||||||
|
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
|
@staticmethod
|
||||||
def related_template_values(template: AccountTemplate, attrs):
|
def set_uniq_name_if_need(initial_data, asset):
|
||||||
ignore_fields = ['id', 'date_created', 'date_updated', 'org_id']
|
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_names = [
|
||||||
field.name for field in template._meta.fields
|
field.name for field in template._meta.fields
|
||||||
if field.name not in ignore_fields
|
if field.name not in ignore_fields
|
||||||
]
|
]
|
||||||
|
attrs = {'source': 'template', 'source_id': template.id}
|
||||||
for name in field_names:
|
for name in field_names:
|
||||||
attrs[name] = attrs.get(name) or getattr(template, name)
|
value = getattr(template, name, None)
|
||||||
|
if value is None:
|
||||||
def set_secret(self, attrs):
|
continue
|
||||||
_id = self.from_id
|
attrs[name] = value
|
||||||
template = attrs.pop('template', None)
|
initial_data.update(attrs)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def push_account(instance, push_now):
|
def push_account_if_need(instance, push_now, stat):
|
||||||
if not push_now:
|
if not push_now or stat != 'created':
|
||||||
return
|
return
|
||||||
push_accounts_to_assets_task.delay([str(instance.id)])
|
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):
|
def create(self, validated_data):
|
||||||
push_now = validated_data.pop('push_now', None)
|
push_now = validated_data.pop('push_now', None)
|
||||||
instance = super().create(validated_data)
|
instance, stat = self.do_create(validated_data)
|
||||||
self.push_account(instance, push_now)
|
self.push_account_if_need(instance, push_now, stat)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
# account cannot be modified
|
# account cannot be modified
|
||||||
validated_data.pop('username', None)
|
validated_data.pop('username', None)
|
||||||
|
validated_data.pop('on_invalid', None)
|
||||||
push_now = validated_data.pop('push_now', None)
|
push_now = validated_data.pop('push_now', None)
|
||||||
instance = super().update(instance, validated_data)
|
instance = super().update(instance, validated_data)
|
||||||
self.push_account(instance, push_now)
|
self.push_account_if_need(instance, push_now, 'updated')
|
||||||
return instance
|
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):
|
class AccountAssetSerializer(serializers.ModelSerializer):
|
||||||
platform = ObjectRelatedField(read_only=True)
|
platform = ObjectRelatedField(read_only=True)
|
||||||
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
|
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
|
||||||
|
@ -106,62 +169,207 @@ class AccountAssetSerializer(serializers.ModelSerializer):
|
||||||
raise serializers.ValidationError(_('Asset not found'))
|
raise serializers.ValidationError(_('Asset not found'))
|
||||||
|
|
||||||
|
|
||||||
class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer):
|
class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer):
|
||||||
asset = AccountAssetSerializer(label=_('Asset'))
|
asset = AccountAssetSerializer(label=_('Asset'))
|
||||||
source = LabeledChoiceField(choices=Source.choices, label=_("Source"), read_only=True)
|
source = LabeledChoiceField(choices=Source.choices, label=_("Source"), read_only=True)
|
||||||
|
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
||||||
su_from = ObjectRelatedField(
|
su_from = ObjectRelatedField(
|
||||||
required=False, queryset=Account.objects, allow_null=True, allow_empty=True,
|
required=False, queryset=Account.objects, allow_null=True, allow_empty=True,
|
||||||
label=_('Su from'), attrs=('id', 'name', 'username')
|
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):
|
class Meta(BaseAccountSerializer.Meta):
|
||||||
model = Account
|
model = Account
|
||||||
fields = BaseAccountSerializer.Meta.fields + [
|
fields = BaseAccountSerializer.Meta.fields + [
|
||||||
'su_from', 'asset', 'template', 'version',
|
'su_from', 'asset', 'version',
|
||||||
'push_now', 'source', 'connectivity', 'strategy'
|
'source', 'source_id', 'connectivity',
|
||||||
|
] + AccountCreateUpdateSerializerMixin.Meta.fields
|
||||||
|
read_only_fields = BaseAccountSerializer.Meta.read_only_fields + [
|
||||||
|
'source', 'source_id', 'connectivity'
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
**BaseAccountSerializer.Meta.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
|
@classmethod
|
||||||
def setup_eager_loading(cls, queryset):
|
def setup_eager_loading(cls, queryset):
|
||||||
""" Perform necessary eager loading of data. """
|
""" Perform necessary eager loading of data. """
|
||||||
queryset = queryset \
|
queryset = queryset.prefetch_related(
|
||||||
.prefetch_related('asset', 'asset__platform', 'asset__platform__automation')
|
'asset', 'asset__platform',
|
||||||
|
'asset__platform__automation'
|
||||||
|
)
|
||||||
return queryset
|
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()
|
class AssetAccountBulkSerializerResultSerializer(serializers.Serializer):
|
||||||
for v in _validators:
|
asset = serializers.CharField(read_only=True, label=_('Asset'))
|
||||||
if ignore and isinstance(v, UniqueTogetherValidator):
|
state = serializers.CharField(read_only=True, label=_('State'))
|
||||||
v = validator.AccountUniqueTogetherValidator(v.queryset, v.fields)
|
error = serializers.CharField(read_only=True, label=_('Error'))
|
||||||
validators.append(v)
|
changed = serializers.BooleanField(read_only=True, label=_('Changed'))
|
||||||
return validators
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
|
||||||
attrs = super().validate(attrs)
|
class AssetAccountBulkSerializer(AccountCreateUpdateSerializerMixin, serializers.ModelSerializer):
|
||||||
attrs.pop('strategy', None)
|
assets = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, many=True, label=_('Assets'))
|
||||||
return attrs
|
|
||||||
|
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):
|
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||||
|
@ -177,8 +385,8 @@ class AccountHistorySerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Account.history.model
|
model = Account.history.model
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'secret', 'secret_type', 'version', 'history_date',
|
'id', 'secret', 'secret_type', 'version',
|
||||||
'history_user'
|
'history_date', 'history_user'
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
|
|
|
@ -13,7 +13,7 @@ __all__ = ['AuthValidateMixin', 'BaseAccountSerializer']
|
||||||
|
|
||||||
class AuthValidateMixin(serializers.Serializer):
|
class AuthValidateMixin(serializers.Serializer):
|
||||||
secret_type = LabeledChoiceField(
|
secret_type = LabeledChoiceField(
|
||||||
choices=SecretType.choices, required=True, label=_('Secret type')
|
choices=SecretType.choices, label=_('Secret type'), default='password'
|
||||||
)
|
)
|
||||||
secret = EncryptedField(
|
secret = EncryptedField(
|
||||||
label=_('Secret'), required=False, max_length=40960, allow_blank=True,
|
label=_('Secret'), required=False, max_length=40960, allow_blank=True,
|
||||||
|
@ -77,6 +77,5 @@ class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
||||||
'date_verified', 'created_by', 'date_created',
|
'date_verified', 'created_by', 'date_created',
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'name': {'required': True},
|
|
||||||
'spec_info': {'label': _('Spec info')},
|
'spec_info': {'label': _('Spec info')},
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@ logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save, sender=Account)
|
@receiver(pre_save, sender=Account)
|
||||||
def on_account_pre_save(sender, instance, created=False, **kwargs):
|
def on_account_pre_save(sender, instance, **kwargs):
|
||||||
if created:
|
if instance.version == 0:
|
||||||
instance.version = 1
|
instance.version = 1
|
||||||
else:
|
else:
|
||||||
instance.version = instance.history.count()
|
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')
|
router.register(r'push-account-records', api.PushAccountRecordViewSet, 'push-account-record')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path('accounts/bulk/', api.AssetAccountBulkCreateApi.as_view(), name='account-bulk-create'),
|
||||||
path('accounts/tasks/', api.AccountsTaskCreateAPI.as_view(), name='account-task-create'),
|
path('accounts/tasks/', api.AccountsTaskCreateAPI.as_view(), name='account-task-create'),
|
||||||
path('account-secrets/<uuid:pk>/histories/', api.AccountHistoriesSecretAPI.as_view(),
|
path('account-secrets/<uuid:pk>/histories/', api.AccountHistoriesSecretAPI.as_view(),
|
||||||
name='account-secret-history'),
|
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.database_protocols(),
|
||||||
**cls.cloud_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'))
|
primary = models.BooleanField(default=False, verbose_name=_('Primary'))
|
||||||
required = models.BooleanField(default=False, verbose_name=_('Required'))
|
required = models.BooleanField(default=False, verbose_name=_('Required'))
|
||||||
default = models.BooleanField(default=False, verbose_name=_('Default'))
|
default = models.BooleanField(default=False, verbose_name=_('Default'))
|
||||||
|
public = models.BooleanField(default=True, verbose_name=_('Public'))
|
||||||
setting = models.JSONField(verbose_name=_('Setting'), default=dict)
|
setting = models.JSONField(verbose_name=_('Setting'), default=dict)
|
||||||
platform = models.ForeignKey('Platform', on_delete=models.CASCADE, related_name='protocols')
|
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 django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.const import SecretType
|
|
||||||
from accounts.models import Account
|
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 import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer
|
||||||
from common.serializers.fields import LabeledChoiceField
|
from common.serializers.fields import LabeledChoiceField
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
|
@ -59,49 +58,19 @@ class AssetPlatformSerializer(serializers.ModelSerializer):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class AssetAccountSerializer(
|
class AssetAccountSerializer(AccountSerializer):
|
||||||
AuthValidateMixin,
|
|
||||||
AccountSerializerCreateValidateMixin,
|
|
||||||
CommonModelSerializer
|
|
||||||
):
|
|
||||||
add_org_fields = False
|
add_org_fields = False
|
||||||
push_now = serializers.BooleanField(
|
asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, required=False, write_only=True)
|
||||||
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')
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta(AccountSerializer.Meta):
|
||||||
model = Account
|
fields = [
|
||||||
fields_mini = [
|
f for f in AccountSerializer.Meta.fields
|
||||||
'id', 'name', 'username', 'privileged',
|
if f not in ['spec_info']
|
||||||
'is_active', 'version', 'secret_type',
|
|
||||||
]
|
]
|
||||||
fields_write_only = [
|
|
||||||
'secret', 'passphrase', 'push_now', 'template'
|
|
||||||
]
|
|
||||||
fields = fields_mini + fields_write_only
|
|
||||||
extra_kwargs = {
|
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 AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -132,7 +101,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
|
||||||
type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))
|
type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))
|
||||||
labels = AssetLabelSerializer(many=True, required=False, label=_('Label'))
|
labels = AssetLabelSerializer(many=True, required=False, label=_('Label'))
|
||||||
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
|
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"))
|
nodes_display = serializers.ListField(read_only=False, required=False, label=_("Node path"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -280,8 +249,11 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
|
||||||
if not accounts_data:
|
if not accounts_data:
|
||||||
return
|
return
|
||||||
for data in accounts_data:
|
for data in accounts_data:
|
||||||
data['asset'] = asset
|
data['asset'] = asset.id
|
||||||
AssetAccountSerializer().create(data)
|
|
||||||
|
s = AssetAccountSerializer(data=accounts_data, many=True)
|
||||||
|
s.is_valid(raise_exception=True)
|
||||||
|
s.save()
|
||||||
|
|
||||||
@atomic
|
@atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
|
|
|
@ -112,8 +112,10 @@ class Applet(JMSBaseModel):
|
||||||
|
|
||||||
def select_host_account(self):
|
def select_host_account(self):
|
||||||
# 选择激活的发布机
|
# 选择激活的发布机
|
||||||
hosts = [item for item in self.hosts.filter(is_active=True).all()
|
hosts = [
|
||||||
if item.load != 'offline']
|
host for host in self.hosts.filter(is_active=True)
|
||||||
|
if host.load != 'offline'
|
||||||
|
]
|
||||||
|
|
||||||
if not hosts:
|
if not hosts:
|
||||||
return None
|
return None
|
||||||
|
|
Loading…
Reference in New Issue