jumpserver/apps/accounts/serializers/account/account.py

477 lines
17 KiB
Python

import uuid
from copy import deepcopy
from django.db import IntegrityError
from django.db import transaction
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, GatheredAccount
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 getattr(self.instance, 'asset', None)
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
request = self.context.get('request')
if request and request.method == 'PATCH':
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
@staticmethod
def get_template_attr_for_account(template):
# Set initial data from template
field_names = [
'name', 'username', 'secret',
'secret_type', 'privileged', 'is_active'
]
attrs = {}
for name in field_names:
value = getattr(template, name, None)
if value is None:
continue
attrs[name] = value
attrs['secret'] = template.get_secret()
return attrs
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
attrs = self.get_template_attr_for_account(template)
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)
def push_account_if_need(self, instance, push_now, params, stat):
if not push_now or stat not in ['created', 'updated']:
return
transaction.on_commit(lambda: self.start_push(instance, params))
@staticmethod
def start_push(instance, params):
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'))
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'
).prefetch_related('labels', 'labels__label')
return queryset
class AccountDetailSerializer(AccountSerializer):
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
class Meta(AccountSerializer.Meta):
model = Account
fields = AccountSerializer.Meta.fields + ['has_secret']
read_only_fields = AccountSerializer.Meta.read_only_fields + ['has_secret']
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 = (
('verify', 'verify'),
('push', 'push'),
('remove', 'remove'),
)
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
assets = serializers.PrimaryKeyRelatedField(
queryset=Asset.objects, required=False, allow_empty=True, many=True
)
accounts = serializers.PrimaryKeyRelatedField(
queryset=Account.objects, required=False, allow_empty=True, many=True
)
gather_accounts = serializers.PrimaryKeyRelatedField(
queryset=GatheredAccount.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'}
)