diff --git a/apps/applications/serializers/application.py b/apps/applications/serializers/application.py index 10c95da24..8f6d103c0 100644 --- a/apps/applications/serializers/application.py +++ b/apps/applications/serializers/application.py @@ -18,15 +18,19 @@ class ApplicationSerializerMixin(serializers.Serializer): attrs = MethodSerializer() def get_attrs_serializer(self): - request = self.context['request'] - query_type = request.query_params.get('type') - query_category = request.query_params.get('category') - if query_type: - serializer_class = type_serializer_classes_mapping.get(query_type) - elif query_category: - serializer_class = category_serializer_classes_mapping.get(query_category) + serializer_class = None + if isinstance(self.instance, models.Application): + instance_type = self.instance.type + serializer_class = type_serializer_classes_mapping.get(instance_type) else: - serializer_class = None + request = self.context['request'] + query_type = request.query_params.get('type') + query_category = request.query_params.get('category') + if query_type: + serializer_class = type_serializer_classes_mapping.get(query_type) + elif query_category: + serializer_class = category_serializer_classes_mapping.get(query_category) + if serializer_class is None: serializer_class = serializers.Serializer serializer = serializer_class() diff --git a/apps/applications/serializers/attrs/application_category/cloud.py b/apps/applications/serializers/attrs/application_category/cloud.py index 60739d508..f5fa71810 100644 --- a/apps/applications/serializers/attrs/application_category/cloud.py +++ b/apps/applications/serializers/attrs/application_category/cloud.py @@ -6,4 +6,4 @@ __all__ = ['CloudSerializer'] class CloudSerializer(serializers.Serializer): - cluster = serializers.CharField(max_length=1024, label=_('Cluster')) + cluster = serializers.CharField(max_length=1024, label=_('Cluster'), allow_null=True) diff --git a/apps/applications/serializers/attrs/application_category/db.py b/apps/applications/serializers/attrs/application_category/db.py index cbe00570c..f2967963e 100644 --- a/apps/applications/serializers/attrs/application_category/db.py +++ b/apps/applications/serializers/attrs/application_category/db.py @@ -8,9 +8,8 @@ __all__ = ['DBSerializer'] class DBSerializer(serializers.Serializer): - host = serializers.CharField(max_length=128, label=_('Host')) - port = serializers.IntegerField(label=_('Port')) - # 添加allow_null=True,兼容之前数据库中database字段为None的情况 + host = serializers.CharField(max_length=128, label=_('Host'), allow_null=True) + port = serializers.IntegerField(label=_('Port'), allow_null=True) database = serializers.CharField( max_length=128, required=True, allow_null=True, label=_('Database') ) diff --git a/apps/applications/serializers/attrs/application_category/remote_app.py b/apps/applications/serializers/attrs/application_category/remote_app.py index 5520e2fc2..9fa691976 100644 --- a/apps/applications/serializers/attrs/application_category/remote_app.py +++ b/apps/applications/serializers/attrs/application_category/remote_app.py @@ -29,8 +29,12 @@ class CharPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField): class RemoteAppSerializer(serializers.Serializer): asset_info = serializers.SerializerMethodField() - asset = CharPrimaryKeyRelatedField(queryset=Asset.objects, required=False, label=_("Asset")) - path = serializers.CharField(max_length=128, label=_('Application path')) + asset = CharPrimaryKeyRelatedField( + queryset=Asset.objects, required=False, label=_("Asset"), allow_null=True + ) + path = serializers.CharField( + max_length=128, label=_('Application path'), allow_null=True + ) @staticmethod def get_asset_info(obj): diff --git a/apps/applications/serializers/attrs/application_type/chrome.py b/apps/applications/serializers/attrs/application_type/chrome.py index 13b0c21f9..5b9e5147a 100644 --- a/apps/applications/serializers/attrs/application_type/chrome.py +++ b/apps/applications/serializers/attrs/application_type/chrome.py @@ -11,15 +11,16 @@ class ChromeSerializer(RemoteAppSerializer): CHROME_PATH = 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe' path = serializers.CharField( - max_length=128, label=_('Application path'), default=CHROME_PATH + max_length=128, label=_('Application path'), default=CHROME_PATH, allow_null=True, ) chrome_target = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('Target URL') + max_length=128, allow_blank=True, required=False, label=_('Target URL'), allow_null=True, ) chrome_username = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('Username') + max_length=128, allow_blank=True, required=False, label=_('Username'), allow_null=True, ) chrome_password = serializers.CharField( - max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password') + max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'), + allow_null=True ) diff --git a/apps/applications/serializers/attrs/application_type/custom.py b/apps/applications/serializers/attrs/application_type/custom.py index 78e26ae69..0cbc09cf9 100644 --- a/apps/applications/serializers/attrs/application_type/custom.py +++ b/apps/applications/serializers/attrs/application_type/custom.py @@ -10,14 +10,18 @@ __all__ = ['CustomSerializer'] class CustomSerializer(RemoteAppSerializer): custom_cmdline = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('Operating parameter') + max_length=128, allow_blank=True, required=False, label=_('Operating parameter'), + allow_null=True, ) custom_target = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('Target url') + max_length=128, allow_blank=True, required=False, label=_('Target url'), + allow_null=True, ) custom_username = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('Username') + max_length=128, allow_blank=True, required=False, label=_('Username'), + allow_null=True, ) custom_password = serializers.CharField( - max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password') + max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'), + allow_null=True, ) diff --git a/apps/applications/serializers/attrs/application_type/mysql.py b/apps/applications/serializers/attrs/application_type/mysql.py index f9854333e..bb4e56c33 100644 --- a/apps/applications/serializers/attrs/application_type/mysql.py +++ b/apps/applications/serializers/attrs/application_type/mysql.py @@ -8,7 +8,7 @@ __all__ = ['MySQLSerializer'] class MySQLSerializer(DBSerializer): - port = serializers.IntegerField(default=3306, label=_('Port')) + port = serializers.IntegerField(default=3306, label=_('Port'), allow_null=True) diff --git a/apps/applications/serializers/attrs/application_type/mysql_workbench.py b/apps/applications/serializers/attrs/application_type/mysql_workbench.py index 2eaa89858..8025ec80f 100644 --- a/apps/applications/serializers/attrs/application_type/mysql_workbench.py +++ b/apps/applications/serializers/attrs/application_type/mysql_workbench.py @@ -11,20 +11,26 @@ class MySQLWorkbenchSerializer(RemoteAppSerializer): MYSQL_WORKBENCH_PATH = 'C:\Program Files\MySQL\MySQL Workbench 8.0 CE\MySQLWorkbench.exe' path = serializers.CharField( - max_length=128, label=_('Application path'), default=MYSQL_WORKBENCH_PATH + max_length=128, label=_('Application path'), default=MYSQL_WORKBENCH_PATH, + allow_null=True, ) mysql_workbench_ip = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('IP') + max_length=128, allow_blank=True, required=False, label=_('IP'), + allow_null=True, ) mysql_workbench_port = serializers.IntegerField( - required=False, label=_('Port') + required=False, label=_('Port'), + allow_null=True, ) mysql_workbench_name = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('Database') + max_length=128, allow_blank=True, required=False, label=_('Database'), + allow_null=True, ) mysql_workbench_username = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('Username') + max_length=128, allow_blank=True, required=False, label=_('Username'), + allow_null=True, ) mysql_workbench_password = serializers.CharField( - max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password') + max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'), + allow_null=True, ) diff --git a/apps/applications/serializers/attrs/application_type/oracle.py b/apps/applications/serializers/attrs/application_type/oracle.py index 73f637990..63b905714 100644 --- a/apps/applications/serializers/attrs/application_type/oracle.py +++ b/apps/applications/serializers/attrs/application_type/oracle.py @@ -8,5 +8,5 @@ __all__ = ['OracleSerializer'] class OracleSerializer(DBSerializer): - port = serializers.IntegerField(default=1521, label=_('Port')) + port = serializers.IntegerField(default=1521, label=_('Port'), allow_null=True) diff --git a/apps/applications/serializers/attrs/application_type/pgsql.py b/apps/applications/serializers/attrs/application_type/pgsql.py index 236cad557..1b3cc2ef4 100644 --- a/apps/applications/serializers/attrs/application_type/pgsql.py +++ b/apps/applications/serializers/attrs/application_type/pgsql.py @@ -8,5 +8,5 @@ __all__ = ['PostgreSerializer'] class PostgreSerializer(DBSerializer): - port = serializers.IntegerField(default=5432, label=_('Port')) + port = serializers.IntegerField(default=5432, label=_('Port'), allow_null=True) diff --git a/apps/applications/serializers/attrs/application_type/vmware_client.py b/apps/applications/serializers/attrs/application_type/vmware_client.py index 66b2bbcf9..1ed44e656 100644 --- a/apps/applications/serializers/attrs/application_type/vmware_client.py +++ b/apps/applications/serializers/attrs/application_type/vmware_client.py @@ -15,14 +15,18 @@ class VMwareClientSerializer(RemoteAppSerializer): VMWARE_CLIENT_PATH = ''.join(PATH.split()) path = serializers.CharField( - max_length=128, label=_('Application path'), default=VMWARE_CLIENT_PATH + max_length=128, label=_('Application path'), default=VMWARE_CLIENT_PATH, + allow_null=True ) vmware_target = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('Target URL') + max_length=128, allow_blank=True, required=False, label=_('Target URL'), + allow_null=True ) vmware_username = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('Username') + max_length=128, allow_blank=True, required=False, label=_('Username'), + allow_null=True ) vmware_password = serializers.CharField( - max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password') + max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'), + allow_null=True ) diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 6491f003f..738c95986 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -208,12 +208,11 @@ class SystemUser(BaseUser): def get_protocol_by_application_type(cls, app_type): from applications.const import ApplicationTypeChoices if app_type in ApplicationTypeChoices.remote_app_types(): - return cls.PROTOCOL_RDP - protocol = None - other_types = [*ApplicationTypeChoices.db_types(), *ApplicationTypeChoices.cloud_types()] - if app_type in other_types and app_type in cls.APPLICATION_CATEGORY_PROTOCOLS: + protocol = cls.PROTOCOL_RDP + else: protocol = app_type - return protocol + if protocol in cls.APPLICATION_CATEGORY_PROTOCOLS: + return protocol class Meta: ordering = ['name'] diff --git a/apps/common/drf/fields.py b/apps/common/drf/fields.py index 2ee17f19a..9b92af381 100644 --- a/apps/common/drf/fields.py +++ b/apps/common/drf/fields.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- # -import copy from rest_framework import serializers __all__ = [ - 'ReadableHiddenField', 'CustomMetaDictField', + 'ReadableHiddenField', ] @@ -24,87 +23,3 @@ class ReadableHiddenField(serializers.HiddenField): if hasattr(value, 'id'): return getattr(value, 'id') return value - -# -# OtherField -# ---------- - - -# TODO: DELETE 替换完成后删除 -class CustomMetaDictField(serializers.DictField): - """ - In use: - RemoteApp params field - CommandStorage meta field - ReplayStorage meta field - """ - type_fields_map = {} - default_type = None - convert_key_remove_type_prefix = False - convert_key_to_upper = False - - def filter_attribute(self, attribute, instance): - fields = self.type_fields_map.get(instance.type, []) - for field in fields: - if field.get('write_only', False): - attribute.pop(field['name'], None) - return attribute - - def get_attribute(self, instance): - """ - 序列化时调用 - """ - attribute = super().get_attribute(instance) - attribute = self.filter_attribute(attribute, instance) - return attribute - - def convert_value_key_remove_type_prefix(self, dictionary, value): - if not self.convert_key_remove_type_prefix: - return value - tp = dictionary.get('type') - prefix = '{}_'.format(tp) - convert_value = {} - for k, v in value.items(): - if k.lower().startswith(prefix): - k = k.lower().split(prefix, 1)[1] - convert_value[k] = v - return convert_value - - def convert_value_key_to_upper(self, value): - if not self.convert_key_to_upper: - return value - convert_value = {k.upper(): v for k, v in value.items()} - return convert_value - - def convert_value_key(self, dictionary, value): - value = self.convert_value_key_remove_type_prefix(dictionary, value) - value = self.convert_value_key_to_upper(value) - return value - - def filter_value_key(self, dictionary, value): - tp = dictionary.get('type') - fields = self.type_fields_map.get(tp, []) - fields_names = [field['name'] for field in fields] - filter_value = {k: v for k, v in value.items() if k in fields_names} - return filter_value - - @staticmethod - def strip_value(value): - new_value = {} - for k, v in value.items(): - if isinstance(v, str): - v = v.strip() - new_value[k] = v - return new_value - - def get_value(self, dictionary): - """ - 反序列化时调用 - """ - value = super().get_value(dictionary) - value = self.convert_value_key(dictionary, value) - value = self.filter_value_key(dictionary, value) - value = self.strip_value(value) - return value - - diff --git a/apps/common/drf/serializers.py b/apps/common/drf/serializers.py index 1087b46da..4beb59fe7 100644 --- a/apps/common/drf/serializers.py +++ b/apps/common/drf/serializers.py @@ -1,3 +1,4 @@ +import copy from rest_framework import serializers from rest_framework.serializers import Serializer from rest_framework.serializers import ModelSerializer @@ -38,10 +39,10 @@ class MethodSerializer(serializers.Serializer): @cached_property def serializer(self) -> serializers.Serializer: method = getattr(self.parent, self.method_name) - return method() - - def get_fields(self): - return self.serializer.get_fields() + _serializer = method() + # 设置serializer的parent值,否则在serializer实例中获取parent会出现断层 + setattr(_serializer, 'parent', self.parent) + return _serializer @cached_property def fields(self): @@ -50,10 +51,16 @@ class MethodSerializer(serializers.Serializer): 这样在调用 field.parent 时, 才会达到预期的结果, 比如: serializers.SerializerMethodField """ - fields = BindingDict(self.serializer) - for key, value in self.get_fields().items(): - fields[key] = value - return fields + return self.serializer.fields + + def run_validation(self, data=serializers.empty): + return self.serializer.run_validation(data) + + def to_representation(self, instance): + return self.serializer.to_representation(instance) + + def get_initial(self): + return self.serializer.get_initial() # Other Serializer diff --git a/apps/terminal/api/storage.py b/apps/terminal/api/storage.py index 9dab8c5d9..be3002060 100644 --- a/apps/terminal/api/storage.py +++ b/apps/terminal/api/storage.py @@ -20,10 +20,10 @@ class BaseStorageViewSetMixin: def destroy(self, request, *args, **kwargs): instance = self.get_object() - if instance.in_defaults(): + if instance.type_null_or_server: data = {'msg': _('Deleting the default storage is not allowed')} return Response(data=data, status=status.HTTP_400_BAD_REQUEST) - if instance.is_using(): + if instance.is_use(): data = {'msg': _('Cannot delete storage that is being used')} return Response(data=data, status=status.HTTP_400_BAD_REQUEST) return super().destroy(request, *args, **kwargs) @@ -67,11 +67,9 @@ class BaseStorageTestConnectiveMixin: return Response(data) -class CommandStorageTestConnectiveApi(BaseStorageTestConnectiveMixin, - generics.RetrieveAPIView): +class CommandStorageTestConnectiveApi(BaseStorageTestConnectiveMixin, generics.RetrieveAPIView): queryset = CommandStorage.objects.all() -class ReplayStorageTestConnectiveApi(BaseStorageTestConnectiveMixin, - generics.RetrieveAPIView): +class ReplayStorageTestConnectiveApi(BaseStorageTestConnectiveMixin, generics.RetrieveAPIView): queryset = ReplayStorage.objects.all() diff --git a/apps/terminal/api/terminal.py b/apps/terminal/api/terminal.py index 005d40544..14213316c 100644 --- a/apps/terminal/api/terminal.py +++ b/apps/terminal/api/terminal.py @@ -148,7 +148,5 @@ class TerminalConfig(APIView): permission_classes = (IsAppUser,) def get(self, request): - user = request.user - terminal = user.terminal - configs = terminal.config - return Response(configs, status=200) \ No newline at end of file + config = request.user.terminal.config + return Response(config, status=200) \ No newline at end of file diff --git a/apps/terminal/apps.py b/apps/terminal/apps.py index 62b8ff085..5341369c7 100644 --- a/apps/terminal/apps.py +++ b/apps/terminal/apps.py @@ -8,4 +8,4 @@ class TerminalConfig(AppConfig): def ready(self): from . import signals_handler - return super().ready() \ No newline at end of file + return super().ready() diff --git a/apps/terminal/backends/__init__.py b/apps/terminal/backends/__init__.py index 52a33b359..4e08d052e 100644 --- a/apps/terminal/backends/__init__.py +++ b/apps/terminal/backends/__init__.py @@ -3,7 +3,6 @@ from django.conf import settings from django.utils.functional import LazyObject from .command.serializers import SessionCommandSerializer -from ..const import COMMAND_STORAGE_TYPE_SERVER TYPE_ENGINE_MAPPING = { @@ -30,13 +29,12 @@ def get_terminal_command_storages(): from ..models import CommandStorage storage_list = {} for s in CommandStorage.objects.all(): - tp = s.type - if tp == COMMAND_STORAGE_TYPE_SERVER: + if s.type_server: storage = get_command_storage() else: - if not TYPE_ENGINE_MAPPING.get(tp): + if not TYPE_ENGINE_MAPPING.get(s.type): continue - engine_class = import_module(TYPE_ENGINE_MAPPING[tp]) + engine_class = import_module(TYPE_ENGINE_MAPPING[s.type]) storage = engine_class.CommandStore(s.config) storage_list[s.name] = storage return storage_list diff --git a/apps/terminal/const.py b/apps/terminal/const.py index b7a48fab3..488726c39 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -1,117 +1,31 @@ # -*- coding: utf-8 -*- # -ASSETS_CACHE_KEY = "terminal__session__assets" -USERS_CACHE_KEY = "terminal__session__users" -SYSTEM_USER_CACHE_KEY = "terminal__session__system_users" - - -# Replay Storage - -REPLAY_STORAGE_TYPE_NULL = 'null' -REPLAY_STORAGE_TYPE_SERVER = 'server' -REPLAY_STORAGE_TYPE_S3 = 's3' -REPLAY_STORAGE_TYPE_CEPH = 'ceph' -REPLAY_STORAGE_TYPE_SWIFT = 'swift' -REPLAY_STORAGE_TYPE_OSS = 'oss' -REPLAY_STORAGE_TYPE_AZURE = 'azure' - -REPLAY_STORAGE_TYPE_EMPTY_FIELDS = [] -REPLAY_STORAGE_TYPE_S3_FIELDS = [ - {'name': 'BUCKET'}, - {'name': 'ACCESS_KEY', 'write_only': True}, - {'name': 'SECRET_KEY', 'write_only': True}, - {'name': 'ENDPOINT'} -] -REPLAY_STORAGE_TYPE_CEPH_FIELDS = [ - {'name': 'BUCKET'}, - {'name': 'ACCESS_KEY', 'write_only': True}, - {'name': 'SECRET_KEY', 'write_only': True}, - {'name': 'ENDPOINT'} -] -REPLAY_STORAGE_TYPE_SWIFT_FIELDS = [ - {'name': 'BUCKET'}, - {'name': 'ACCESS_KEY', 'write_only': True}, - {'name': 'SECRET_KEY', 'write_only': True}, - {'name': 'REGION'}, - {'name': 'ENDPOINT'}, - {'name': 'PROTOCOL'}, -] -REPLAY_STORAGE_TYPE_OSS_FIELDS = [ - {'name': 'BUCKET'}, - {'name': 'ACCESS_KEY', 'write_only': True}, - {'name': 'SECRET_KEY', 'write_only': True}, - {'name': 'ENDPOINT'} -] -REPLAY_STORAGE_TYPE_AZURE_FIELDS = [ - {'name': 'CONTAINER_NAME'}, - {'name': 'ACCOUNT_NAME'}, - {'name': 'ACCOUNT_KEY', 'write_only': True}, - {'name': 'ENDPOINT_SUFFIX'} -] - -REPLAY_STORAGE_TYPE_FIELDS_MAP = { - REPLAY_STORAGE_TYPE_NULL: REPLAY_STORAGE_TYPE_EMPTY_FIELDS, - REPLAY_STORAGE_TYPE_SERVER: REPLAY_STORAGE_TYPE_EMPTY_FIELDS, - REPLAY_STORAGE_TYPE_S3: REPLAY_STORAGE_TYPE_S3_FIELDS, - REPLAY_STORAGE_TYPE_CEPH: REPLAY_STORAGE_TYPE_CEPH_FIELDS, - REPLAY_STORAGE_TYPE_SWIFT: REPLAY_STORAGE_TYPE_SWIFT_FIELDS, - REPLAY_STORAGE_TYPE_OSS: REPLAY_STORAGE_TYPE_OSS_FIELDS, - REPLAY_STORAGE_TYPE_AZURE: REPLAY_STORAGE_TYPE_AZURE_FIELDS -} - -REPLAY_STORAGE_TYPE_CHOICES_DEFAULT = [ - (REPLAY_STORAGE_TYPE_NULL, 'Null'), - (REPLAY_STORAGE_TYPE_SERVER, 'Server'), -] - -REPLAY_STORAGE_TYPE_CHOICES_EXTENDS = [ - (REPLAY_STORAGE_TYPE_S3, 'S3'), - (REPLAY_STORAGE_TYPE_CEPH, 'Ceph'), - (REPLAY_STORAGE_TYPE_SWIFT, 'Swift'), - (REPLAY_STORAGE_TYPE_OSS, 'OSS'), - (REPLAY_STORAGE_TYPE_AZURE, 'Azure') -] - -REPLAY_STORAGE_TYPE_CHOICES = REPLAY_STORAGE_TYPE_CHOICES_DEFAULT + \ - REPLAY_STORAGE_TYPE_CHOICES_EXTENDS - - -# Command Storage - -COMMAND_STORAGE_TYPE_NULL = 'null' -COMMAND_STORAGE_TYPE_SERVER = 'server' -COMMAND_STORAGE_TYPE_ES = 'es' - -COMMAND_STORAGE_TYPE_EMPTY_FIELDS = [] -COMMAND_STORAGE_TYPE_ES_FIELDS = [ - {'name': 'HOSTS'}, - {'name': 'INDEX'}, - {'name': 'DOC_TYPE'} -] - -COMMAND_STORAGE_TYPE_FIELDS_MAP = { - COMMAND_STORAGE_TYPE_NULL: COMMAND_STORAGE_TYPE_EMPTY_FIELDS, - COMMAND_STORAGE_TYPE_SERVER: COMMAND_STORAGE_TYPE_EMPTY_FIELDS, - COMMAND_STORAGE_TYPE_ES: COMMAND_STORAGE_TYPE_ES_FIELDS, -} - -COMMAND_STORAGE_TYPE_CHOICES_DEFAULT = [ - (COMMAND_STORAGE_TYPE_NULL, 'Null'), - (COMMAND_STORAGE_TYPE_SERVER, 'Server'), -] - -COMMAND_STORAGE_TYPE_CHOICES_EXTENDS = [ - (COMMAND_STORAGE_TYPE_ES, 'Elasticsearch') -] - -COMMAND_STORAGE_TYPE_CHOICES = COMMAND_STORAGE_TYPE_CHOICES_DEFAULT + \ - COMMAND_STORAGE_TYPE_CHOICES_EXTENDS - - from django.db.models import TextChoices from django.utils.translation import ugettext_lazy as _ +# Replay & Command Storage Choices +# -------------------------------- + + +class ReplayStorageTypeChoices(TextChoices): + null = 'null', 'Null', + server = 'server', 'Server' + s3 = 's3', 'S3' + ceph = 'ceph', 'Ceph' + swift = 'swift', 'Swift' + oss = 'oss', 'OSS' + azure = 'azure', 'Azure' + + +class CommandStorageTypeChoices(TextChoices): + null = 'null', 'Null', + server = 'server', 'Server' + es = 'es', 'Elasticsearch' + + +# Component Status Choices +# ------------------------ class ComponentStatusChoices(TextChoices): critical = 'critical', _('Critical') diff --git a/apps/terminal/models/storage.py b/apps/terminal/models/storage.py index 66fbe393d..e3b7113c2 100644 --- a/apps/terminal/models/storage.py +++ b/apps/terminal/models/storage.py @@ -2,102 +2,113 @@ from __future__ import unicode_literals import os import jms_storage - from django.db import models from django.utils.translation import ugettext_lazy as _ from django.conf import settings - from common.mixins import CommonModelMixin from common.fields.model import EncryptJsonDictTextField -from .. import const from .terminal import Terminal +from .. import const class CommandStorage(CommonModelMixin): - TYPE_CHOICES = const.COMMAND_STORAGE_TYPE_CHOICES - TYPE_DEFAULTS = dict(const.REPLAY_STORAGE_TYPE_CHOICES_DEFAULT).keys() - TYPE_SERVER = const.COMMAND_STORAGE_TYPE_SERVER - name = models.CharField(max_length=128, verbose_name=_("Name"), unique=True) type = models.CharField( - max_length=16, choices=TYPE_CHOICES, verbose_name=_('Type'), - default=TYPE_SERVER + max_length=16, choices=const.CommandStorageTypeChoices.choices, + default=const.CommandStorageTypeChoices.server.value, verbose_name=_('Type'), ) meta = EncryptJsonDictTextField(default={}) - comment = models.TextField( - max_length=128, default='', blank=True, verbose_name=_('Comment') - ) + comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) def __str__(self): return self.name + @property + def type_null(self): + return self.type == const.CommandStorageTypeChoices.null.value + + @property + def type_server(self): + return self.type == const.CommandStorageTypeChoices.server.value + + @property + def type_null_or_server(self): + return self.type_null or self.type_server + @property def config(self): config = self.meta config.update({'TYPE': self.type}) return config - def in_defaults(self): - return self.type in self.TYPE_DEFAULTS - def is_valid(self): - if self.in_defaults(): + if self.type_null_or_server: return True storage = jms_storage.get_log_storage(self.config) return storage.ping() - def is_using(self): + def is_use(self): return Terminal.objects.filter(command_storage=self.name).exists() class ReplayStorage(CommonModelMixin): - TYPE_CHOICES = const.REPLAY_STORAGE_TYPE_CHOICES - TYPE_SERVER = const.REPLAY_STORAGE_TYPE_SERVER - TYPE_DEFAULTS = dict(const.REPLAY_STORAGE_TYPE_CHOICES_DEFAULT).keys() - name = models.CharField(max_length=128, verbose_name=_("Name"), unique=True) type = models.CharField( - max_length=16, choices=TYPE_CHOICES, verbose_name=_('Type'), - default=TYPE_SERVER + max_length=16, choices=const.ReplayStorageTypeChoices.choices, + default=const.ReplayStorageTypeChoices.server.value, verbose_name=_('Type') ) meta = EncryptJsonDictTextField(default={}) - comment = models.TextField( - max_length=128, default='', blank=True, verbose_name=_('Comment') - ) + comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) def __str__(self): return self.name - def convert_type(self): - s3_type_list = [const.REPLAY_STORAGE_TYPE_CEPH] - tp = self.type - if tp in s3_type_list: - tp = const.REPLAY_STORAGE_TYPE_S3 - return tp + @property + def type_null(self): + return self.type == const.ReplayStorageTypeChoices.null.value - def get_extra_config(self): - extra_config = {'TYPE': self.convert_type()} - if self.type == const.REPLAY_STORAGE_TYPE_SWIFT: - extra_config.update({'signer': 'S3SignerType'}) - return extra_config + @property + def type_server(self): + return self.type == const.ReplayStorageTypeChoices.server.value + + @property + def type_null_or_server(self): + return self.type_null or self.type_server + + @property + def type_swift(self): + return self.type == const.ReplayStorageTypeChoices.swift.value + + @property + def type_ceph(self): + return self.type == const.ReplayStorageTypeChoices.ceph.value @property def config(self): - config = self.meta - extra_config = self.get_extra_config() - config.update(extra_config) - return config + _config = {} - def in_defaults(self): - return self.type in self.TYPE_DEFAULTS + # add type config + if self.type_ceph: + _type = const.ReplayStorageTypeChoices.s3.value + else: + _type = self.type + _config.update({'TYPE': _type}) + + # add special config + if self.type_swift: + _config.update({'signer': 'S3SignerType'}) + + # add meta config + _config.update(self.meta) + return _config def is_valid(self): - if self.in_defaults(): + if self.type_null_or_server: return True storage = jms_storage.get_object_storage(self.config) target = 'tests.py' src = os.path.join(settings.BASE_DIR, 'common', target) return storage.is_valid(src, target) - def is_using(self): + def is_use(self): return Terminal.objects.filter(replay_storage=self.name).exists() diff --git a/apps/terminal/serializers/storage.py b/apps/terminal/serializers/storage.py index 0e7db21a2..35606d40f 100644 --- a/apps/terminal/serializers/storage.py +++ b/apps/terminal/serializers/storage.py @@ -2,74 +2,197 @@ # import copy from rest_framework import serializers - -from common.drf.fields import CustomMetaDictField +from urllib.parse import urlparse +from django.utils.translation import ugettext_lazy as _ +from django.db.models import TextChoices +from common.drf.serializers import MethodSerializer from ..models import ReplayStorage, CommandStorage from .. import const -class ReplayStorageMetaDictField(CustomMetaDictField): - type_fields_map = const.REPLAY_STORAGE_TYPE_FIELDS_MAP - default_type = const.REPLAY_STORAGE_TYPE_SERVER - convert_key_remove_type_prefix = True - convert_key_to_upper = True +# Replay storage serializers +# -------------------------- -class BaseStorageSerializerMixin: - type_fields_map = None - - def process_meta(self, instance, validated_data): - new_meta = copy.deepcopy(validated_data.get('meta', {})) - tp = validated_data.get('type', '') - - if tp != instance.type: - return new_meta - - old_meta = instance.meta - fields = self.type_fields_map.get(instance.type, []) - for field in fields: - if not field.get('write_only', False): - continue - field_name = field['name'] - new_value = new_meta.get(field_name, '') - old_value = old_meta.get(field_name, '') - field_value = new_value if new_value else old_value - new_meta[field_name] = field_value - - return new_meta - - def update(self, instance, validated_data): - meta = self.process_meta(instance, validated_data) - validated_data['meta'] = meta - return super().update(instance, validated_data) +class ReplayStorageTypeBaseSerializer(serializers.Serializer): + BUCKET = serializers.CharField( + required=True, max_length=1024, label=_('Bucket'), allow_null=True + ) + ACCESS_KEY = serializers.CharField( + max_length=1024, required=False, allow_blank=True, write_only=True, label=_('Access key'), + allow_null=True, + ) + SECRET_KEY = serializers.CharField( + max_length=1024, required=False, allow_blank=True, write_only=True, label=_('Secret key'), + allow_null=True, + ) + ENDPOINT = serializers.CharField( + required=True, max_length=1024, label=_('Endpoint'), allow_null=True, + ) -class ReplayStorageSerializer(BaseStorageSerializerMixin, - serializers.ModelSerializer): +class ReplayStorageTypeS3Serializer(ReplayStorageTypeBaseSerializer): + endpoint_help_text = ''' + S3 format: http://s3.{REGION_NAME}.amazonaws.com + S3(China) format: http://s3.{REGION_NAME}.amazonaws.com.cn + Such as: http://s3.cn-north-1.amazonaws.com.cn + ''' + ENDPOINT = serializers.CharField( + required=True, max_length=1024, label=_('Endpoint'), help_text=_(endpoint_help_text), + allow_null=True, + ) - meta = ReplayStorageMetaDictField() - type_fields_map = const.REPLAY_STORAGE_TYPE_FIELDS_MAP +class ReplayStorageTypeCephSerializer(ReplayStorageTypeBaseSerializer): + pass + + +class ReplayStorageTypeSwiftSerializer(ReplayStorageTypeBaseSerializer): + class ProtocolChoices(TextChoices): + http = 'http', 'http' + https = 'https', 'https' + + REGION = serializers.CharField( + required=True, max_length=1024, label=_('Region'), allow_null=True + ) + PROTOCOL = serializers.ChoiceField( + choices=ProtocolChoices.choices, default=ProtocolChoices.http.value, label=_('Protocol'), + allow_null=True, + ) + + +class ReplayStorageTypeOSSSerializer(ReplayStorageTypeBaseSerializer): + endpoint_help_text = ''' + OSS format: http://{REGION_NAME}.aliyuncs.com + Such as: http://oss-cn-hangzhou.aliyuncs.com + ''' + ENDPOINT = serializers.CharField( + max_length=1024, label=_('Endpoint'), help_text=_(endpoint_help_text), allow_null=True, + ) + + +class ReplayStorageTypeAzureSerializer(serializers.Serializer): + class EndpointSuffixChoices(TextChoices): + china = 'core.chinacloudapi.cn', 'core.chinacloudapi.cn' + international = 'core.windows.net', 'core.windows.net' + + CONTAINER_NAME = serializers.CharField(max_length=1024, label=_('Container'), allow_null=True) + ACCOUNT_NAME = serializers.CharField(max_length=1024, label=_('Account name'), allow_null=True) + ACCOUNT_KEY = serializers.CharField(max_length=1024, label=_('Account key'), allow_null=True) + ENDPOINT_SUFFIX = serializers.ChoiceField( + choices=EndpointSuffixChoices.choices, default=EndpointSuffixChoices.china.value, + label=_('Endpoint suffix'), allow_null=True, + ) + +# mapping + + +replay_storage_type_serializer_classes_mapping = { + const.ReplayStorageTypeChoices.s3.value: ReplayStorageTypeS3Serializer, + const.ReplayStorageTypeChoices.ceph.value: ReplayStorageTypeCephSerializer, + const.ReplayStorageTypeChoices.swift.value: ReplayStorageTypeSwiftSerializer, + const.ReplayStorageTypeChoices.oss.value: ReplayStorageTypeOSSSerializer, + const.ReplayStorageTypeChoices.azure.value: ReplayStorageTypeAzureSerializer +} + +# ReplayStorageSerializer + + +class ReplayStorageSerializer(serializers.ModelSerializer): + meta = MethodSerializer() class Meta: model = ReplayStorage fields = ['id', 'name', 'type', 'meta', 'comment'] + def validate_meta(self, meta): + _meta = self.instance.meta if self.instance else {} + _meta.update(meta) + return _meta -class CommandStorageMetaDictField(CustomMetaDictField): - type_fields_map = const.COMMAND_STORAGE_TYPE_FIELDS_MAP - default_type = const.COMMAND_STORAGE_TYPE_SERVER - convert_key_remove_type_prefix = True - convert_key_to_upper = True + def get_meta_serializer(self): + serializer_class = None + query_type = self.context['request'].query_params.get('type') + if query_type: + serializer_class = replay_storage_type_serializer_classes_mapping.get(query_type) + if isinstance(self.instance, ReplayStorage): + instance_type = self.instance.type + serializer_class = replay_storage_type_serializer_classes_mapping.get(instance_type) + if serializer_class is None: + serializer_class = serializers.Serializer + serializer = serializer_class() + return serializer -class CommandStorageSerializer(BaseStorageSerializerMixin, - serializers.ModelSerializer): +# Command storage serializers +# --------------------------- - meta = CommandStorageMetaDictField() - type_fields_map = const.COMMAND_STORAGE_TYPE_FIELDS_MAP +def es_host_format_validator(host): + h = urlparse(host) + default_error_msg = _('The address format is incorrect') + if h.scheme not in ['http', 'https']: + raise serializers.ValidationError(default_error_msg) + if ':' not in h.netloc: + raise serializers.ValidationError(default_error_msg) + _host, _port = h.netloc.split(':') + if not _host: + error_msg = _('Host invalid') + raise serializers.ValidationError(error_msg) + if not _port.isdigit(): + error_msg = _('Port invalid') + raise serializers.ValidationError(error_msg) + return host + + +class CommandStorageTypeESSerializer(serializers.Serializer): + + hosts_help_text = ''' + Tip: If there are multiple hosts, use a comma (,) to separate them. + (eg: http://www.jumpserver.a.com, http://www.jumpserver.b.com) + ''' + HOSTS = serializers.ListField( + child=serializers.CharField(validators=[es_host_format_validator]), label=_('Hosts'), + help_text=_(hosts_help_text), allow_null=True + ) + INDEX = serializers.CharField( + max_length=1024, default='jumpserver', label=_('Index'), allow_null=True + ) + DOC_TYPE = serializers.CharField( + max_length=1024, read_only=True, default='command', label=_('Doc type'), allow_null=True + ) + +# mapping + + +command_storage_type_serializer_classes_mapping = { + const.CommandStorageTypeChoices.es.value: CommandStorageTypeESSerializer +} + +# CommandStorageSerializer + + +class CommandStorageSerializer(serializers.ModelSerializer): + meta = MethodSerializer() class Meta: model = CommandStorage fields = ['id', 'name', 'type', 'meta', 'comment'] + + def validate_meta(self, meta): + _meta = self.instance.meta if self.instance else {} + _meta.update(meta) + return _meta + + def get_meta_serializer(self): + serializer_class = None + query_type = self.context['request'].query_params.get('type') + if query_type: + serializer_class = command_storage_type_serializer_classes_mapping.get(query_type) + if isinstance(self.instance, CommandStorage): + instance_type = self.instance.type + serializer_class = command_storage_type_serializer_classes_mapping.get(instance_type) + if serializer_class is None: + serializer_class = serializers.Serializer + serializer = serializer_class() + return serializer diff --git a/apps/terminal/tasks.py b/apps/terminal/tasks.py index 7efa7665d..b743f10f9 100644 --- a/apps/terminal/tasks.py +++ b/apps/terminal/tasks.py @@ -73,7 +73,6 @@ def clean_expired_session_period(): logger.info("Clean session replay done") - @shared_task def upload_session_replay_to_external_storage(session_id): logger.info(f'Start upload session to external storage: {session_id}') diff --git a/apps/terminal/utils.py b/apps/terminal/utils.py index 4f346868d..d2e5b8e91 100644 --- a/apps/terminal/utils.py +++ b/apps/terminal/utils.py @@ -39,7 +39,7 @@ def download_session_replay(session): configs = { storage.name: storage.config for storage in replay_storages - if not storage.in_defaults() + if not storage.type_null_or_server } if settings.SERVER_REPLAY_STORAGE: configs['SERVER_REPLAY_STORAGE'] = settings.SERVER_REPLAY_STORAGE diff --git a/apps/tickets/api/ticket.py b/apps/tickets/api/ticket.py index 3d68387da..775baccba 100644 --- a/apps/tickets/api/ticket.py +++ b/apps/tickets/api/ticket.py @@ -19,7 +19,7 @@ __all__ = ['TicketViewSet'] class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet): permission_classes = (IsValidUser,) - serializer_class = serializers.TicketSerializer + serializer_class = serializers.TicketDisplaySerializer serializer_classes = { 'default': serializers.TicketDisplaySerializer, 'display': serializers.TicketDisplaySerializer, diff --git a/apps/tickets/models/ticket/mixin/meta/apply_application.py b/apps/tickets/models/ticket/mixin/meta/apply_application.py index 35fefb024..000cd7338 100644 --- a/apps/tickets/models/ticket/mixin/meta/apply_application.py +++ b/apps/tickets/models/ticket/mixin/meta/apply_application.py @@ -10,9 +10,9 @@ from tickets.utils import convert_model_instance_data_field_name_to_verbose_name class ConstructDisplayFieldMixin: def construct_meta_apply_application_open_fields_display(self): meta_display_fields = ['apply_category_display', 'apply_type_display'] - apply_category = self.meta['apply_category'] + apply_category = self.meta.get('apply_category') apply_category_display = ApplicationCategoryChoices.get_label(apply_category) - apply_type = self.meta['apply_type'] + apply_type = self.meta.get('apply_type') apply_type_display = ApplicationTypeChoices.get_label(apply_type) meta_display_values = [apply_category_display, apply_type_display] meta_display = dict(zip(meta_display_fields, meta_display_values)) @@ -20,8 +20,8 @@ class ConstructDisplayFieldMixin: def construct_meta_apply_application_approve_fields_display(self): meta_display_fields = ['approve_applications_snapshot', 'approve_system_users_snapshot'] - approve_applications_id = self.meta['approve_applications'] - approve_system_users_id = self.meta['approve_system_users'] + approve_applications_id = self.meta.get('approve_applications', []) + approve_system_users_id = self.meta.get('approve_system_users', []) with tmp_to_org(self.org_id): approve_applications_snapshot = list( Application.objects.filter(id__in=approve_applications_id).values( @@ -42,12 +42,12 @@ class ConstructDisplayFieldMixin: class ConstructBodyMixin: def construct_apply_application_applied_body(self): - apply_category_display = self.meta['apply_category_display'] - apply_type_display = self.meta['apply_type_display'] - apply_application_group = self.meta['apply_application_group'] - apply_system_user_group = self.meta['apply_system_user_group'] - apply_date_start = self.meta['apply_date_start'] - apply_date_expired = self.meta['apply_date_expired'] + apply_category_display = self.meta.get('apply_category_display') + apply_type_display = self.meta.get('apply_type_display') + apply_application_group = self.meta.get('apply_application_group', []) + apply_system_user_group = self.meta.get('apply_system_user_group', []) + apply_date_start = self.meta.get('apply_date_start') + apply_date_expired = self.meta.get('apply_date_expired') applied_body = '''{}: {}, {}: {}, {}: {}, @@ -66,16 +66,16 @@ class ConstructBodyMixin: def construct_apply_application_approved_body(self): # 审批信息 - approve_applications_snapshot = self.meta['approve_applications_snapshot'] + approve_applications_snapshot = self.meta.get('approve_applications_snapshot', []) approve_applications_snapshot_display = convert_model_instance_data_field_name_to_verbose_name( Application, approve_applications_snapshot ) - approve_system_users_snapshot = self.meta['approve_system_users_snapshot'] + approve_system_users_snapshot = self.meta.get('approve_system_users_snapshot', []) approve_system_users_snapshot_display = convert_model_instance_data_field_name_to_verbose_name( SystemUser, approve_system_users_snapshot ) - approve_date_start = self.meta['approve_date_start'] - approve_date_expired = self.meta['approve_date_expired'] + approve_date_start = self.meta.get('approve_date_start') + approve_date_expired = self.meta.get('approve_date_expired') approved_body = '''{}: {}, {}: {}, {}: {}, @@ -97,12 +97,12 @@ class CreatePermissionMixin: if application_permission: return application_permission - apply_category = self.meta['apply_category'] - apply_type = self.meta['apply_type'] - approved_applications_id = self.meta['approve_applications'] - approve_system_users_id = self.meta['approve_system_users'] - approve_date_start = self.meta['approve_date_start'] - approve_date_expired = self.meta['approve_date_expired'] + apply_category = self.meta.get('apply_category') + apply_type = self.meta.get('apply_type') + approved_applications_id = self.meta.get('approve_applications', []) + approve_system_users_id = self.meta.get('approve_system_users', []) + approve_date_start = self.meta.get('approve_date_start') + approve_date_expired = self.meta.get('approve_date_expired') permission_name = '{}({})'.format( __('Created by ticket ({})'.format(self.title)), str(self.id)[:4] ) diff --git a/apps/tickets/models/ticket/mixin/meta/apply_asset.py b/apps/tickets/models/ticket/mixin/meta/apply_asset.py index 15a8a2b90..eeae032e9 100644 --- a/apps/tickets/models/ticket/mixin/meta/apply_asset.py +++ b/apps/tickets/models/ticket/mixin/meta/apply_asset.py @@ -10,7 +10,7 @@ class ConstructDisplayFieldMixin: def construct_meta_apply_asset_open_fields_display(self): meta_display_fields = ['apply_actions_display'] - apply_actions = self.meta['apply_actions'] + apply_actions = self.meta.get('apply_actions', Action.NONE) apply_actions_display = Action.value_to_choices_display(apply_actions) meta_display_values = [apply_actions_display] @@ -21,10 +21,10 @@ class ConstructDisplayFieldMixin: meta_display_fields = [ 'approve_actions_display', 'approve_assets_snapshot', 'approve_system_users_snapshot' ] - approve_actions = self.meta['approve_actions'] - approve_assets_id = self.meta['approve_assets'] - approve_system_users_id = self.meta['approve_system_users'] + approve_actions = self.meta.get('approve_actions', Action.NONE) approve_actions_display = Action.value_to_choices_display(approve_actions) + approve_assets_id = self.meta.get('approve_assets', []) + approve_system_users_id = self.meta.get('approve_system_users', []) with tmp_to_org(self.org_id): approve_assets_snapshot = list( Asset.objects.filter(id__in=approve_assets_id).values( @@ -46,12 +46,12 @@ class ConstructDisplayFieldMixin: class ConstructBodyMixin: def construct_apply_asset_applied_body(self): - apply_ip_group = self.meta['apply_ip_group'] - apply_hostname_group = self.meta['apply_hostname_group'] - apply_system_user_group = self.meta['apply_system_user_group'] - apply_actions_display = self.meta['apply_actions_display'] - apply_date_start = self.meta['apply_date_start'] - apply_date_expired = self.meta['apply_date_expired'] + apply_ip_group = self.meta.get('apply_ip_group', []) + apply_hostname_group = self.meta.get('apply_hostname_group', []) + apply_system_user_group = self.meta.get('apply_system_user_group', []) + apply_actions_display = self.meta.get('apply_actions_display', []) + apply_date_start = self.meta.get('apply_date_start') + apply_date_expired = self.meta.get('apply_date_expired') applied_body = '''{}: {}, {}: {}, {}: {}, @@ -68,17 +68,17 @@ class ConstructBodyMixin: return applied_body def construct_apply_asset_approved_body(self): - approve_assets_snapshot = self.meta['approve_assets_snapshot'] + approve_assets_snapshot = self.meta.get('approve_assets_snapshot', []) approve_assets_snapshot_display = convert_model_instance_data_field_name_to_verbose_name( Asset, approve_assets_snapshot ) - approve_system_users_snapshot = self.meta['approve_system_users_snapshot'] + approve_system_users_snapshot = self.meta.get('approve_system_users_snapshot', []) approve_system_users_snapshot_display = convert_model_instance_data_field_name_to_verbose_name( SystemUser, approve_system_users_snapshot ) - approve_actions_display = self.meta['approve_actions_display'] - approve_date_start = self.meta['approve_date_start'] - approve_date_expired = self.meta['approve_date_expired'] + approve_actions_display = self.meta.get('approve_actions_display', []) + approve_date_start = self.meta.get('approve_date_start') + approve_date_expired = self.meta.get('approve_date_expired') approved_body = '''{}: {}, {}: {}, {}: {}, @@ -101,11 +101,11 @@ class CreatePermissionMixin: if asset_permission: return asset_permission - approve_assets_id = self.meta['approve_assets'] - approve_system_users_id = self.meta['approve_system_users'] - approve_actions = self.meta['approve_actions'] - approve_date_start = self.meta['approve_date_start'] - approve_date_expired = self.meta['approve_date_expired'] + approve_assets_id = self.meta.get('approve_assets', []) + approve_system_users_id = self.meta.get('approve_system_users', []) + approve_actions = self.meta.get('approve_actions', Action.NONE) + approve_date_start = self.meta.get('approve_date_start') + approve_date_expired = self.meta.get('approve_date_expired') permission_name = '{}({})'.format( __('Created by ticket ({})'.format(self.title)), str(self.id)[:4] ) diff --git a/apps/tickets/models/ticket/mixin/meta/login_confirm.py b/apps/tickets/models/ticket/mixin/meta/login_confirm.py index 92ac57194..681739d26 100644 --- a/apps/tickets/models/ticket/mixin/meta/login_confirm.py +++ b/apps/tickets/models/ticket/mixin/meta/login_confirm.py @@ -4,9 +4,9 @@ from django.utils.translation import ugettext as __ class ConstructBodyMixin: def construct_login_confirm_applied_body(self): - apply_login_ip = self.meta['apply_login_ip'] - apply_login_city = self.meta['apply_login_city'] - apply_login_datetime = self.meta['apply_login_datetime'] + apply_login_ip = self.meta.get('apply_login_ip') + apply_login_city = self.meta.get('apply_login_city') + apply_login_datetime = self.meta.get('apply_login_datetime') applied_body = '''{}: {}, {}: {}, {}: {} diff --git a/apps/tickets/serializers/ticket/meta/meta.py b/apps/tickets/serializers/ticket/meta/meta.py index 63a25207d..1b3c071a8 100644 --- a/apps/tickets/serializers/ticket/meta/meta.py +++ b/apps/tickets/serializers/ticket/meta/meta.py @@ -17,14 +17,17 @@ action_approve = const.TicketActionChoices.approve.value type_serializer_classes_mapping = { const.TicketTypeChoices.apply_asset.value: { + 'default': apply_asset.ApplyAssetSerializer, action_open: apply_asset.ApplySerializer, action_approve: apply_asset.ApproveSerializer, }, const.TicketTypeChoices.apply_application.value: { + 'default': apply_application.ApplyApplicationSerializer, action_open: apply_application.ApplySerializer, action_approve: apply_application.ApproveSerializer, }, const.TicketTypeChoices.login_confirm.value: { + 'default': login_confirm.LoginConfirmSerializer, action_open: login_confirm.ApplySerializer, } } diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py b/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py index f9b516970..50cd05a8f 100644 --- a/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py +++ b/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py @@ -1,88 +1,148 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ - +from django.db.models import Q from applications.models import Application from applications.const import ApplicationCategoryChoices, ApplicationTypeChoices from assets.models import SystemUser -from .mixin import BaseApproveSerializerMixin +from orgs.utils import tmp_to_org +from tickets.models import Ticket __all__ = [ - 'ApplyApplicationTypeSerializer', 'ApplySerializer', 'ApproveSerializer', + 'ApplyApplicationSerializer', 'ApplySerializer', 'ApproveSerializer', ] class ApplySerializer(serializers.Serializer): # 申请信息 apply_category = serializers.ChoiceField( - required=True, choices=ApplicationCategoryChoices.choices, label=_('Category') + required=True, choices=ApplicationCategoryChoices.choices, label=_('Category'), + allow_null=True, ) apply_category_display = serializers.CharField( - read_only=True, label=_('Category display') + read_only=True, label=_('Category display'), allow_null=True, ) apply_type = serializers.ChoiceField( - required=True, choices=ApplicationTypeChoices.choices, label=_('Type') + required=True, choices=ApplicationTypeChoices.choices, label=_('Type'), + allow_null=True ) apply_type_display = serializers.CharField( - required=False, read_only=True, label=_('Type display') + required=False, read_only=True, label=_('Type display'), + allow_null=True ) apply_application_group = serializers.ListField( required=False, child=serializers.CharField(), label=_('Application group'), - default=list, + default=list, allow_null=True ) apply_system_user_group = serializers.ListField( required=False, child=serializers.CharField(), label=_('System user group'), - default=list, + default=list, allow_null=True ) apply_date_start = serializers.DateTimeField( - required=True, label=_('Date start') + required=True, label=_('Date start'), allow_null=True ) apply_date_expired = serializers.DateTimeField( - required=True, label=_('Date expired') + required=True, label=_('Date expired'), allow_null=True ) -class ApproveSerializer(BaseApproveSerializerMixin, serializers.Serializer): +class ApproveSerializer(serializers.Serializer): # 审批信息 approve_applications = serializers.ListField( - required=True, child=serializers.UUIDField(), label=_('Approve applications') + required=True, child=serializers.UUIDField(), label=_('Approve applications'), + allow_null=True ) approve_applications_snapshot = serializers.ListField( required=False, read_only=True, child=serializers.CharField(), - label=_('Approve applications display'), + label=_('Approve applications display'), allow_null=True, default=list ) approve_system_users = serializers.ListField( - required=True, child=serializers.UUIDField(), label=_('Approve system users') + required=True, child=serializers.UUIDField(), label=_('Approve system users'), + allow_null=True ) approve_system_users_snapshot = serializers.ListField( required=False, read_only=True, child=serializers.CharField(), - label=_('Approve system user display'), + label=_('Approve system user display'), allow_null=True, default=list ) approve_date_start = serializers.DateTimeField( - required=True, label=_('Date start') + required=True, label=_('Date start'), allow_null=True ) approve_date_expired = serializers.DateTimeField( - required=True, label=_('Date expired') + required=True, label=_('Date expired'), allow_null=True ) def validate_approve_applications(self, approve_applications): - application_type = self.root.instance.meta['apply_type'] - queries = {'type': application_type} - applications_id = self.filter_approve_resources( - resource_model=Application, resources_id=approve_applications, queries=queries - ) - return applications_id + if not isinstance(self.root.instance, Ticket): + return [] + + with tmp_to_org(self.root.instance.org_id): + apply_type = self.root.instance.meta.get('apply_type') + queries = Q(type=apply_type) + queries &= Q(id__in=approve_applications) + applications_id = Application.objects.filter(queries).values_list('id', flat=True) + applications_id = [str(application_id) for application_id in applications_id] + if applications_id: + return applications_id + + raise serializers.ValidationError(_( + 'No `Application` are found under Organization `{}`'.format(self.root.instance.org_name) + )) def validate_approve_system_users(self, approve_system_users): - application_type = self.root.instance.meta['apply_type'] - protocol = SystemUser.get_protocol_by_application_type(application_type) - queries = {'protocol': protocol} - system_users_id = self.filter_approve_system_users(approve_system_users, queries) - return system_users_id + if not isinstance(self.root.instance, Ticket): + return [] + + with tmp_to_org(self.root.instance.org_id): + apply_type = self.root.instance.meta.get('apply_type') + protocol = SystemUser.get_protocol_by_application_type(apply_type) + queries = Q(protocol=protocol) + queries &= Q(id__in=approve_system_users) + system_users_id = SystemUser.objects.filter(queries).values_list('id', flat=True) + system_users_id = [str(system_user_id) for system_user_id in system_users_id] + if system_users_id: + return system_users_id + + raise serializers.ValidationError(_( + 'No `SystemUser` are found under Organization `{}`'.format(self.root.instance.org_name) + )) -class ApplyApplicationTypeSerializer(ApplySerializer, ApproveSerializer): - pass +class ApplyApplicationSerializer(ApplySerializer, ApproveSerializer): + # 推荐信息 + recommend_applications = serializers.SerializerMethodField() + recommend_system_users = serializers.SerializerMethodField() + def get_recommend_applications(self, value): + if not isinstance(self.root.instance, Ticket): + return [] + apply_application_group = value.get('apply_application_group', []) + apply_type = value.get('apply_type') + queries = Q() + for application in apply_application_group: + queries |= Q(name__icontains=application) + queries &= Q(type=apply_type) + + with tmp_to_org(self.root.instance.org_id): + applications_id = Application.objects.filter(queries).values_list('id', flat=True)[:5] + applications_id = [str(application_id) for application_id in applications_id] + return applications_id + + def get_recommend_system_users(self, value): + if not isinstance(self.root.instance, Ticket): + return [] + + apply_type = value.get('apply_type') + apply_system_user_group = value.get('apply_system_user_group', []) + protocol = SystemUser.get_protocol_by_application_type(apply_type) + queries = Q() + for system_user in apply_system_user_group: + queries |= Q(username__icontains=system_user) + queries |= Q(name__icontains=system_user) + queries &= Q(protocol=protocol) + + with tmp_to_org(self.root.instance.org_id): + system_users_id = SystemUser.objects.filter(queries).values_list('id', flat=True)[:5] + system_users_id = [str(system_user_id) for system_user_id in system_users_id] + return system_users_id diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py b/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py index dd983bbcf..4d2104ff4 100644 --- a/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py +++ b/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py @@ -1,8 +1,10 @@ from django.utils.translation import ugettext_lazy as _ +from django.db.models import Q from rest_framework import serializers from perms.serializers import ActionsField from assets.models import Asset, SystemUser -from .mixin import BaseApproveSerializerMixin +from orgs.utils import tmp_to_org +from tickets.models import Ticket __all__ = [ @@ -14,74 +16,129 @@ class ApplySerializer(serializers.Serializer): # 申请信息 apply_ip_group = serializers.ListField( required=False, child=serializers.IPAddressField(), label=_('IP group'), - default=list, + default=list, allow_null=True, ) apply_hostname_group = serializers.ListField( required=False, child=serializers.CharField(), label=_('Hostname group'), - default=list, + default=list, allow_null=True, ) apply_system_user_group = serializers.ListField( required=False, child=serializers.CharField(), label=_('System user group'), - default=list, + default=list, allow_null=True ) apply_actions = ActionsField( - required=True + required=True, allow_null=True ) apply_actions_display = serializers.ListField( required=False, read_only=True, child=serializers.CharField(), - label=_('Approve assets display'), + label=_('Approve assets display'), allow_null=True, default=list, ) apply_date_start = serializers.DateTimeField( - required=True, label=_('Date start') + required=True, label=_('Date start'), allow_null=True, ) apply_date_expired = serializers.DateTimeField( - required=True, label=_('Date expired') + required=True, label=_('Date expired'), allow_null=True, ) -class ApproveSerializer(BaseApproveSerializerMixin, serializers.Serializer): +class ApproveSerializer(serializers.Serializer): # 审批信息 approve_assets = serializers.ListField( - required=True, child=serializers.UUIDField(), label=_('Approve assets') + required=True, allow_null=True, child=serializers.UUIDField(), label=_('Approve assets') ) approve_assets_snapshot = serializers.ListField( required=False, read_only=True, child=serializers.DictField(), - label=_('Approve assets display'), + label=_('Approve assets display'), allow_null=True, default=list, ) approve_system_users = serializers.ListField( - required=True, child=serializers.UUIDField(), label=_('Approve system users') + required=True, allow_null=True, child=serializers.UUIDField(), + label=_('Approve system users') ) approve_system_users_snapshot = serializers.ListField( required=False, read_only=True, child=serializers.DictField(), - label=_('Approve assets display'), + label=_('Approve assets display'), allow_null=True, default=list, ) approve_actions = ActionsField( - required=True + required=True, allow_null=True, ) approve_actions_display = serializers.ListField( required=False, read_only=True, child=serializers.CharField(), - label=_('Approve assets display'), + label=_('Approve assets display'), allow_null=True, default=list, ) approve_date_start = serializers.DateTimeField( - required=True, label=_('Date start') + required=True, label=_('Date start'), allow_null=True, ) approve_date_expired = serializers.DateTimeField( - required=True, label=_('Date expired') + required=True, label=_('Date expired'), allow_null=True ) def validate_approve_assets(self, approve_assets): - assets_id = self.filter_approve_resources(resource_model=Asset, resources_id=approve_assets) - return assets_id + if not isinstance(self.root.instance, Ticket): + return [] + + with tmp_to_org(self.root.instance.org_id): + assets_id = Asset.objects.filter(id__in=approve_assets).values_list('id', flat=True) + assets_id = [str(asset_id) for asset_id in assets_id] + if assets_id: + return assets_id + + raise serializers.ValidationError(_( + 'No `Asset` are found under Organization `{}`'.format(self.root.instance.org_name) + )) def validate_approve_system_users(self, approve_system_users): - queries = {'protocol__in': SystemUser.ASSET_CATEGORY_PROTOCOLS} - system_users_id = self.filter_approve_system_users(approve_system_users, queries) - return system_users_id + if not isinstance(self.root.instance, Ticket): + return [] + + with tmp_to_org(self.root.instance.org_id): + queries = Q(protocol__in=SystemUser.ASSET_CATEGORY_PROTOCOLS) + queries &= Q(id__in=approve_system_users) + system_users_id = SystemUser.objects.filter(queries).values_list('id', flat=True) + system_users_id = [str(system_user_id) for system_user_id in system_users_id] + if system_users_id: + return system_users_id + + raise serializers.ValidationError(_( + 'No `Asset` are found under Organization `{}`'.format(self.root.instance.org_name) + )) class ApplyAssetSerializer(ApplySerializer, ApproveSerializer): - pass + # 推荐信息 + recommend_assets = serializers.SerializerMethodField() + recommend_system_users = serializers.SerializerMethodField() + + def get_recommend_assets(self, value): + if not isinstance(self.root.instance, Ticket): + return [] + + apply_ip_group = value.get('apply_ip_group', []) + apply_hostname_group = value.get('apply_hostname_group', []) + queries = Q(ip__in=apply_ip_group) + for hostname in apply_hostname_group: + queries |= Q(hostname__icontains=hostname) + + with tmp_to_org(self.root.instance.org_id): + assets_id = Asset.objects.filter(queries).values_list('id', flat=True)[:5] + assets_id = [str(asset_id) for asset_id in assets_id] + return assets_id + + def get_recommend_system_users(self, value): + if not isinstance(self.root.instance, Ticket): + return [] + + apply_system_user_group = value.get('apply_system_user_group', []) + queries = Q() + for system_user in apply_system_user_group: + queries |= Q(username__icontains=system_user) + queries |= Q(name__icontains=system_user) + queries &= Q(protocol__in=SystemUser.ASSET_CATEGORY_PROTOCOLS) + + with tmp_to_org(self.root.instance.org_id): + system_users_id = SystemUser.objects.filter(queries).values_list('id', flat=True)[:5] + system_users_id = [str(system_user_id) for system_user_id in system_users_id] + return system_users_id diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/login_confirm.py b/apps/tickets/serializers/ticket/meta/ticket_type/login_confirm.py index 27bcad02f..9308d0ee2 100644 --- a/apps/tickets/serializers/ticket/meta/ticket_type/login_confirm.py +++ b/apps/tickets/serializers/ticket/meta/ticket_type/login_confirm.py @@ -4,19 +4,22 @@ from django.utils.translation import ugettext_lazy as _ __all__ = [ - 'ApplySerializer', + 'ApplySerializer', 'LoginConfirmSerializer', ] class ApplySerializer(serializers.Serializer): # 申请信息 apply_login_ip = serializers.IPAddressField( - required=True, label=_('Login ip') + required=True, label=_('Login ip'), allow_null=True ) apply_login_city = serializers.CharField( - required=True, max_length=64, label=_('Login city') + required=True, max_length=64, label=_('Login city'), allow_null=True ) apply_login_datetime = serializers.DateTimeField( - required=True, label=_('Login datetime') + required=True, label=_('Login datetime'), allow_null=True ) + +class LoginConfirmSerializer(ApplySerializer): + pass diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/mixin.py b/apps/tickets/serializers/ticket/meta/ticket_type/mixin.py deleted file mode 100644 index 9539267fb..000000000 --- a/apps/tickets/serializers/ticket/meta/ticket_type/mixin.py +++ /dev/null @@ -1,39 +0,0 @@ -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from orgs.utils import tmp_to_org -from assets.models import SystemUser - - -class BaseApproveSerializerMixin: - - def _filter_approve_resources_by_org(self, model, resources_id): - with tmp_to_org(self.root.instance.org_id): - org_resources = model.objects.filter(id__in=resources_id) - if not org_resources: - error = _('None of the approved `{}` belong to Organization `{}`' - ''.format(model.__name__, self.root.instance.org_name)) - raise serializers.ValidationError(error) - return org_resources - - @staticmethod - def _filter_approve_resources_by_queries(model, resources, queries=None): - if queries: - resources = resources.filter(**queries) - if not resources: - error = _('None of the approved `{}` does not comply with the filtering rules `{}`' - ''.format(model.__name__, queries)) - raise serializers.ValidationError(error) - return resources - - def filter_approve_resources(self, resource_model, resources_id, queries=None): - resources = self._filter_approve_resources_by_org(resource_model, resources_id) - resources = self._filter_approve_resources_by_queries(resource_model, resources, queries) - resources_id = list(resources.values_list('id', flat=True)) - return resources_id - - def filter_approve_system_users(self, system_users_id, queries=None): - system_users_id = self.filter_approve_resources( - resource_model=SystemUser, resources_id=system_users_id, queries=queries - ) - return system_users_id diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py index 35a49048d..521d8dc3e 100644 --- a/apps/tickets/serializers/ticket/ticket.py +++ b/apps/tickets/serializers/ticket/ticket.py @@ -22,6 +22,9 @@ class TicketSerializer(OrgResourceModelSerializerMixin): type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type')) action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action')) status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status')) + action = ReadableHiddenField(default=const.TicketActionChoices.open.value) + applicant = ReadableHiddenField(default=serializers.CurrentUserDefault()) + processor = ReadableHiddenField(default=serializers.CurrentUserDefault()) meta = MethodSerializer() class Meta: @@ -38,48 +41,55 @@ class TicketSerializer(OrgResourceModelSerializerMixin): def get_meta_serializer(self): request = self.context['request'] - view = self.context['view'] - query_type = request.query_params.get('type') - query_action = request.query_params.get('action') - view_action = view.action - action = query_action if query_action else view_action - if query_type: - serializer_class = type_serializer_classes_mapping.get(query_type, {}).get(action) + default_serializer_class = serializers.Serializer + if isinstance(self.instance, Ticket): + _type = self.instance.type else: - serializer_class = None - if serializer_class is None: - serializer_class = serializers.Serializer - serializer = serializer_class() - return serializer + _type = request.query_params.get('type') + + if not _type: + return default_serializer_class() + + action_serializer_classes_mapping = type_serializer_classes_mapping.get(_type) + if not action_serializer_classes_mapping: + return default_serializer_class() + + query_action = request.query_params.get('action') + _action = query_action if query_action else self.context['view'].action + serializer_class = action_serializer_classes_mapping.get(_action) + if serializer_class: + return serializer_class() + + serializer_class = action_serializer_classes_mapping.get('default') + if serializer_class: + return serializer_class() + + return default_serializer_class() class TicketDisplaySerializer(TicketSerializer): - class Meta(TicketSerializer.Meta): + class Meta: + model = Ticket + fields = TicketSerializer.Meta.fields read_only_fields = TicketSerializer.Meta.fields -class TicketActionSerializer(TicketSerializer): - action = ReadableHiddenField(default=const.TicketActionChoices.open.value) - - class Meta(TicketSerializer.Meta): - required_fields = ['action'] - read_only_fields = list(set(TicketDisplaySerializer.Meta.fields) - set(required_fields)) - - -class TicketApplySerializer(TicketActionSerializer): - applicant = ReadableHiddenField(default=serializers.CurrentUserDefault()) +class TicketApplySerializer(TicketSerializer): org_id = serializers.CharField( max_length=36, allow_blank=True, required=True, label=_("Organization") ) - class Meta(TicketActionSerializer.Meta): - required_fields = TicketActionSerializer.Meta.required_fields + [ - 'id', 'title', 'type', 'applicant', 'meta', 'assignees', 'comment', 'org_id' + class Meta: + model = Ticket + fields = TicketSerializer.Meta.fields + required_fields = [ + 'id', 'title', 'type', 'applicant', 'action', 'meta', 'assignees', 'comment', 'org_id' ] - read_only_fields = list(set(TicketDisplaySerializer.Meta.fields) - set(required_fields)) + read_only_fields = list(set(fields) - set(required_fields)) extra_kwargs = { 'type': {'required': True}, + 'org_id': {'required': True}, } def validate_type(self, tp): @@ -115,37 +125,43 @@ class TicketApplySerializer(TicketActionSerializer): return const.TicketActionChoices.open.value -class TicketProcessSerializer(TicketActionSerializer): - processor = ReadableHiddenField(default=serializers.CurrentUserDefault()) +class TicketApproveSerializer(TicketSerializer): - class Meta(TicketActionSerializer.Meta): - required_fields = TicketActionSerializer.Meta.required_fields + ['processor'] - read_only_fields = list(set(TicketDisplaySerializer.Meta.fields) - set(required_fields)) - - -class TicketApproveSerializer(TicketProcessSerializer): - - class Meta(TicketProcessSerializer.Meta): - required_fields = TicketProcessSerializer.Meta.required_fields + ['meta'] - read_only_fields = list(set(TicketDisplaySerializer.Meta.fields) - set(required_fields)) + class Meta: + model = Ticket + fields = TicketSerializer.Meta.fields + required_fields = ['processor', 'action', 'meta'] + read_only_fields = list(set(fields) - set(required_fields)) def validate_meta(self, meta): - instance_meta = self.instance.meta - instance_meta.update(meta) - return instance_meta + _meta = self.instance.meta if self.instance else {} + _meta.update(meta) + return _meta @staticmethod def validate_action(action): return const.TicketActionChoices.approve.value -class TicketRejectSerializer(TicketProcessSerializer): +class TicketRejectSerializer(TicketSerializer): + meta = MethodSerializer(read_only=True) + + class Meta: + model = Ticket + fields = TicketSerializer.Meta.fields + read_only_fields = fields def validate_action(self, action): return const.TicketActionChoices.reject.value -class TicketCloseSerializer(TicketProcessSerializer): +class TicketCloseSerializer(TicketSerializer): + meta = MethodSerializer(read_only=True) + + class Meta: + model = Ticket + fields = TicketSerializer.Meta.fields + read_only_fields = fields def validate_action(self, action): return const.TicketActionChoices.close.value diff --git a/apps/tickets/signals_handler.py b/apps/tickets/signals_handler.py index 887cc1a79..0afd2a587 100644 --- a/apps/tickets/signals_handler.py +++ b/apps/tickets/signals_handler.py @@ -17,26 +17,27 @@ logger = get_logger(__name__) @receiver(pre_save, sender=Ticket) def on_ticket_pre_save(sender, instance=None, **kwargs): + instance.set_display_fields() + if instance.has_processed: instance.set_status_closed() - instance.set_display_fields() @receiver(post_save, sender=Ticket) -def on_ticket_post_save(sender, instance=None, **kwargs): - instance.create_action_comment() - if instance.action_open: +def on_ticket_post_save(sender, instance=None, created=False, **kwargs): + + if created and instance.action_open: + instance.create_action_comment() instance.create_applied_comment() - return - if instance.action_approve: - instance.create_permission() - instance.create_approved_comment() - logger.debug( - 'Ticket () has processed, send mail to applicant: {}'.format( - instance.title, instance.applicant_display - ) - ) - send_ticket_processed_mail_to_applicant(instance) + + if not created and instance.has_processed: + instance.create_action_comment() + msg = 'Ticket () has processed, send mail to applicant: {}' + logger.debug(msg.format(instance.title, instance.applicant_display)) + send_ticket_processed_mail_to_applicant(instance) + if instance.action_approve: + instance.create_permission() + instance.create_approved_comment() @receiver(m2m_changed, sender=Ticket.assignees.through)