mirror of https://github.com/jumpserver/jumpserver
				
				
				
			
		
			
				
	
	
		
			447 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			447 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
import uuid
 | 
						|
from copy import deepcopy
 | 
						|
 | 
						|
from django.db import IntegrityError
 | 
						|
from django.db.models import Q
 | 
						|
from django.utils.translation import gettext_lazy as _
 | 
						|
from rest_framework import serializers
 | 
						|
from rest_framework.generics import get_object_or_404
 | 
						|
from rest_framework.validators import UniqueTogetherValidator
 | 
						|
 | 
						|
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.models import Asset
 | 
						|
from common.serializers import SecretReadableMixin
 | 
						|
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
 | 
						|
from common.utils import get_logger
 | 
						|
from .base import BaseAccountSerializer, AuthValidateMixin
 | 
						|
 | 
						|
logger = get_logger(__name__)
 | 
						|
 | 
						|
 | 
						|
class AccountCreateUpdateSerializerMixin(serializers.Serializer):
 | 
						|
    template = serializers.PrimaryKeyRelatedField(
 | 
						|
        queryset=AccountTemplate.objects, required=False,
 | 
						|
        label=_("Template"), write_only=True, allow_null=True
 | 
						|
    )
 | 
						|
    push_now = serializers.BooleanField(
 | 
						|
        default=False, label=_("Push now"), write_only=True
 | 
						|
    )
 | 
						|
    params = serializers.JSONField(
 | 
						|
        decoder=None, encoder=None, required=False, style={'base_template': 'textarea.html'}
 | 
						|
    )
 | 
						|
    on_invalid = LabeledChoiceField(
 | 
						|
        choices=AccountInvalidPolicy.choices, default=AccountInvalidPolicy.ERROR,
 | 
						|
        write_only=True, allow_null=True, label=_('Exist policy'),
 | 
						|
    )
 | 
						|
    _template = None
 | 
						|
    clean_auth_fields: callable
 | 
						|
 | 
						|
    class Meta:
 | 
						|
        fields = ['template', 'push_now', 'params', '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': UniqueTogetherValidator.missing_message})
 | 
						|
            asset = data.get('asset') or self.instance.asset
 | 
						|
            self.from_template_if_need(data)
 | 
						|
            self.set_uniq_name_if_need(data, asset)
 | 
						|
 | 
						|
    def set_uniq_name_if_need(self, initial_data, asset):
 | 
						|
        name = initial_data.get('name')
 | 
						|
        if name is not None:
 | 
						|
            return
 | 
						|
        if not name:
 | 
						|
            name = initial_data.get('username')
 | 
						|
        if self.instance and self.instance.name == name:
 | 
						|
            return
 | 
						|
        if Account.objects.filter(name=name, asset=asset).exists():
 | 
						|
            name = name + '_' + uuid.uuid4().hex[:4]
 | 
						|
        initial_data['name'] = name
 | 
						|
 | 
						|
    def from_template_if_need(self, initial_data):
 | 
						|
        if isinstance(initial_data, str):
 | 
						|
            return
 | 
						|
 | 
						|
        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'})
 | 
						|
 | 
						|
        self._template = template
 | 
						|
        # Set initial data from template
 | 
						|
        ignore_fields = ['id', 'date_created', 'date_updated', 'su_from', 'org_id']
 | 
						|
        field_names = [
 | 
						|
            field.name for field in template._meta.fields
 | 
						|
            if field.name not in ignore_fields
 | 
						|
        ]
 | 
						|
        attrs = {}
 | 
						|
        for name in field_names:
 | 
						|
            value = getattr(template, name, None)
 | 
						|
            if value is None:
 | 
						|
                continue
 | 
						|
            attrs[name] = value
 | 
						|
        initial_data.update(attrs)
 | 
						|
        initial_data.update({
 | 
						|
            'source': Source.TEMPLATE,
 | 
						|
            'source_id': str(template.id)
 | 
						|
        })
 | 
						|
        asset_id = initial_data.get('asset')
 | 
						|
        if isinstance(asset_id, list) or not asset_id:
 | 
						|
            return
 | 
						|
        asset = get_object_or_404(Asset, pk=asset_id)
 | 
						|
        initial_data['su_from'] = template.get_su_from_account(asset)
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def push_account_if_need(instance, push_now, params, stat):
 | 
						|
        if not push_now or stat not in ['created', 'updated']:
 | 
						|
            return
 | 
						|
        push_accounts_to_assets_task.delay([str(instance.id)], params)
 | 
						|
 | 
						|
    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 and not self.parent:
 | 
						|
            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)
 | 
						|
        params = validated_data.pop('params', None)
 | 
						|
        self.clean_auth_fields(validated_data)
 | 
						|
        instance, stat = self.do_create(validated_data)
 | 
						|
        self.push_account_if_need(instance, push_now, params, 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)
 | 
						|
        params = validated_data.pop('params', None)
 | 
						|
        validated_data['source_id'] = None
 | 
						|
        instance = super().update(instance, validated_data)
 | 
						|
        self.push_account_if_need(instance, push_now, params, 'updated')
 | 
						|
        return instance
 | 
						|
 | 
						|
 | 
						|
class AccountAssetSerializer(serializers.ModelSerializer):
 | 
						|
    platform = ObjectRelatedField(read_only=True)
 | 
						|
    category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
 | 
						|
    type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))
 | 
						|
 | 
						|
    class Meta:
 | 
						|
        model = Asset
 | 
						|
        fields = ['id', 'name', 'address', 'type', 'category', 'platform', 'auto_config']
 | 
						|
 | 
						|
    def to_internal_value(self, data):
 | 
						|
        if isinstance(data, dict):
 | 
						|
            i = data.get('id') or data.get('pk')
 | 
						|
        else:
 | 
						|
            i = data
 | 
						|
 | 
						|
        try:
 | 
						|
            return Asset.objects.get(id=i)
 | 
						|
        except Asset.DoesNotExist:
 | 
						|
            raise serializers.ValidationError(_('Asset not found'))
 | 
						|
 | 
						|
 | 
						|
class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer):
 | 
						|
    asset = AccountAssetSerializer(label=_('Asset'))
 | 
						|
    has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
 | 
						|
    source = LabeledChoiceField(
 | 
						|
        choices=Source.choices, label=_("Source"), required=False,
 | 
						|
        allow_null=True, default=Source.LOCAL
 | 
						|
    )
 | 
						|
    su_from = ObjectRelatedField(
 | 
						|
        required=False, queryset=Account.objects, allow_null=True, allow_empty=True,
 | 
						|
        label=_('Su from'), attrs=('id', 'name', 'username')
 | 
						|
    )
 | 
						|
 | 
						|
    class Meta(BaseAccountSerializer.Meta):
 | 
						|
        model = Account
 | 
						|
        fields = BaseAccountSerializer.Meta.fields + [
 | 
						|
            'su_from', 'asset', 'version',
 | 
						|
            'source', 'source_id', 'connectivity',
 | 
						|
        ] + AccountCreateUpdateSerializerMixin.Meta.fields
 | 
						|
        read_only_fields = BaseAccountSerializer.Meta.read_only_fields + [
 | 
						|
            'connectivity'
 | 
						|
        ]
 | 
						|
        extra_kwargs = {
 | 
						|
            **BaseAccountSerializer.Meta.extra_kwargs,
 | 
						|
            'name': {'required': False},
 | 
						|
            'source_id': {'required': False, 'allow_null': True},
 | 
						|
        }
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def setup_eager_loading(cls, queryset):
 | 
						|
        """ Perform necessary eager loading of data. """
 | 
						|
        queryset = queryset.prefetch_related(
 | 
						|
            'asset', 'asset__platform',
 | 
						|
            'asset__platform__automation'
 | 
						|
        )
 | 
						|
        return queryset
 | 
						|
 | 
						|
 | 
						|
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'))
 | 
						|
 | 
						|
 | 
						|
class AssetAccountBulkSerializer(
 | 
						|
    AccountCreateUpdateSerializerMixin, AuthValidateMixin, serializers.ModelSerializer
 | 
						|
):
 | 
						|
    su_from_username = serializers.CharField(
 | 
						|
        max_length=128, required=False, write_only=True, allow_null=True, label=_("Su from"),
 | 
						|
        allow_blank=True,
 | 
						|
    )
 | 
						|
    assets = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, many=True, label=_('Assets'))
 | 
						|
 | 
						|
    class Meta:
 | 
						|
        model = Account
 | 
						|
        fields = [
 | 
						|
            'name', 'username', 'secret', 'secret_type', 'passphrase',
 | 
						|
            'privileged', 'is_active', 'comment', 'template',
 | 
						|
            'on_invalid', 'push_now', 'assets', 'su_from_username',
 | 
						|
            'source', 'source_id',
 | 
						|
        ]
 | 
						|
        extra_kwargs = {
 | 
						|
            'name': {'required': False},
 | 
						|
            'secret_type': {'required': False},
 | 
						|
            'source': {'required': False, 'allow_null': True},
 | 
						|
            'source_id': {'required': False, 'allow_null': True},
 | 
						|
        }
 | 
						|
 | 
						|
    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_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.get('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 generate_su_from_data(self, validated_data):
 | 
						|
        template = self._template
 | 
						|
        asset = validated_data['asset']
 | 
						|
        su_from = validated_data.get('su_from')
 | 
						|
        su_from_username = validated_data.pop('su_from_username', None)
 | 
						|
        if template:
 | 
						|
            su_from = template.get_su_from_account(asset)
 | 
						|
        elif su_from_username:
 | 
						|
            su_from = asset.accounts.filter(username=su_from_username).first()
 | 
						|
        validated_data['su_from'] = su_from
 | 
						|
 | 
						|
    def perform_create(self, vd, handler):
 | 
						|
        lookup = self.get_filter_lookup(vd)
 | 
						|
        vd = deepcopy(vd)
 | 
						|
        self.generate_su_from_data(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)
 | 
						|
        asset_ids = [asset.id for asset in assets]
 | 
						|
        secret_type_supports = Asset.get_secret_type_assets(asset_ids, 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:
 | 
						|
                self.clean_auth_fields(vd)
 | 
						|
                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 Meta(AccountSerializer.Meta):
 | 
						|
        extra_kwargs = {
 | 
						|
            'secret': {'write_only': False},
 | 
						|
        }
 | 
						|
 | 
						|
 | 
						|
class AccountHistorySerializer(serializers.ModelSerializer):
 | 
						|
    secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type'))
 | 
						|
    id = serializers.IntegerField(label=_('ID'), source='history_id', read_only=True)
 | 
						|
 | 
						|
    class Meta:
 | 
						|
        model = Account.history.model
 | 
						|
        fields = ['id', 'secret', 'secret_type', 'version', 'history_date', 'history_user']
 | 
						|
        read_only_fields = fields
 | 
						|
        extra_kwargs = {
 | 
						|
            'history_user': {'label': _('User')},
 | 
						|
            'history_date': {'label': _('Date')},
 | 
						|
        }
 | 
						|
 | 
						|
 | 
						|
class AccountTaskSerializer(serializers.Serializer):
 | 
						|
    ACTION_CHOICES = (
 | 
						|
        ('test', 'test'),
 | 
						|
        ('verify', 'verify'),
 | 
						|
        ('push', 'push'),
 | 
						|
    )
 | 
						|
    action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
 | 
						|
    accounts = serializers.PrimaryKeyRelatedField(
 | 
						|
        queryset=Account.objects, required=False, allow_empty=True, many=True
 | 
						|
    )
 | 
						|
    task = serializers.CharField(read_only=True)
 | 
						|
    params = serializers.JSONField(
 | 
						|
        decoder=None, encoder=None, required=False,
 | 
						|
        style={'base_template': 'textarea.html'}
 | 
						|
    )
 |