perf: merge v3

pull/8873/head
ibuler 2022-08-23 10:26:43 +08:00
commit 1e57a0eb1f
26 changed files with 166 additions and 55 deletions

View File

@ -7,5 +7,6 @@ from .node import *
from .domain import * from .domain import *
from .gathered_user import * from .gathered_user import *
from .favorite_asset import * from .favorite_asset import *
from .account_template import *
from .account_backup import * from .account_backup import *
from .account_history import * from .account_history import *

View File

@ -0,0 +1,12 @@
from orgs.mixins.api import OrgBulkModelViewSet
from ..models import AccountTemplate
from .. import serializers
class AccountTemplateViewSet(OrgBulkModelViewSet):
model = AccountTemplate
filterset_fields = ("username", 'name')
search_fields = ('username', 'name')
serializer_classes = {
'default': serializers.AccountTemplateSerializer
}

View File

@ -16,5 +16,5 @@ class GatheredUserViewSet(OrgModelViewSet):
serializer_class = GatheredUserSerializer serializer_class = GatheredUserSerializer
extra_filter_backends = [AssetRelatedByNodeFilterBackend] extra_filter_backends = [AssetRelatedByNodeFilterBackend]
filterset_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id'] filterset_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__name', 'asset_id']
search_fields = ['username', 'asset__ip', 'asset__hostname'] search_fields = ['username', 'asset__ip', 'asset__name']

View File

@ -1,6 +1,6 @@
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from common.db.models import IncludesTextChoicesMeta from common.db.models import IncludesTextChoicesMeta, ChoicesMixin
from common.tree import TreeNode from common.tree import TreeNode
@ -24,7 +24,7 @@ class PlatformMixin:
} }
class Category(PlatformMixin, models.TextChoices): class Category(PlatformMixin, ChoicesMixin, models.TextChoices):
HOST = 'host', _('Host') HOST = 'host', _('Host')
NETWORK = 'network', _("NetworkDevice") NETWORK = 'network', _("NetworkDevice")
DATABASE = 'database', _("Database") DATABASE = 'database', _("Database")
@ -60,7 +60,7 @@ class Category(PlatformMixin, models.TextChoices):
} }
class HostTypes(PlatformMixin, models.TextChoices): class HostTypes(PlatformMixin, ChoicesMixin, models.TextChoices):
LINUX = 'linux', 'Linux' LINUX = 'linux', 'Linux'
WINDOWS = 'windows', 'Windows' WINDOWS = 'windows', 'Windows'
UNIX = 'unix', 'Unix' UNIX = 'unix', 'Unix'
@ -84,14 +84,14 @@ class HostTypes(PlatformMixin, models.TextChoices):
} }
class NetworkTypes(PlatformMixin, models.TextChoices): class NetworkTypes(PlatformMixin, ChoicesMixin, models.TextChoices):
SWITCH = 'switch', _("Switch") SWITCH = 'switch', _("Switch")
ROUTER = 'router', _("Router") ROUTER = 'router', _("Router")
FIREWALL = 'firewall', _("Firewall") FIREWALL = 'firewall', _("Firewall")
OTHER_NETWORK = 'other_network', _("Other device") OTHER_NETWORK = 'other_network', _("Other device")
class DatabaseTypes(PlatformMixin, models.TextChoices): class DatabaseTypes(PlatformMixin, ChoicesMixin, models.TextChoices):
MYSQL = 'mysql', 'MySQL' MYSQL = 'mysql', 'MySQL'
MARIADB = 'mariadb', 'MariaDB' MARIADB = 'mariadb', 'MariaDB'
POSTGRESQL = 'postgresql', 'PostgreSQL' POSTGRESQL = 'postgresql', 'PostgreSQL'
@ -110,15 +110,15 @@ class DatabaseTypes(PlatformMixin, models.TextChoices):
return meta return meta
class WebTypes(PlatformMixin, models.TextChoices): class WebTypes(PlatformMixin, ChoicesMixin, models.TextChoices):
General = 'general', 'General' General = 'general', 'General'
class CloudTypes(PlatformMixin, models.TextChoices): class CloudTypes(PlatformMixin, ChoicesMixin, models.TextChoices):
K8S = 'k8s', 'Kubernetes' K8S = 'k8s', 'Kubernetes'
class AllTypes(metaclass=IncludesTextChoicesMeta): class AllTypes(ChoicesMixin, metaclass=IncludesTextChoicesMeta):
choices: list choices: list
includes = [ includes = [
HostTypes, NetworkTypes, DatabaseTypes, HostTypes, NetworkTypes, DatabaseTypes,
@ -202,7 +202,7 @@ class AllTypes(metaclass=IncludesTextChoicesMeta):
return nodes return nodes
class Protocol(models.TextChoices): class Protocol(ChoicesMixin, models.TextChoices):
ssh = 'ssh', 'SSH' ssh = 'ssh', 'SSH'
rdp = 'rdp', 'RDP' rdp = 'rdp', 'RDP'
telnet = 'telnet', 'Telnet' telnet = 'telnet', 'Telnet'

View File

@ -18,6 +18,9 @@ def create_app_platform(apps, *args):
{'name': 'MongoDB', 'category': 'database', 'type': 'mongodb'}, {'name': 'MongoDB', 'category': 'database', 'type': 'mongodb'},
{'name': 'Redis', 'category': 'database', 'type': 'redis'}, {'name': 'Redis', 'category': 'database', 'type': 'redis'},
{'name': 'Chrome', 'category': 'remote_app', 'type': 'chrome'}, {'name': 'Chrome', 'category': 'remote_app', 'type': 'chrome'},
{'name': 'MysqlWorkbench', 'category': 'remote_app', 'type': 'mysql_workbench'},
{'name': 'VmwareClient', 'category': 'remote_app', 'type': 'vmware_client'},
{'name': 'General', 'category': 'remote_app', 'type': 'general_remote_app'},
{'name': 'Kubernetes', 'category': 'cloud', 'type': 'k8s'}, {'name': 'Kubernetes', 'category': 'cloud', 'type': 'k8s'},
] ]

View File

@ -0,0 +1,40 @@
# Generated by Django 3.2.13 on 2022-08-19 07:23
import assets.models.base
import common.db.fields
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0110_auto_20220817_1716'),
]
operations = [
migrations.CreateModel(
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')),
('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')),
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('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')),
('token', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Token')),
('privileged', models.BooleanField(default=False, verbose_name='Privileged account')),
],
options={
'verbose_name': 'Account template',
},
bases=(models.Model, assets.models.base.AuthMixin),
)
]

View File

@ -0,0 +1,22 @@
# Generated by Django 3.2.13 on 2022-08-19 07:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0111_auto_20220819_1523'),
]
operations = [
migrations.RemoveField(
model_name='platform',
name='protocols_default',
),
migrations.AddField(
model_name='platform',
name='protocols_enabled',
field=models.BooleanField(default=True, verbose_name='Protocols enabled'),
),
]

View File

@ -5,7 +5,7 @@ from simple_history.models import HistoricalRecords
from common.db import fields from common.db import fields
from .base import BaseAccount, AbsConnectivity from .base import BaseAccount, AbsConnectivity
__all__ = ['Account'] __all__ = ['Account', 'AccountTemplate']
class Account(BaseAccount, AbsConnectivity): class Account(BaseAccount, AbsConnectivity):
@ -27,3 +27,14 @@ class Account(BaseAccount, AbsConnectivity):
def __str__(self): def __str__(self):
return '{}@{}'.format(self.username, self.asset.name) return '{}@{}'.format(self.username, self.asset.name)
class AccountTemplate(BaseAccount, AbsConnectivity):
token = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Token'))
privileged = models.BooleanField(verbose_name=_("Privileged account"), default=False)
class Meta:
verbose_name = _('Account template')
def __str__(self):
return '{}@{}'.format(self.username, self.name)

View File

@ -11,6 +11,7 @@ from django.utils.translation import ugettext_lazy as _
from common.utils import lazyproperty from common.utils import lazyproperty
from orgs.mixins.models import OrgManager, JMSOrgBaseModel from orgs.mixins.models import OrgManager, JMSOrgBaseModel
from ...const import Category
from ..platform import Platform from ..platform import Platform
from ..base import AbsConnectivity from ..base import AbsConnectivity

View File

@ -9,5 +9,6 @@ from .gathered_user import *
from .favorite_asset import * from .favorite_asset import *
from .account import * from .account import *
from .account_history import * from .account_history import *
from .account_template import *
from .backup import * from .backup import *
from .platform import * from .platform import *

View File

@ -6,7 +6,6 @@ from assets.models import Account
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import AuthSerializerMixin from .base import AuthSerializerMixin
from common.utils.encode import ssh_pubkey_gen
from common.drf.serializers import SecretReadableMixin from common.drf.serializers import SecretReadableMixin
@ -33,17 +32,6 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
} }
ref_name = 'AssetAccountSerializer' ref_name = 'AssetAccountSerializer'
def _validate_gen_key(self, attrs):
private_key = attrs.get('private_key')
if not private_key:
return attrs
password = attrs.get('passphrase')
username = attrs.get('username')
public_key = ssh_pubkey_gen(private_key, password=password, username=username)
attrs['public_key'] = public_key
return attrs
def validate(self, attrs): def validate(self, attrs):
attrs = self._validate_gen_key(attrs) attrs = self._validate_gen_key(attrs)
return attrs return attrs

View File

@ -0,0 +1,22 @@
from assets.models import AccountTemplate
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import AuthSerializerMixin
from .account import AccountSerializer
class AccountTemplateSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class Meta:
model = AccountTemplate
fields_mini = ['id', 'privileged', 'username', 'name']
fields_write_only = AccountSerializer.Meta.fields_write_only
fields_other = AccountSerializer.Meta.fields_other
fields = fields_mini + fields_write_only + fields_other
extra_kwargs = AccountSerializer.Meta.extra_kwargs
def validate(self, attrs):
print(attrs)
raise ValueError('test')
attrs = self._validate_gen_key(attrs)
return attrs

View File

@ -5,6 +5,7 @@ from .common import AssetSerializer
__all__ = [ __all__ = [
'DeviceSerializer', 'HostSerializer', 'DatabaseSerializer', 'DeviceSerializer', 'HostSerializer', 'DatabaseSerializer',
'NetworkSerializer', 'CloudSerializer',
] ]

View File

@ -13,7 +13,8 @@ from .utils import validate_password_for_ansible
class AuthSerializer(serializers.ModelSerializer): class AuthSerializer(serializers.ModelSerializer):
password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024, label=_('Password')) 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')) 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): def gen_keys(self, private_key=None, password=None):
if private_key is None: if private_key is None:
@ -74,6 +75,18 @@ class AuthSerializerMixin(serializers.ModelSerializer):
validated_data.pop(field, None) validated_data.pop(field, None)
validated_data.pop('passphrase', None) validated_data.pop('passphrase', None)
@staticmethod
def _validate_gen_key(attrs):
private_key = attrs.get('private_key')
if not private_key:
return attrs
password = attrs.get('passphrase')
username = attrs.get('username')
public_key = ssh_pubkey_gen(private_key, password=password, username=username)
attrs['public_key'] = public_key
return attrs
def create(self, validated_data): def create(self, validated_data):
self.clean_auth_fields(validated_data) self.clean_auth_fields(validated_data)
return super().create(validated_data) return super().create(validated_data)

View File

@ -14,6 +14,7 @@ router.register(r'assets', api.AssetViewSet, 'asset')
router.register(r'hosts', api.HostViewSet, 'host') router.register(r'hosts', api.HostViewSet, 'host')
router.register(r'databases', api.DatabaseViewSet, 'database') router.register(r'databases', api.DatabaseViewSet, 'database')
router.register(r'accounts', api.AccountViewSet, 'account') router.register(r'accounts', api.AccountViewSet, 'account')
router.register(r'account-templates', api.AccountTemplateViewSet, 'account-template')
router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret') router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret')
router.register(r'accounts-history', api.AccountHistoryViewSet, 'account-history') router.register(r'accounts-history', api.AccountHistoryViewSet, 'account-history')
router.register(r'account-history-secrets', api.AccountHistorySecretsViewSet, 'account-history-secret') router.register(r'account-history-secrets', api.AccountHistorySecretsViewSet, 'account-history-secret')

View File

@ -107,11 +107,11 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
] ]
filterset_fields = [ filterset_fields = [
'user__name', 'user__username', 'command', 'user__name', 'user__username', 'command',
'run_as__name', 'run_as__username', 'is_finished' 'account', 'is_finished'
] ]
search_fields = [ search_fields = [
'command', 'user__name', 'user__username', 'command', 'user__name', 'user__username',
'run_as__name', 'run_as__username', 'account__username',
] ]
ordering = ['-date_created'] ordering = ['-date_created']
@ -121,7 +121,7 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
return queryset.model.objects.none() return queryset.model.objects.none()
if current_org.is_root(): if current_org.is_root():
return queryset return queryset
queryset = queryset.filter(run_as__org_id=current_org.org_id()) # queryset = queryset.filter(run_as__org_id=current_org.org_id())
return queryset return queryset
@ -131,7 +131,7 @@ class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet)
filterset_fields = [ filterset_fields = [
'id', 'asset', 'commandexecution' 'id', 'asset', 'commandexecution'
] ]
search_fields = ('asset__hostname', ) search_fields = ('asset__name', )
http_method_names = ['options', 'get'] http_method_names = ['options', 'get']
rbac_perms = { rbac_perms = {
'GET': 'ops.view_commandexecution', 'GET': 'ops.view_commandexecution',
@ -142,7 +142,7 @@ class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet)
queryset = super().get_queryset() queryset = super().get_queryset()
queryset = queryset.annotate( queryset = queryset.annotate(
asset_display=Concat( asset_display=Concat(
F('asset__hostname'), Value('('), F('asset__name'), Value('('),
F('asset__ip'), Value(')') F('asset__ip'), Value(')')
) )
) )

View File

@ -88,24 +88,22 @@ class CommandExecutionSerializer(serializers.ModelSerializer):
model = CommandExecution model = CommandExecution
fields_mini = ['id'] fields_mini = ['id']
fields_small = fields_mini + [ fields_small = fields_mini + [
'run_as', 'command', 'is_finished', 'user', 'command', 'is_finished', 'user',
'date_start', 'result', 'is_success', 'org_id' 'date_start', 'result', 'is_success', 'org_id'
] ]
fields = fields_small + ['hosts', 'hosts_display', 'run_as_display', 'user_display'] fields = fields_small + ['hosts', 'hosts_display', 'user_display']
extra_kwargs = { extra_kwargs = {
'result': {'label': _('Result')}, # model 上的方法,只能在这修改 'result': {'label': _('Result')}, # model 上的方法,只能在这修改
'is_success': {'label': _('Is success')}, 'is_success': {'label': _('Is success')},
'hosts': {'label': _('Hosts')}, # 外键,会生成 sql。不在 model 上修改 'hosts': {'label': _('Hosts')}, # 外键,会生成 sql。不在 model 上修改
'run_as': {'label': _('Run as')},
'user': {'label': _('User')}, 'user': {'label': _('User')},
'run_as_display': {'label': _('Run as display')},
'user_display': {'label': _('User display')}, 'user_display': {'label': _('User display')},
} }
@classmethod @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """ """ Perform necessary eager loading of data. """
queryset = queryset.prefetch_related('user', 'run_as', 'hosts') queryset = queryset.prefetch_related('user', 'hosts')
return queryset return queryset

View File

@ -45,18 +45,12 @@ class ConnectionTokenMixin:
@staticmethod @staticmethod
def check_user_has_resource_permission(user, asset, application, system_user): def check_user_has_resource_permission(user, asset, application, system_user):
from perms.utils.asset import has_asset_system_permission from perms.utils.asset import has_asset_system_permission
from perms.utils.application import has_application_system_permission
if asset and not has_asset_system_permission(user, asset, system_user): if asset and not has_asset_system_permission(user, asset, system_user):
error = f'User not has this asset and system user permission: ' \ error = f'User not has this asset and system user permission: ' \
f'user={user.id} system_user={system_user.id} asset={asset.id}' f'user={user.id} system_user={system_user.id} asset={asset.id}'
raise PermissionDenied(error) raise PermissionDenied(error)
if application and not has_application_system_permission(user, application, system_user):
error = f'User not has this application and system user permission: ' \
f'user={user.id} system_user={system_user.id} application={application.id}'
raise PermissionDenied(error)
def get_smart_endpoint(self, protocol, asset=None, application=None): def get_smart_endpoint(self, protocol, asset=None, application=None):
if asset: if asset:
target_ip = asset.get_target_ip() target_ip = asset.get_target_ip()
@ -204,8 +198,7 @@ class ConnectionTokenMixin:
class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelViewSet): class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelViewSet):
filterset_fields = ( filterset_fields = (
'type', 'type', 'user_display', 'asset_display'
'user_display', 'system_user_display', 'application_display', 'asset_display'
) )
search_fields = filterset_fields search_fields = filterset_fields
serializer_classes = { serializer_classes = {

View File

@ -10,7 +10,7 @@ def migrate_system_user_to_account(apps, schema_editor):
while True: while True:
connection_tokens = connection_token_model.objects \ connection_tokens = connection_token_model.objects \
.prefetch_related('system_users')[count:bulk_size] .prefetch_related('system_user')[count:bulk_size]
if not connection_tokens: if not connection_tokens:
break break
count += len(connection_tokens) count += len(connection_tokens)

View File

@ -85,6 +85,14 @@ class BitOperationChoice:
return [(cls.NAME_MAP[i], j) for i, j in cls.DB_CHOICES] return [(cls.NAME_MAP[i], j) for i, j in cls.DB_CHOICES]
class ChoicesMixin:
_value2label_map_: dict
@classmethod
def get_label(cls, value: (str, int)):
return cls._value2label_map_[value]
class BaseCreateUpdateModel(models.Model): class BaseCreateUpdateModel(models.Model):
created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
updated_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Updated by')) updated_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Updated by'))

View File

@ -25,7 +25,6 @@ class CommandExecutionViewSet(RootOrgViewMixin, viewsets.ModelViewSet):
def check_hosts(self, serializer): def check_hosts(self, serializer):
data = serializer.validated_data data = serializer.validated_data
assets = data["hosts"] assets = data["hosts"]
system_user = data["run_as"]
user = self.request.user user = self.request.user
# TOdo: # TOdo:

View File

@ -47,10 +47,6 @@ class CommandExecution(OrgModelMixin):
inv = JMSInventory(self.allow_assets, run_as=username, system_user=self.run_as) inv = JMSInventory(self.allow_assets, run_as=username, system_user=self.run_as)
return inv return inv
@lazyproperty
def run_as_display(self):
return str(self.run_as)
@lazyproperty @lazyproperty
def user_display(self): def user_display(self):
return str(self.user) return str(self.user)

View File

@ -138,9 +138,8 @@ class CommandExecutionSerializer(serializers.ModelSerializer):
'command', 'result', 'log_url', 'command', 'result', 'log_url',
'is_finished', 'date_created', 'date_finished' 'is_finished', 'date_created', 'date_finished'
] ]
fields_fk = ['run_as']
fields_m2m = ['hosts'] fields_m2m = ['hosts']
fields = fields_small + fields_fk + fields_m2m fields = fields_small + fields_m2m
read_only_fields = [ read_only_fields = [
'result', 'is_finished', 'log_url', 'date_created', 'result', 'is_finished', 'log_url', 'date_created',
'date_finished' 'date_finished'

View File

@ -80,12 +80,12 @@ class AssetPermissionAssetRelationViewSet(RelationMixin):
filterset_fields = [ filterset_fields = [
'id', 'asset', 'assetpermission', 'id', 'asset', 'assetpermission',
] ]
search_fields = ["id", "asset__hostname", "asset__ip", "assetpermission__name"] search_fields = ["id", "asset__name", "asset__ip", "assetpermission__name"]
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
queryset = queryset \ queryset = queryset \
.annotate(asset_display=F('asset__hostname')) .annotate(asset_display=F('asset__name'))
return queryset return queryset

View File

@ -12,7 +12,9 @@ from rest_framework.generics import (
) )
from orgs.utils import tmp_to_root_org from orgs.utils import tmp_to_root_org
from perms.utils.permission import get_asset_system_user_ids_with_actions_by_user, validate_permission from perms.utils.permission import (
get_asset_system_user_ids_with_actions_by_user, validate_permission
)
from common.permissions import IsValidUser from common.permissions import IsValidUser
from common.utils import get_logger, lazyproperty from common.utils import get_logger, lazyproperty

View File

@ -25,7 +25,7 @@ class AssetGrantedSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Asset model = Asset
only_fields = [ only_fields = [
"id", "name", "ip", "protocols", "os", 'domain', "id", "name", "ip", "protocols", 'domain',
"platform", "comment", "org_id", "is_active" "platform", "comment", "org_id", "is_active"
] ]
fields = only_fields + ['org_name'] fields = only_fields + ['org_name']