perf: 修改表结构

pull/8873/head
ibuler 2022-09-06 19:57:03 +08:00
parent 984b8dfb28
commit 585ce6b46a
22 changed files with 159 additions and 886 deletions

View File

@ -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')

View File

@ -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,),
),
]

View File

@ -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')

View File

@ -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

View File

@ -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)

View File

@ -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),
),
]

View File

@ -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),
)
),
]

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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')}
}

View File

@ -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'

View File

@ -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

View File

@ -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 ''

View File

@ -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',

View File

@ -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)

View File

@ -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',

View File

@ -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)