mirror of https://github.com/jumpserver/jumpserver
perf: 修改表结构
parent
984b8dfb28
commit
585ce6b46a
|
@ -1,91 +0,0 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class AppCategory(models.TextChoices):
|
||||
db = 'db', _('Database')
|
||||
remote_app = 'remote_app', _('Remote app')
|
||||
cloud = 'cloud', 'Cloud'
|
||||
|
||||
@classmethod
|
||||
def get_label(cls, category):
|
||||
return dict(cls.choices).get(category, '')
|
||||
|
||||
@classmethod
|
||||
def is_xpack(cls, category):
|
||||
return category in ['remote_app']
|
||||
|
||||
|
||||
class AppType(models.TextChoices):
|
||||
# db category
|
||||
mysql = 'mysql', 'MySQL'
|
||||
mariadb = 'mariadb', 'MariaDB'
|
||||
oracle = 'oracle', 'Oracle'
|
||||
pgsql = 'postgresql', 'PostgreSQL'
|
||||
sqlserver = 'sqlserver', 'SQLServer'
|
||||
redis = 'redis', 'Redis'
|
||||
mongodb = 'mongodb', 'MongoDB'
|
||||
|
||||
# remote-app category
|
||||
chrome = 'chrome', 'Chrome'
|
||||
mysql_workbench = 'mysql_workbench', 'MySQL Workbench'
|
||||
vmware_client = 'vmware_client', 'vSphere Client'
|
||||
custom = 'custom', _('Custom')
|
||||
|
||||
# cloud category
|
||||
k8s = 'k8s', 'Kubernetes'
|
||||
|
||||
@classmethod
|
||||
def category_types_mapper(cls):
|
||||
return {
|
||||
AppCategory.db: [
|
||||
cls.mysql, cls.mariadb, cls.oracle, cls.pgsql,
|
||||
cls.sqlserver, cls.redis, cls.mongodb
|
||||
],
|
||||
AppCategory.remote_app: [
|
||||
cls.chrome, cls.mysql_workbench,
|
||||
cls.vmware_client, cls.custom
|
||||
],
|
||||
AppCategory.cloud: [cls.k8s]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def type_category_mapper(cls):
|
||||
mapper = {}
|
||||
for category, tps in cls.category_types_mapper().items():
|
||||
for tp in tps:
|
||||
mapper[tp] = category
|
||||
return mapper
|
||||
|
||||
@classmethod
|
||||
def get_label(cls, tp):
|
||||
return dict(cls.choices).get(tp, '')
|
||||
|
||||
@classmethod
|
||||
def db_types(cls):
|
||||
return [tp.value for tp in cls.category_types_mapper()[AppCategory.db]]
|
||||
|
||||
@classmethod
|
||||
def remote_app_types(cls):
|
||||
return [tp.value for tp in cls.category_types_mapper()[AppCategory.remote_app]]
|
||||
|
||||
@classmethod
|
||||
def cloud_types(cls):
|
||||
return [tp.value for tp in cls.category_types_mapper()[AppCategory.cloud]]
|
||||
|
||||
@classmethod
|
||||
def is_xpack(cls, tp):
|
||||
tp_category_mapper = cls.type_category_mapper()
|
||||
category = tp_category_mapper[tp]
|
||||
|
||||
if AppCategory.is_xpack(category):
|
||||
return True
|
||||
return tp in ['oracle', 'postgresql', 'sqlserver']
|
||||
|
||||
|
||||
class OracleVersion(models.TextChoices):
|
||||
version_11g = '11g', '11g'
|
||||
version_12c = '12c', '12c'
|
||||
version_other = 'other', _('Other')
|
|
@ -71,6 +71,6 @@ class Migration(migrations.Migration):
|
|||
'verbose_name': 'Account',
|
||||
'unique_together': {('username', 'app', 'systemuser')},
|
||||
},
|
||||
bases=(models.Model, assets.models.base.AuthMixin),
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,320 +0,0 @@
|
|||
from collections import defaultdict
|
||||
from urllib.parse import urlencode, parse_qsl
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from common.mixins import CommonModelMixin
|
||||
from common.tree import TreeNode
|
||||
from common.utils import is_uuid
|
||||
from assets.models import Asset, SystemUser
|
||||
from ..const import OracleVersion
|
||||
|
||||
from ..utils import KubernetesTree
|
||||
from .. import const
|
||||
|
||||
|
||||
class ApplicationTreeNodeMixin:
|
||||
id: str
|
||||
name: str
|
||||
type: str
|
||||
category: str
|
||||
attrs: dict
|
||||
|
||||
@staticmethod
|
||||
def create_tree_id(pid, type, v):
|
||||
i = dict(parse_qsl(pid))
|
||||
i[type] = v
|
||||
tree_id = urlencode(i)
|
||||
return tree_id
|
||||
|
||||
@classmethod
|
||||
def create_choice_node(cls, c, id_, pid, tp, opened=False, counts=None,
|
||||
show_empty=True, show_count=True):
|
||||
count = counts.get(c.value, 0)
|
||||
if count == 0 and not show_empty:
|
||||
return None
|
||||
label = c.label
|
||||
if count is not None and show_count:
|
||||
label = '{} ({})'.format(label, count)
|
||||
data = {
|
||||
'id': id_,
|
||||
'name': label,
|
||||
'title': label,
|
||||
'pId': pid,
|
||||
'isParent': bool(count),
|
||||
'open': opened,
|
||||
'iconSkin': '',
|
||||
'meta': {
|
||||
'type': tp,
|
||||
'data': {
|
||||
'name': c.name,
|
||||
'value': c.value
|
||||
}
|
||||
}
|
||||
}
|
||||
return TreeNode(**data)
|
||||
|
||||
@classmethod
|
||||
def create_root_tree_node(cls, queryset, show_count=True):
|
||||
count = queryset.count() if show_count else None
|
||||
root_id = 'applications'
|
||||
root_name = _('Applications')
|
||||
if count is not None and show_count:
|
||||
root_name = '{} ({})'.format(root_name, count)
|
||||
node = TreeNode(**{
|
||||
'id': root_id,
|
||||
'name': root_name,
|
||||
'title': root_name,
|
||||
'pId': '',
|
||||
'isParent': True,
|
||||
'open': True,
|
||||
'iconSkin': '',
|
||||
'meta': {
|
||||
'type': 'applications_root',
|
||||
}
|
||||
})
|
||||
return node
|
||||
|
||||
@classmethod
|
||||
def create_category_tree_nodes(cls, pid, counts=None, show_empty=True, show_count=True):
|
||||
nodes = []
|
||||
categories = const.AppType.category_types_mapper().keys()
|
||||
for category in categories:
|
||||
if not settings.XPACK_ENABLED and const.AppCategory.is_xpack(category):
|
||||
continue
|
||||
i = cls.create_tree_id(pid, 'category', category.value)
|
||||
node = cls.create_choice_node(
|
||||
category, i, pid=pid, tp='category',
|
||||
counts=counts, opened=False, show_empty=show_empty,
|
||||
show_count=show_count
|
||||
)
|
||||
if not node:
|
||||
continue
|
||||
nodes.append(node)
|
||||
return nodes
|
||||
|
||||
@classmethod
|
||||
def create_types_tree_nodes(cls, pid, counts, show_empty=True, show_count=True):
|
||||
nodes = []
|
||||
temp_pid = pid
|
||||
type_category_mapper = const.AppType.type_category_mapper()
|
||||
types = const.AppType.type_category_mapper().keys()
|
||||
|
||||
for tp in types:
|
||||
if not settings.XPACK_ENABLED and const.AppType.is_xpack(tp):
|
||||
continue
|
||||
category = type_category_mapper.get(tp)
|
||||
pid = cls.create_tree_id(pid, 'category', category.value)
|
||||
i = cls.create_tree_id(pid, 'type', tp.value)
|
||||
node = cls.create_choice_node(
|
||||
tp, i, pid, tp='type', counts=counts, opened=False,
|
||||
show_empty=show_empty, show_count=show_count
|
||||
)
|
||||
pid = temp_pid
|
||||
if not node:
|
||||
continue
|
||||
nodes.append(node)
|
||||
return nodes
|
||||
|
||||
@staticmethod
|
||||
def get_tree_node_counts(queryset):
|
||||
counts = defaultdict(int)
|
||||
values = queryset.values_list('type', 'category')
|
||||
for i in values:
|
||||
tp = i[0]
|
||||
category = i[1]
|
||||
counts[tp] += 1
|
||||
counts[category] += 1
|
||||
return counts
|
||||
|
||||
@classmethod
|
||||
def create_category_type_tree_nodes(cls, queryset, pid, show_empty=True, show_count=True):
|
||||
counts = cls.get_tree_node_counts(queryset)
|
||||
tree_nodes = []
|
||||
|
||||
# 类别的节点
|
||||
tree_nodes += cls.create_category_tree_nodes(
|
||||
pid, counts, show_empty=show_empty,
|
||||
show_count=show_count
|
||||
)
|
||||
|
||||
# 类型的节点
|
||||
tree_nodes += cls.create_types_tree_nodes(
|
||||
pid, counts, show_empty=show_empty,
|
||||
show_count=show_count
|
||||
)
|
||||
return tree_nodes
|
||||
|
||||
@classmethod
|
||||
def create_tree_nodes(cls, queryset, root_node=None, show_empty=True, show_count=True):
|
||||
tree_nodes = []
|
||||
|
||||
# 根节点有可能是组织名称
|
||||
if root_node is None:
|
||||
root_node = cls.create_root_tree_node(queryset, show_count=show_count)
|
||||
tree_nodes.append(root_node)
|
||||
|
||||
tree_nodes += cls.create_category_type_tree_nodes(
|
||||
queryset, root_node.id, show_empty=show_empty, show_count=show_count
|
||||
)
|
||||
|
||||
# 应用的节点
|
||||
for app in queryset:
|
||||
if not settings.XPACK_ENABLED and const.AppType.is_xpack(app.type):
|
||||
continue
|
||||
node = app.as_tree_node(root_node.id)
|
||||
tree_nodes.append(node)
|
||||
return tree_nodes
|
||||
|
||||
def create_app_tree_pid(self, root_id):
|
||||
pid = self.create_tree_id(root_id, 'category', self.category)
|
||||
pid = self.create_tree_id(pid, 'type', self.type)
|
||||
return pid
|
||||
|
||||
def as_tree_node(self, pid, k8s_as_tree=False):
|
||||
if self.type == const.AppType.k8s and k8s_as_tree:
|
||||
node = KubernetesTree(pid).as_tree_node(self)
|
||||
else:
|
||||
node = self._as_tree_node(pid)
|
||||
return node
|
||||
|
||||
def _attrs_to_tree(self):
|
||||
if self.category == const.AppCategory.db:
|
||||
return self.attrs
|
||||
return {}
|
||||
|
||||
def _as_tree_node(self, pid):
|
||||
icon_skin_category_mapper = {
|
||||
'remote_app': 'chrome',
|
||||
'db': 'database',
|
||||
'cloud': 'cloud'
|
||||
}
|
||||
icon_skin = icon_skin_category_mapper.get(self.category, 'file')
|
||||
pid = self.create_app_tree_pid(pid)
|
||||
node = TreeNode(**{
|
||||
'id': str(self.id),
|
||||
'name': self.name,
|
||||
'title': self.name,
|
||||
'pId': pid,
|
||||
'isParent': False,
|
||||
'open': False,
|
||||
'iconSkin': icon_skin,
|
||||
'meta': {
|
||||
'type': 'application',
|
||||
'data': {
|
||||
'category': self.category,
|
||||
'type': self.type,
|
||||
'attrs': self._attrs_to_tree()
|
||||
}
|
||||
}
|
||||
})
|
||||
return node
|
||||
|
||||
|
||||
class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
|
||||
APP_TYPE = const.AppType
|
||||
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
category = models.CharField(
|
||||
max_length=16, choices=const.AppCategory.choices, verbose_name=_('Category')
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=16, choices=const.AppType.choices, verbose_name=_('Type')
|
||||
)
|
||||
domain = models.ForeignKey(
|
||||
'assets.Domain', null=True, blank=True, related_name='applications',
|
||||
on_delete=models.SET_NULL, verbose_name=_("Domain"),
|
||||
)
|
||||
attrs = models.JSONField(default=dict, verbose_name=_('Attrs'))
|
||||
comment = models.TextField(
|
||||
max_length=128, default='', blank=True, verbose_name=_('Comment')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Application')
|
||||
unique_together = [('org_id', 'name')]
|
||||
ordering = ('name',)
|
||||
permissions = [
|
||||
('match_application', _('Can match application')),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
category_display = self.get_category_display()
|
||||
type_display = self.get_type_display()
|
||||
return f'{self.name}({type_display})[{category_display}]'
|
||||
|
||||
@property
|
||||
def category_remote_app(self):
|
||||
return self.category == const.AppCategory.remote_app.value
|
||||
|
||||
@property
|
||||
def category_cloud(self):
|
||||
return self.category == const.AppCategory.cloud.value
|
||||
|
||||
@property
|
||||
def category_db(self):
|
||||
return self.category == const.AppCategory.db.value
|
||||
|
||||
def is_type(self, tp):
|
||||
return self.type == tp
|
||||
|
||||
def get_rdp_remote_app_setting(self):
|
||||
from applications.serializers.attrs import get_serializer_class_by_application_type
|
||||
if not self.category_remote_app:
|
||||
raise ValueError(f"Not a remote app application: {self.name}")
|
||||
serializer_class = get_serializer_class_by_application_type(self.type)
|
||||
fields = serializer_class().get_fields()
|
||||
|
||||
parameters = [self.type]
|
||||
for field_name in list(fields.keys()):
|
||||
if field_name in ['asset']:
|
||||
continue
|
||||
value = self.attrs.get(field_name)
|
||||
if not value:
|
||||
continue
|
||||
if field_name == 'path':
|
||||
value = '\"%s\"' % value
|
||||
parameters.append(str(value))
|
||||
|
||||
parameters = ' '.join(parameters)
|
||||
return {
|
||||
'program': '||jmservisor',
|
||||
'working_directory': '',
|
||||
'parameters': parameters
|
||||
}
|
||||
|
||||
def get_remote_app_asset(self, raise_exception=True):
|
||||
asset_id = self.attrs.get('asset')
|
||||
if is_uuid(asset_id):
|
||||
return Asset.objects.filter(id=asset_id).first()
|
||||
if raise_exception:
|
||||
raise ValueError("Remote App not has asset attr")
|
||||
|
||||
def get_target_ip(self):
|
||||
target_ip = ''
|
||||
if self.category_remote_app:
|
||||
asset = self.get_remote_app_asset()
|
||||
target_ip = asset.ip if asset else target_ip
|
||||
elif self.category_cloud:
|
||||
target_ip = self.attrs.get('cluster')
|
||||
elif self.category_db:
|
||||
target_ip = self.attrs.get('host')
|
||||
return target_ip
|
||||
|
||||
def get_target_protocol_for_oracle(self):
|
||||
""" Oracle 类型需要单独处理,因为要携带版本号 """
|
||||
if not self.is_type(self.APP_TYPE.oracle):
|
||||
return
|
||||
version = self.attrs.get('version', OracleVersion.version_12c)
|
||||
if version == OracleVersion.version_other:
|
||||
return
|
||||
return 'oracle_%s' % version
|
||||
|
||||
|
||||
class ApplicationUser(SystemUser):
|
||||
class Meta:
|
||||
proxy = True
|
||||
verbose_name = _('Application user')
|
|
@ -1,60 +0,0 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from common.utils import get_logger, is_uuid, get_object_or_none
|
||||
from assets.models import Asset
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
__all__ = ['RemoteAppSerializer']
|
||||
|
||||
|
||||
class ExistAssetPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
|
||||
|
||||
def to_internal_value(self, data):
|
||||
instance = super().to_internal_value(data)
|
||||
return str(instance.id)
|
||||
|
||||
def to_representation(self, _id):
|
||||
# _id 是 instance.id
|
||||
if self.pk_field is not None:
|
||||
return self.pk_field.to_representation(_id)
|
||||
# 解决删除资产后,远程应用更新页面会显示资产ID的问题
|
||||
asset = get_object_or_none(Asset, id=_id)
|
||||
if not asset:
|
||||
return None
|
||||
return _id
|
||||
|
||||
|
||||
class RemoteAppSerializer(serializers.Serializer):
|
||||
asset_info = serializers.SerializerMethodField(label=_('Asset Info'))
|
||||
asset = ExistAssetPrimaryKeyRelatedField(
|
||||
queryset=Asset.objects, required=True, label=_("Asset"), allow_null=True
|
||||
)
|
||||
path = serializers.CharField(
|
||||
max_length=128, label=_('Application path'), allow_null=True
|
||||
)
|
||||
|
||||
def validate_asset(self, asset):
|
||||
if not asset:
|
||||
raise serializers.ValidationError(_('This field is required.'))
|
||||
return asset
|
||||
|
||||
@staticmethod
|
||||
def get_asset_info(obj):
|
||||
asset_id = obj.get('asset')
|
||||
if not asset_id or not is_uuid(asset_id):
|
||||
return {}
|
||||
try:
|
||||
asset = Asset.objects.get(id=str(asset_id))
|
||||
except ObjectDoesNotExist as e:
|
||||
logger.error(e)
|
||||
return {}
|
||||
if not asset:
|
||||
return {}
|
||||
asset_info = {'id': str(asset.id), 'hostname': asset.hostname}
|
||||
return asset_info
|
|
@ -1,16 +0,0 @@
|
|||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..application_category import DBSerializer
|
||||
from applications.const import OracleVersion
|
||||
|
||||
__all__ = ['OracleSerializer']
|
||||
|
||||
|
||||
class OracleSerializer(DBSerializer):
|
||||
version = serializers.ChoiceField(
|
||||
choices=OracleVersion.choices, default=OracleVersion.version_12c,
|
||||
allow_null=True, label=_('Version'),
|
||||
help_text=_('Magnus currently supports only 11g and 12c connections')
|
||||
)
|
||||
port = serializers.IntegerField(default=1521, label=_('Port'), allow_null=True)
|
|
@ -1,6 +1,5 @@
|
|||
# Generated by Django 3.2.12 on 2022-07-11 08:59
|
||||
|
||||
import assets.models.base
|
||||
import common.db.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
@ -21,10 +20,7 @@ class Migration(migrations.Migration):
|
|||
name='HistoricalAccount',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('connectivity', models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity')),
|
||||
('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
|
||||
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
|
@ -55,10 +51,7 @@ class Migration(migrations.Migration):
|
|||
name='Account',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('connectivity', models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity')),
|
||||
('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
|
||||
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
|
@ -77,6 +70,5 @@ class Migration(migrations.Migration):
|
|||
'permissions': [('view_accountsecret', 'Can view asset account secret'), ('change_accountsecret', 'Can change asset account secret'), ('view_historyaccount', 'Can view asset history account'), ('view_historyaccountsecret', 'Can view asset history account secret')],
|
||||
'unique_together': {('username', 'asset')},
|
||||
},
|
||||
bases=(models.Model, assets.models.base.AuthMixin, assets.models.protocol.ProtocolMixin),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -17,8 +17,6 @@ class Migration(migrations.Migration):
|
|||
name='AccountTemplate',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('connectivity', models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity')),
|
||||
('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
|
||||
|
@ -34,7 +32,7 @@ class Migration(migrations.Migration):
|
|||
],
|
||||
options={
|
||||
'verbose_name': 'Account template',
|
||||
'unique_together': {('name', 'org_id')},
|
||||
},
|
||||
bases=(models.Model, assets.models.base.AuthMixin),
|
||||
)
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,138 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import F
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from simple_history.models import HistoricalRecords
|
||||
|
||||
from common.utils import lazyproperty, get_logger
|
||||
from .base import BaseUser, AbsConnectivity
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
__all__ = ['AuthBook']
|
||||
|
||||
|
||||
class AuthBook(BaseUser, AbsConnectivity):
|
||||
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset'))
|
||||
systemuser = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user"))
|
||||
version = models.IntegerField(default=1, verbose_name=_('Version'))
|
||||
history = HistoricalRecords()
|
||||
|
||||
auth_attrs = ['username', 'password', 'private_key', 'public_key']
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('AuthBook')
|
||||
unique_together = [('username', 'asset', 'systemuser')]
|
||||
permissions = [
|
||||
('test_authbook', _('Can test asset account connectivity')),
|
||||
('view_assetaccountsecret', _('Can view asset account secret')),
|
||||
('change_assetaccountsecret', _('Can change asset account secret')),
|
||||
('view_assethistoryaccount', _('Can view asset history account')),
|
||||
('view_assethistoryaccountsecret', _('Can view asset history account secret')),
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.auth_snapshot = {}
|
||||
|
||||
def get_or_systemuser_attr(self, attr):
|
||||
val = getattr(self, attr, None)
|
||||
if val:
|
||||
return val
|
||||
if self.systemuser:
|
||||
return getattr(self.systemuser, attr, '')
|
||||
return ''
|
||||
|
||||
def load_auth(self):
|
||||
for attr in self.auth_attrs:
|
||||
value = self.get_or_systemuser_attr(attr)
|
||||
self.auth_snapshot[attr] = [getattr(self, attr), value]
|
||||
setattr(self, attr, value)
|
||||
|
||||
def unload_auth(self):
|
||||
if not self.systemuser:
|
||||
return
|
||||
|
||||
for attr, values in self.auth_snapshot.items():
|
||||
origin_value, loaded_value = values
|
||||
current_value = getattr(self, attr, '')
|
||||
if current_value == loaded_value:
|
||||
setattr(self, attr, origin_value)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.unload_auth()
|
||||
instance = super().save(*args, **kwargs)
|
||||
self.load_auth()
|
||||
return instance
|
||||
|
||||
@property
|
||||
def username_display(self):
|
||||
return self.get_or_systemuser_attr('username') or '*'
|
||||
|
||||
@lazyproperty
|
||||
def systemuser_display(self):
|
||||
if not self.systemuser:
|
||||
return ''
|
||||
return str(self.systemuser)
|
||||
|
||||
@property
|
||||
def smart_name(self):
|
||||
username = self.username_display
|
||||
|
||||
if self.asset:
|
||||
asset = str(self.asset)
|
||||
else:
|
||||
asset = '*'
|
||||
return '{}@{}'.format(username, asset)
|
||||
|
||||
def sync_to_system_user_account(self):
|
||||
if self.systemuser:
|
||||
return
|
||||
matched = AuthBook.objects.filter(
|
||||
asset=self.asset, systemuser__username=self.username
|
||||
)
|
||||
if not matched:
|
||||
return
|
||||
|
||||
for i in matched:
|
||||
i.password = self.password
|
||||
i.private_key = self.private_key
|
||||
i.public_key = self.public_key
|
||||
i.comment = 'Update triggered by account {}'.format(self.id)
|
||||
|
||||
# 不触发post_save信号
|
||||
self.__class__.objects.bulk_update(matched, fields=['password', 'private_key', 'public_key'])
|
||||
|
||||
def remove_asset_admin_user_if_need(self):
|
||||
if not self.asset or not self.systemuser:
|
||||
return
|
||||
if not self.systemuser.is_admin_user or self.asset.admin_user != self.systemuser:
|
||||
return
|
||||
self.asset.admin_user = None
|
||||
self.asset.save()
|
||||
logger.debug('Remove asset admin user: {} {}'.format(self.asset, self.systemuser))
|
||||
|
||||
def update_asset_admin_user_if_need(self):
|
||||
if not self.asset or not self.systemuser:
|
||||
return
|
||||
if not self.systemuser.is_admin_user or self.asset.admin_user == self.systemuser:
|
||||
return
|
||||
self.asset.admin_user = self.systemuser
|
||||
self.asset.save()
|
||||
logger.debug('Update asset admin user: {} {}'.format(self.asset, self.systemuser))
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls):
|
||||
queryset = cls.objects.all() \
|
||||
.annotate(ip=F('asset__ip')) \
|
||||
.annotate(hostname=F('asset__hostname')) \
|
||||
.annotate(platform=F('asset__platform__name')) \
|
||||
.annotate(protocols=F('asset__protocols'))
|
||||
return queryset
|
||||
|
||||
def __str__(self):
|
||||
return self.smart_name
|
||||
|
|
@ -3,20 +3,21 @@
|
|||
#
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from common.db import fields
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
|
||||
from .base import BaseAccount
|
||||
from .protocol import ProtocolMixin
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
||||
|
||||
__all__ = ['SystemUser']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SystemUser(BaseAccount, ProtocolMixin):
|
||||
class SystemUser(OrgModelMixin):
|
||||
LOGIN_AUTO = 'auto'
|
||||
LOGIN_MANUAL = 'manual'
|
||||
LOGIN_MODE_CHOICES = (
|
||||
|
@ -28,6 +29,19 @@ class SystemUser(BaseAccount, ProtocolMixin):
|
|||
common = 'common', _('Common user')
|
||||
admin = 'admin', _('Admin user')
|
||||
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
|
||||
password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
|
||||
private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key'))
|
||||
public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key'))
|
||||
token = models.TextField(default='', verbose_name=_('Token'))
|
||||
|
||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
|
||||
date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
|
||||
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
|
||||
|
||||
username_same_with_user = models.BooleanField(default=False, verbose_name=_("Username same with user"))
|
||||
type = models.CharField(max_length=16, choices=Type.choices, default=Type.common, verbose_name=_('Type'))
|
||||
priority = models.IntegerField(default=81, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
|
@ -37,7 +51,6 @@ class SystemUser(BaseAccount, ProtocolMixin):
|
|||
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
|
||||
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
|
||||
sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root"))
|
||||
token = models.TextField(default='', verbose_name=_('Token'))
|
||||
home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True)
|
||||
system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True)
|
||||
ad_domain = models.CharField(default='', max_length=256)
|
||||
|
|
|
@ -2,13 +2,14 @@ from django.db import models
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from simple_history.models import HistoricalRecords
|
||||
|
||||
from .base import BaseAccount, AbsConnectivity
|
||||
from common.utils import lazyproperty
|
||||
|
||||
from .base import BaseAccount
|
||||
|
||||
__all__ = ['Account', 'AccountTemplate']
|
||||
|
||||
|
||||
class Account(BaseAccount, AbsConnectivity):
|
||||
privileged = models.BooleanField(verbose_name=_("Privileged account"), default=False)
|
||||
class Account(BaseAccount):
|
||||
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset'))
|
||||
version = models.IntegerField(default=0, verbose_name=_('Version'))
|
||||
history = HistoricalRecords()
|
||||
|
@ -23,15 +24,28 @@ class Account(BaseAccount, AbsConnectivity):
|
|||
('view_historyaccountsecret', _('Can view asset history account secret')),
|
||||
]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return "{}({})_{}".format(self.asset_name, self.ip, self.username)
|
||||
|
||||
@lazyproperty
|
||||
def ip(self):
|
||||
return self.asset.ip
|
||||
|
||||
@lazyproperty
|
||||
def asset_name(self):
|
||||
return self.asset.name
|
||||
|
||||
def __str__(self):
|
||||
return '{}@{}'.format(self.username, self.asset.name)
|
||||
|
||||
|
||||
class AccountTemplate(BaseAccount, AbsConnectivity):
|
||||
privileged = models.BooleanField(verbose_name=_("Privileged account"), default=False)
|
||||
|
||||
class AccountTemplate(BaseAccount):
|
||||
class Meta:
|
||||
verbose_name = _('Account template')
|
||||
unique_together = (
|
||||
('name', 'org_id'),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return '{}@{}'.format(self.username, self.name)
|
||||
return self.username
|
||||
|
|
|
@ -78,7 +78,6 @@ class Asset(AbsConnectivity, NodesRelationMixin, JMSOrgBaseModel):
|
|||
nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets',
|
||||
verbose_name=_("Nodes"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
|
||||
|
||||
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
|
||||
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
|
||||
info = models.JSONField(verbose_name='Info', default=dict, blank=True)
|
||||
|
|
|
@ -13,11 +13,10 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from django.conf import settings
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from common.utils import random_string
|
||||
from common.utils import (
|
||||
ssh_key_string_to_obj, ssh_key_gen, get_logger
|
||||
ssh_key_string_to_obj, ssh_key_gen, get_logger,
|
||||
random_string, ssh_pubkey_gen,
|
||||
)
|
||||
from common.utils.encode import ssh_pubkey_gen
|
||||
from common.db import fields
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
||||
|
@ -55,11 +54,29 @@ class AbsConnectivity(models.Model):
|
|||
abstract = True
|
||||
|
||||
|
||||
class AuthMixin:
|
||||
private_key = ''
|
||||
password = ''
|
||||
public_key = ''
|
||||
username = ''
|
||||
class BaseAccount(OrgModelMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
|
||||
password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
|
||||
private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key'))
|
||||
public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key'))
|
||||
token = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Token'))
|
||||
privileged = models.BooleanField(verbose_name=_("Privileged account"), default=False)
|
||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
|
||||
date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
|
||||
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
|
||||
|
||||
ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT"
|
||||
ASSET_USER_CACHE_TIME = 600
|
||||
|
||||
APPS_AMOUNT_CACHE_KEY = "APP_USER_{}_APPS_AMOUNT"
|
||||
APP_USER_CACHE_TIME = 600
|
||||
|
||||
def expire_assets_amount(self):
|
||||
cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id)
|
||||
cache.delete(cache_key)
|
||||
|
||||
@property
|
||||
def ssh_key_fingerprint(self):
|
||||
|
@ -115,18 +132,11 @@ class AuthMixin:
|
|||
pass
|
||||
return None
|
||||
|
||||
def set_auth(self, password=None, private_key=None, public_key=None):
|
||||
def set_auth(self, **kwargs):
|
||||
update_fields = []
|
||||
if password:
|
||||
self.password = password
|
||||
update_fields.append('password')
|
||||
if private_key:
|
||||
self.private_key = private_key
|
||||
update_fields.append('private_key')
|
||||
if public_key:
|
||||
self.public_key = public_key
|
||||
update_fields.append('public_key')
|
||||
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
update_fields.append(k)
|
||||
if update_fields:
|
||||
self.save(update_fields=update_fields)
|
||||
|
||||
|
@ -141,6 +151,7 @@ class AuthMixin:
|
|||
self.password = ''
|
||||
self.private_key = ''
|
||||
self.public_key = ''
|
||||
self.token = ''
|
||||
self.save()
|
||||
|
||||
@staticmethod
|
||||
|
@ -168,33 +179,6 @@ class AuthMixin:
|
|||
public_key=_public_key
|
||||
)
|
||||
|
||||
|
||||
class BaseAccount(OrgModelMixin, AuthMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
|
||||
password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
|
||||
private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key'))
|
||||
public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key'))
|
||||
token = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Token'))
|
||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
|
||||
date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
|
||||
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
|
||||
|
||||
ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT"
|
||||
ASSET_USER_CACHE_TIME = 600
|
||||
|
||||
APPS_AMOUNT_CACHE_KEY = "APP_USER_{}_APPS_AMOUNT"
|
||||
APP_USER_CACHE_TIME = 600
|
||||
|
||||
def get_username(self):
|
||||
return self.username
|
||||
|
||||
def expire_assets_amount(self):
|
||||
cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id)
|
||||
cache.delete(cache_key)
|
||||
|
||||
def _to_secret_json(self):
|
||||
"""Push system user use it"""
|
||||
return {
|
||||
|
@ -203,6 +187,7 @@ class BaseAccount(OrgModelMixin, AuthMixin):
|
|||
'password': self.password,
|
||||
'public_key': self.public_key,
|
||||
'private_key': self.private_key_file,
|
||||
'token': self.token
|
||||
}
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -57,6 +57,7 @@ class Gateway(BaseAccount):
|
|||
class Protocol(models.TextChoices):
|
||||
ssh = 'ssh', 'SSH'
|
||||
|
||||
name = models.CharField(max_length=128, verbose_name='Name')
|
||||
ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
|
||||
port = models.IntegerField(default=22, verbose_name=_('Port'))
|
||||
protocol = models.CharField(choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol"))
|
||||
|
@ -64,6 +65,7 @@ class Gateway(BaseAccount):
|
|||
comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
||||
token = None
|
||||
privileged = None
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
|
@ -5,67 +5,3 @@ from django.utils.translation import gettext_lazy as _
|
|||
class Protocol(models.Model):
|
||||
name = models.CharField(max_length=32, verbose_name=_("Name"))
|
||||
port = models.IntegerField(verbose_name=_("Port"))
|
||||
|
||||
|
||||
class ProtocolMixin:
|
||||
protocol: str
|
||||
|
||||
class Protocol(models.TextChoices):
|
||||
ssh = 'ssh', 'SSH'
|
||||
rdp = 'rdp', 'RDP'
|
||||
telnet = 'telnet', 'Telnet'
|
||||
vnc = 'vnc', 'VNC'
|
||||
mysql = 'mysql', 'MySQL'
|
||||
oracle = 'oracle', 'Oracle'
|
||||
mariadb = 'mariadb', 'MariaDB'
|
||||
postgresql = 'postgresql', 'PostgreSQL'
|
||||
sqlserver = 'sqlserver', 'SQLServer'
|
||||
redis = 'redis', 'Redis'
|
||||
mongodb = 'mongodb', 'MongoDB'
|
||||
k8s = 'k8s', 'K8S'
|
||||
|
||||
SUPPORT_PUSH_PROTOCOLS = [Protocol.ssh, Protocol.rdp]
|
||||
|
||||
ASSET_CATEGORY_PROTOCOLS = [
|
||||
Protocol.ssh, Protocol.rdp, Protocol.telnet, Protocol.vnc
|
||||
]
|
||||
APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS = [
|
||||
Protocol.rdp
|
||||
]
|
||||
APPLICATION_CATEGORY_DB_PROTOCOLS = [
|
||||
Protocol.mysql, Protocol.mariadb, Protocol.oracle,
|
||||
Protocol.postgresql, Protocol.sqlserver,
|
||||
Protocol.redis, Protocol.mongodb
|
||||
]
|
||||
APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [
|
||||
Protocol.k8s
|
||||
]
|
||||
APPLICATION_CATEGORY_PROTOCOLS = [
|
||||
*APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS,
|
||||
*APPLICATION_CATEGORY_DB_PROTOCOLS,
|
||||
*APPLICATION_CATEGORY_CLOUD_PROTOCOLS
|
||||
]
|
||||
|
||||
@property
|
||||
def is_protocol_support_push(self):
|
||||
return self.protocol in self.SUPPORT_PUSH_PROTOCOLS
|
||||
|
||||
@classmethod
|
||||
def get_protocol_by_application_type(cls, app_type):
|
||||
from applications.const import AppType
|
||||
if app_type in cls.APPLICATION_CATEGORY_PROTOCOLS:
|
||||
protocol = app_type
|
||||
elif app_type in AppType.remote_app_types():
|
||||
protocol = cls.Protocol.rdp
|
||||
else:
|
||||
protocol = None
|
||||
return protocol
|
||||
|
||||
@property
|
||||
def can_perm_to_asset(self):
|
||||
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
|
||||
|
||||
@property
|
||||
def is_asset_protocol(self):
|
||||
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
|
||||
|
||||
|
|
|
@ -4,29 +4,58 @@ from rest_framework import serializers
|
|||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from common.drf.serializers import SecretReadableMixin
|
||||
from assets.models import Account
|
||||
from assets.serializers.base import AuthSerializerMixin
|
||||
from .account_template import AccountTemplateSerializerMixin
|
||||
from .common import BaseAccountSerializer
|
||||
from assets.models import Account, AccountTemplate
|
||||
from assets.serializers.base import AuthValidateMixin
|
||||
from .common import AccountFieldsSerializerMixin
|
||||
|
||||
|
||||
class AccountSerializer(
|
||||
AccountTemplateSerializerMixin,
|
||||
AuthSerializerMixin,
|
||||
BulkOrgResourceModelSerializer
|
||||
):
|
||||
class AccountSerializerCreateMixin(serializers.ModelSerializer):
|
||||
template = serializers.UUIDField(
|
||||
required=False, allow_null=True, write_only=True,
|
||||
label=_('Account template')
|
||||
)
|
||||
push_to_asset = serializers.BooleanField(default=False, label=_("Push to asset"), write_only=True)
|
||||
|
||||
@staticmethod
|
||||
def validate_template(value):
|
||||
AccountTemplate.objects.get_or_create()
|
||||
model = AccountTemplate
|
||||
try:
|
||||
return model.objects.get(id=value)
|
||||
except AccountTemplate.DoesNotExist:
|
||||
raise serializers.ValidationError(_('Account template not found'))
|
||||
|
||||
@staticmethod
|
||||
def replace_attrs(account_template: AccountTemplate, attrs: dict):
|
||||
exclude_fields = [
|
||||
'_state', 'org_id', 'date_verified', 'id',
|
||||
'date_created', 'date_updated', 'created_by'
|
||||
]
|
||||
template_attrs = {k: v for k, v in account_template.__dict__.items() if k not in exclude_fields}
|
||||
for k, v in template_attrs.items():
|
||||
attrs.setdefault(k, v)
|
||||
|
||||
def validate(self, attrs):
|
||||
account_template = attrs.pop('template', None)
|
||||
if account_template:
|
||||
self.replace_attrs(account_template, attrs)
|
||||
push_to_asset = attrs.pop('push_to_asset', False)
|
||||
return super().validate(attrs)
|
||||
|
||||
|
||||
class AccountSerializer(AuthValidateMixin,
|
||||
AccountSerializerCreateMixin,
|
||||
AccountFieldsSerializerMixin,
|
||||
BulkOrgResourceModelSerializer):
|
||||
name = serializers.CharField(max_length=128, read_only=True, label=_("Name"))
|
||||
ip = serializers.ReadOnlyField(label=_("IP"))
|
||||
asset_name = serializers.ReadOnlyField(label=_("Asset"))
|
||||
platform = serializers.ReadOnlyField(label=_("Platform"))
|
||||
|
||||
class Meta(BaseAccountSerializer.Meta):
|
||||
class Meta(AccountFieldsSerializerMixin.Meta):
|
||||
model = Account
|
||||
fields = BaseAccountSerializer.Meta.fields + ['account_template', ]
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs = self._validate_gen_key(attrs)
|
||||
attrs = super()._validate(attrs)
|
||||
return attrs
|
||||
fields = AccountFieldsSerializerMixin.Meta.fields \
|
||||
+ ['template', 'push_to_asset']
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
|
@ -47,7 +76,6 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
|||
'password': {'write_only': False},
|
||||
'private_key': {'write_only': False},
|
||||
'public_key': {'write_only': False},
|
||||
'systemuser_display': {'label': _('System user display')}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
|
||||
from assets.models import Account
|
||||
from common.drf.serializers import SecretReadableMixin
|
||||
from .common import BaseAccountSerializer
|
||||
from .common import AccountFieldsSerializerMixin
|
||||
from .account import AccountSerializer, AccountSecretSerializer
|
||||
|
||||
|
||||
class AccountHistorySerializer(AccountSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Account.history.model
|
||||
fields = BaseAccountSerializer.Meta.fields_mini + \
|
||||
BaseAccountSerializer.Meta.fields_write_only + \
|
||||
BaseAccountSerializer.Meta.fields_fk + \
|
||||
['history_id', 'date_created', 'date_updated']
|
||||
fields = AccountFieldsSerializerMixin.Meta.fields_mini + \
|
||||
AccountFieldsSerializerMixin.Meta.fields_write_only + \
|
||||
AccountFieldsSerializerMixin.Meta.fields_fk + \
|
||||
['history_id', 'date_created', 'date_updated']
|
||||
read_only_fields = fields
|
||||
ref_name = 'AccountHistorySerializer'
|
||||
|
||||
|
|
|
@ -3,16 +3,16 @@ from rest_framework import serializers
|
|||
|
||||
from assets.models import AccountTemplate
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from assets.serializers.base import AuthSerializerMixin
|
||||
from .common import BaseAccountSerializer
|
||||
from assets.serializers.base import AuthValidateMixin
|
||||
from .common import AccountFieldsSerializerMixin
|
||||
|
||||
|
||||
class AccountTemplateSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
class AccountTemplateSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
||||
class Meta:
|
||||
model = AccountTemplate
|
||||
fields_mini = ['id', 'privileged', 'username', 'name']
|
||||
fields_write_only = BaseAccountSerializer.Meta.fields_write_only
|
||||
fields_other = BaseAccountSerializer.Meta.fields_other
|
||||
fields_mini = ['id', 'privileged', 'username']
|
||||
fields_write_only = AccountFieldsSerializerMixin.Meta.fields_write_only
|
||||
fields_other = AccountFieldsSerializerMixin.Meta.fields_other
|
||||
fields = fields_mini + fields_write_only + fields_other
|
||||
extra_kwargs = {
|
||||
'username': {'required': True},
|
||||
|
@ -35,39 +35,3 @@ class AccountTemplateSerializer(AuthSerializerMixin, BulkOrgResourceModelSeriali
|
|||
return
|
||||
raise serializers.ValidationError(required_field_dict)
|
||||
|
||||
|
||||
class AccountTemplateSerializerMixin(serializers.ModelSerializer):
|
||||
account_template = serializers.UUIDField(
|
||||
required=False, allow_null=True, write_only=True,
|
||||
label=_('Account template')
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_account_template(value):
|
||||
AccountTemplate.objects.get_or_create()
|
||||
model = AccountTemplate
|
||||
try:
|
||||
return model.objects.get(id=value)
|
||||
except AccountTemplate.DoesNotExist:
|
||||
raise serializers.ValidationError(_('Account template not found'))
|
||||
|
||||
@staticmethod
|
||||
def replace_attrs(account_template: AccountTemplate, attrs: dict):
|
||||
exclude_fields = [
|
||||
'_state', 'org_id', 'date_verified', 'id', 'date_created', 'date_updated', 'created_by'
|
||||
]
|
||||
template_attrs = {k: v for k, v in account_template.__dict__.items() if k not in exclude_fields}
|
||||
for k, v in template_attrs.items():
|
||||
attrs.setdefault(k, v)
|
||||
|
||||
def _validate(self, attrs):
|
||||
account_template = attrs.pop('account_template', None)
|
||||
if account_template:
|
||||
self.replace_attrs(account_template, attrs)
|
||||
else:
|
||||
AccountTemplateSerializer.validate_required(attrs)
|
||||
return attrs
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -2,25 +2,26 @@
|
|||
#
|
||||
from rest_framework import serializers
|
||||
|
||||
__all__ = [
|
||||
'BaseAccountSerializer',
|
||||
]
|
||||
__all__ = ['AccountFieldsSerializerMixin']
|
||||
|
||||
|
||||
class BaseAccountSerializer(serializers.ModelSerializer):
|
||||
class AccountFieldsSerializerMixin(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
fields_mini = [
|
||||
'id', 'privileged', 'username', 'ip', 'asset_name',
|
||||
'platform', 'version'
|
||||
'id', 'name', 'username', 'privileged', 'ip',
|
||||
'asset_name', 'platform', 'version'
|
||||
]
|
||||
fields_write_only = ['password', 'private_key', 'public_key', 'passphrase']
|
||||
fields_other = ['date_created', 'date_updated', 'connectivity', 'date_verified', 'comment']
|
||||
fields_other = ['date_created', 'date_updated', 'comment']
|
||||
fields_small = fields_mini + fields_write_only + fields_other
|
||||
fields_fk = ['asset']
|
||||
fields = fields_small + fields_fk
|
||||
ref_name = 'AssetAccountSerializer'
|
||||
extra_kwargs = {
|
||||
'username': {'required': True},
|
||||
'private_key': {'write_only': True},
|
||||
'public_key': {'write_only': True},
|
||||
}
|
||||
|
||||
def validate_name(self, value):
|
||||
if not value:
|
||||
return self.initial_data.get('username')
|
||||
return ''
|
||||
|
|
|
@ -67,8 +67,7 @@ class AssetSerializer(JMSWritableNestedModelSerializer):
|
|||
'domain', 'platform', 'platform',
|
||||
]
|
||||
fields_m2m = [
|
||||
'nodes', 'labels', 'accounts', 'protocols',
|
||||
'nodes_display',
|
||||
'nodes', 'labels', 'accounts', 'protocols', 'nodes_display',
|
||||
]
|
||||
read_only_fields = [
|
||||
'category', 'type', 'connectivity', 'date_verified',
|
||||
|
|
|
@ -11,44 +11,20 @@ from assets.models import Type
|
|||
from .utils import validate_password_for_ansible
|
||||
|
||||
|
||||
class AuthSerializer(serializers.ModelSerializer):
|
||||
password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024, label=_('Password'))
|
||||
private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=16384,
|
||||
label=_('Private key'))
|
||||
|
||||
def gen_keys(self, private_key=None, password=None):
|
||||
if private_key is None:
|
||||
return None, None
|
||||
public_key = ssh_pubkey_gen(private_key=private_key, password=password)
|
||||
return private_key, public_key
|
||||
|
||||
def save(self, **kwargs):
|
||||
password = self.validated_data.pop('password', None) or None
|
||||
private_key = self.validated_data.pop('private_key', None) or None
|
||||
self.instance = super().save(**kwargs)
|
||||
if password or private_key:
|
||||
private_key, public_key = self.gen_keys(private_key, password)
|
||||
self.instance.set_auth(password=password, private_key=private_key,
|
||||
public_key=public_key)
|
||||
return self.instance
|
||||
|
||||
|
||||
class AuthSerializerMixin(serializers.ModelSerializer):
|
||||
class AuthValidateMixin(serializers.Serializer):
|
||||
password = EncryptedField(
|
||||
label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024,
|
||||
validators=[validate_password_for_ansible]
|
||||
label=_('Password'), required=False, allow_blank=True, allow_null=True,
|
||||
max_length=1024, validators=[validate_password_for_ansible]
|
||||
)
|
||||
private_key = EncryptedField(
|
||||
label=_('SSH private key'), required=False, allow_blank=True, allow_null=True, max_length=16384
|
||||
label=_('SSH private key'), required=False, allow_blank=True,
|
||||
allow_null=True, max_length=16384
|
||||
)
|
||||
passphrase = serializers.CharField(
|
||||
allow_blank=True, allow_null=True, required=False, max_length=512,
|
||||
write_only=True, label=_('Key password')
|
||||
)
|
||||
|
||||
def validate_password(self, password):
|
||||
return password
|
||||
|
||||
def validate_private_key(self, private_key):
|
||||
if not private_key:
|
||||
return
|
||||
|
@ -64,9 +40,6 @@ class AuthSerializerMixin(serializers.ModelSerializer):
|
|||
private_key = string_io.getvalue()
|
||||
return private_key
|
||||
|
||||
def validate_public_key(self, public_key):
|
||||
return public_key
|
||||
|
||||
@staticmethod
|
||||
def clean_auth_fields(validated_data):
|
||||
for field in ('password', 'private_key', 'public_key'):
|
||||
|
@ -87,6 +60,10 @@ class AuthSerializerMixin(serializers.ModelSerializer):
|
|||
attrs['public_key'] = public_key
|
||||
return attrs
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs = self._validate_gen_key(attrs)
|
||||
return super().validate(attrs)
|
||||
|
||||
def create(self, validated_data):
|
||||
self.clean_auth_fields(validated_data)
|
||||
return super().create(validated_data)
|
||||
|
@ -97,9 +74,9 @@ class AuthSerializerMixin(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class TypesField(serializers.MultipleChoiceField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['choices'] = Type.CHOICES
|
||||
super().__init__(*args, **kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_representation(self, value):
|
||||
return Type.value_to_choices(value)
|
||||
|
|
|
@ -7,7 +7,7 @@ from common.validators import alphanumeric
|
|||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from common.drf.serializers import SecretReadableMixin
|
||||
from ..models import Domain, Gateway
|
||||
from .base import AuthSerializerMixin
|
||||
from .base import AuthValidateMixin
|
||||
|
||||
|
||||
class DomainSerializer(BulkOrgResourceModelSerializer):
|
||||
|
@ -38,17 +38,17 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
|
|||
return obj.gateway_set.all().count()
|
||||
|
||||
|
||||
class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
class GatewaySerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
||||
is_connective = serializers.BooleanField(required=False, label=_('Connectivity'))
|
||||
|
||||
class Meta:
|
||||
model = Gateway
|
||||
fields_mini = ['id', 'name']
|
||||
fields_mini = ['id', 'username']
|
||||
fields_write_only = [
|
||||
'password', 'private_key', 'public_key', 'passphrase'
|
||||
]
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'username', 'ip', 'port', 'protocol',
|
||||
'ip', 'port', 'protocol',
|
||||
'is_active', 'is_connective',
|
||||
'date_created', 'date_updated',
|
||||
'created_by', 'comment',
|
||||
|
|
|
@ -2,7 +2,6 @@ from django.db import models
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from perms.models import Action
|
||||
from applications.const import AppCategory, AppType
|
||||
from .general import Ticket
|
||||
|
||||
__all__ = ['ApplyApplicationTicket']
|
||||
|
@ -12,10 +11,10 @@ class ApplyApplicationTicket(Ticket):
|
|||
apply_permission_name = models.CharField(max_length=128, verbose_name=_('Permission name'))
|
||||
# 申请信息
|
||||
apply_category = models.CharField(
|
||||
max_length=16, choices=AppCategory.choices, verbose_name=_('Category')
|
||||
max_length=16, verbose_name=_('Category')
|
||||
)
|
||||
apply_type = models.CharField(
|
||||
max_length=16, choices=AppType.choices, verbose_name=_('Type')
|
||||
max_length=16, verbose_name=_('Type')
|
||||
)
|
||||
apply_applications = models.ManyToManyField(
|
||||
'applications.Application', verbose_name=_('Apply applications'),
|
||||
|
@ -29,14 +28,6 @@ class ApplyApplicationTicket(Ticket):
|
|||
apply_date_start = models.DateTimeField(verbose_name=_('Date start'), null=True)
|
||||
apply_date_expired = models.DateTimeField(verbose_name=_('Date expired'), null=True)
|
||||
|
||||
@property
|
||||
def apply_category_display(self):
|
||||
return AppCategory.get_label(self.apply_category)
|
||||
|
||||
@property
|
||||
def apply_type_display(self):
|
||||
return AppType.get_label(self.apply_type)
|
||||
|
||||
@property
|
||||
def apply_actions_display(self):
|
||||
return Action.value_to_choices_display(self.apply_actions)
|
||||
|
|
Loading…
Reference in New Issue