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

422 lines
15 KiB
Python
Raw Normal View History

import uuid
from django.db import IntegrityError
from django.db.models import Q
2022-08-24 08:36:42 +00:00
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
from accounts.const import SecretType, Source, AccountInvalidPolicy
v3.0.0-rc1 (#9322) * perf:automation * pref: 修改账号推送 * perf: 修改 assets * perf: 修改 accounts * feat: 优化代码 * fix: 修复 ObjectRelatedField 获取 value attr 时先判断是否有 attr 属性 * perf: 增加翻译 * feat: 增加部分翻译 * feat: 去除无用列 * perf: ticket remove app * fix: 修复创建账号备份任务失败的问题 * perf: 添加 accounts app * perf: ticket type serializer (#9252) Co-authored-by: feng <1304903146@qq.com> * perf: ticket * perf: 修改 accounts api * perf: 优化 AssetPermissionSerializer fields 顺序 * perf: 修改 accounts * feat: 限制常用用户名api返回长度 * feat: 限制常用用户名api返回长度 * perf: 修改 LoginAssetACL 序列类,增加 users_username_group, accounts_username_group... 字段 * perf: 修改 CommandFilterACLSerializer 增加 command_groups_amount 字段 * perf: 修改rbac API啥的 (#9254) * perf: migrate * perf: 修改 AssetPermedSerializer domain 字段类型 * perf: 放开push account 权限位 * perf: 修改 accounts * perf: 修改 LoginACLSerializer 字段类型 * pref: 修改数据库 migrations * perf: filter asset systemuser * perf: 修改 SessionSerializer 字段类型 * pref: 修改 applet host * perf: 修改 SessionCommandSerializer 字段类型 * perf: 修改 accounts import * perf: 修改 celery datetime * perf: 修改 asset serializer * pref: 修改 labeled field * feat: 修改翻译 * perf: 修改 JobSerializer 字段类型 * feat: 支持使用 ws 发送终断任务 * perf: add AccessTokenAuthentication * perf: 修改 BaseStorageSerializer 字段类型 * perf: 修改 AppletHostSerializer 字段类型 * perf: signal event * perf: asset types automations (#9259) Co-authored-by: feng <1304903146@qq.com> * perf: 修改下载 rdp 文件时返回的 address 地址信息为空的问题 * perf: 修改 AssetSerializer.accounts.secret 为 write_only; 修改 DomainWithGatewaySerializer.gateways 返回 account 信息及 secret 字段; * perf: automation 干库 (#9260) Co-authored-by: feng <1304903146@qq.com> * perf: account push api * feat: 修改迁移文件 * feat: 删除无用代码 * feat: 优化部分资源无操作日志 * perf: 修改 account * perf: perm tree * perf: asset serializers retrieve * perf: 格式化代码 * perf: AutomationExecution (#9268) Co-authored-by: feng <1304903146@qq.com> * perf: AssetDetailSerializer 和 Asset Model 添加 specific_info 字段; * perf: 修改账号推送 * feat: handle ws heartbeat status * perf: k8s tree (#9269) Co-authored-by: feng <1304903146@qq.com> * perf: 修改账号推送 * perf: 修改 asset detail serializer * fix: 修复 windows 不能运行 powershell 命令的问题 * feat: 支持按照资源时间线查看操作活动 * feat: 翻译 * feat: 优化操作日志 * perf: asset clone * fix: 错误的修改改回去 * perf: create asset account * feat: 增加task 刷新续传功能 * fix: applet host deloypment filter host * perf: 修改了 common 结构,和 push accounts * perf: 整理 common 结构 * perf: 修改 const import * perf: 修改 allow bulk destroy * fix: applet host search fileds * perf: applet bulk delete * fix: applet list 404 * perf: 修改 common view * feat: 增加一些翻译, 修复 playbook 上传的错误 * fix: 修改错别字 * perf: 修改 applets status * perf: 修改网关 api * perf: automateion (#9281) Co-authored-by: feng <1304903146@qq.com> Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com> * perf: 失效 connect methods 当 applet 删除 或者 host 删除 * perf: 网关账号的密码类型改成 LabelField * perf: chrome applet script * perf: verify code ttl (#9282) Co-authored-by: feng <1304903146@qq.com> * perf: database ping * perf: ws * perf: 修改网关创建 * perf: account task org (#9285) Co-authored-by: feng <1304903146@qq.com> * perf: asset test api * perf: port 添加 account * pref: 修改 db mapper permission * fix: db port mapper list api * perf: account change secret (#9286) Co-authored-by: feng <1304903146@qq.com> * perf: 修改 setup_eager_loading * perf: SecretStrategy * feat: 修改 ConnectionToken Create API 支持校验 ACL 逻辑 * feat: 修改 ConnectionToken Create API 支持校验 ACL 逻辑 * feat: 修改 ConnectionToken Create API 支持校验 ACL 逻辑 * pref: web database 信号转发 * perf: account push automation * perf: push filter account * perf: 修改 publish 版本 * perf: 修改网关 * fix: 修改资产 Specific 信息中 JSONField 字段返回 json.loads 对象 * feat: 远程应用内置Navicat Premium 16 * feat: 更新下载链接 * feat: 整理代码格式 * perf: 修改 terminal point * perf: update chrome applet script * fix: 资产 specific 获取 JSONField 时, 判断值的类型不为 list, dict * perf: domain (#9292) Co-authored-by: feng <1304903146@qq.com> * perf: 优化 endpoint 监听端口,仅 oracle 动态 * perf: 修改翻译 * perf: 修改文案 * perf: 修改缺失的翻译 * perf: 修改 endpoint help text * feat: 还原格式 * feat: 去掉基类 * feat: 增加特权账号字段 * perf: decode content * fix: check pid * perf: 修改 smart endpoint * perf: 修改 endpoint mysql default port * feat: 优化 * perf: 修改 endpoint mysql default port * perf: gateway test (#9295) Co-authored-by: feng <1304903146@qq.com> * perf: migrate * perf: 修改 endpoint mysql default port * fix: 修复获取任务执行结果死循环 * feat: 作业审计日志增加字段 * fix: add on_transaction_commit task post save * perf: gateway (#9297) Co-authored-by: feng <1304903146@qq.com> * feat: 过滤 jumpserver 自动产生的用户 * fix: 修复ops节点选择的问题 * fix: 修改 统一 connection-token 和 command 的 review API 返回数据 from_ticket_info * perf: change secret (#9298) Co-authored-by: feng <1304903146@qq.com> * perf: 修改 db port manager * perf: 修改 db port manager * perf: add celery log mark * perf: remove debug log data * fix: navicat use manual type * fix: remove navicate download url * perf: push_account_enabled (#9301) Co-authored-by: feng <1304903146@qq.com> * fix: 修改navicat启动程序MD5值 * perf: push account (#9303) Co-authored-by: feng <1304903146@qq.com> * feat: Redis/MongoDB 支持SSL * fix: 修改授权规则过滤字段 node_name,node_id; 修复获取授权节点下的资产为空的问题; * perf: push account button (#9305) Co-authored-by: feng <1304903146@qq.com> * perf: account push * fix: 修复获取 /user//assets/tree/ 返回用户授权的所有资产 * perf: asset ping (#9307) Co-authored-by: feng <1304903146@qq.com> * perf: asset enabled_info * perf: 优化activity记录都保存至operatelog中 * feat: 远程应用navicat支持试用版连接 * perf: 优化迁移文件 * perf: 修改资产列表 API category type 字段 choices 根据 category 进行返回 * fix * perf: 修改账号列表 API 解决根据 node_id asset_id 搜索账号列表无效的问题 * fix: navicat dba账号登录 * perf: 优化navicat连接 * perf: 修改账号列表 Model Manager 继承自 OrgManager,解决组织过滤问题 * perf: 修改账号列表 Filter 支持根据 platform,category,type 字段搜索 * perf: change secret email (#9312) Co-authored-by: feng <1304903146@qq.com> * feat: 保证认证信息一定清理 * perf: add mariadb * perf: 修改资产类型树数量统计资产或账号 * perf: applet chrome quit * perf: 优化关闭欢迎页面 * fix * perf: executed amount * perf: 修改 built-in applet installation * perf: 修改资产列表增加标签搜索 * perf: 修改资产列表增加标签搜索 * perf: account task automation (#9319) Co-authored-by: feng <1304903146@qq.com> * perf: account trigger * perf: 修改系统设置文案:批量命令执行 -> 作业中心 * perf: 优化migrate (#9320) Co-authored-by: feng <1304903146@qq.com> * perf: 修改资产节点树 API,支持搜索资产、节点 * perf: audit dashboard (#9321) Co-authored-by: feng <1304903146@qq.com> * fix: 修改 has_perm 权限判断兼容 list 和 str 类型 * perf: 修改一些换行 * perf: 修改 ansible config * fix: oracle依赖文件地址错误 (#9324) * perf: ansible mudules * perf: 修改 runner host cwd Co-authored-by: ibuler <ibuler@qq.com> Co-authored-by: Aaron3S <chenyang@fit2cloud.com> Co-authored-by: Bai <baijiangjie@gmail.com> Co-authored-by: feng <1304903146@qq.com> Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com> Co-authored-by: Eric <xplzv@126.com> Co-authored-by: jiangweidong <weidong.jiang@fit2cloud.com> Co-authored-by: jiangweidong <80373698+Hi-JWD@users.noreply.github.com>
2023-01-16 11:02:09 +00:00
from accounts.models import Account, AccountTemplate
2023-02-20 10:00:29 +00:00
from accounts.tasks import push_accounts_to_assets_task
from assets.const import Category, AllTypes
2023-02-09 12:48:25 +00:00
from assets.models import Asset
from common.serializers import SecretReadableMixin
2023-02-09 12:48:25 +00:00
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
)
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, label=_('Exist policy')
)
_template = None
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 to_internal_value(self, data):
self.from_template_if_need(data)
return super().to_internal_value(data)
def set_uniq_name_if_need(self, initial_data, asset):
name = initial_data.get('name')
2023-04-18 08:29:41 +00:00
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', '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)
@staticmethod
def push_account_if_need(instance, push_now, params, stat):
2023-04-14 07:34:56 +00:00
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 generate_source_data(self, validated_data):
template = self._template
if template is None:
return
validated_data['source'] = Source.TEMPLATE
validated_data['source_id'] = str(template.id)
def create(self, validated_data):
push_now = validated_data.pop('push_now', None)
params = validated_data.pop('params', None)
self.generate_source_data(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
2022-12-21 10:36:15 +00:00
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'))
2022-12-21 10:36:15 +00:00
class Meta:
model = Asset
fields = ['id', 'name', 'address', 'type', 'category', 'platform', 'auto_config']
2022-12-21 10:36:15 +00:00
def to_internal_value(self, data):
if isinstance(data, dict):
2023-03-17 10:44:21 +00:00
i = data.get('id') or data.get('pk')
2022-12-21 10:36:15 +00:00
else:
i = data
try:
return Asset.objects.get(id=i)
except Asset.DoesNotExist:
raise serializers.ValidationError(_('Asset not found'))
class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer):
2022-12-21 10:36:15 +00:00
asset = AccountAssetSerializer(label=_('Asset'))
2022-12-27 09:54:34 +00:00
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')
)
2022-09-23 10:59:19 +00:00
class Meta(BaseAccountSerializer.Meta):
2022-07-15 10:03:32 +00:00
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 + [
'source', 'source_id', 'connectivity'
]
2022-09-23 10:59:19 +00:00
extra_kwargs = {
**BaseAccountSerializer.Meta.extra_kwargs,
'name': {'required': False},
2022-09-23 10:59:19 +00:00
}
@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
):
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_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 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)
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:
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)
self.generate_source_data(validated_data)
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 = {
2022-10-18 12:37:17 +00:00
'secret': {'write_only': False},
}
2022-10-20 08:39:11 +00:00
class AccountHistorySerializer(serializers.ModelSerializer):
secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type'))
2023-04-19 02:37:46 +00:00
id = serializers.IntegerField(label=_('ID'), source='history_id', read_only=True)
2022-10-20 08:39:11 +00:00
class Meta:
model = Account.history.model
2023-04-13 11:19:20 +00:00
fields = ['id', 'secret', 'secret_type', 'version', 'history_date', 'history_user']
2022-10-20 08:39:11 +00:00
read_only_fields = fields
extra_kwargs = {
'history_user': {'label': _('User')},
'history_date': {'label': _('Date')},
}
2022-10-20 08:39:11 +00:00
class AccountTaskSerializer(serializers.Serializer):
ACTION_CHOICES = (
('test', 'test'),
2023-02-20 10:00:29 +00:00
('verify', 'verify'),
('push', 'push'),
)
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
2023-02-20 15:50:33 +00:00
accounts = serializers.PrimaryKeyRelatedField(
queryset=Account.objects, required=False, allow_empty=True, many=True
)
task = serializers.CharField(read_only=True)
2023-04-14 07:34:56 +00:00
params = serializers.JSONField(
decoder=None, encoder=None, required=False,
style={'base_template': 'textarea.html'}
)