diff --git a/apps/applications/api/__init__.py b/apps/applications/api/__init__.py index 7220c0c12..68f0d1803 100644 --- a/apps/applications/api/__init__.py +++ b/apps/applications/api/__init__.py @@ -1,5 +1,3 @@ from .application import * from .mixin import * from .remote_app import * -from .database_app import * -from .k8s_app import * diff --git a/apps/applications/api/application.py b/apps/applications/api/application.py index ad447798c..cb51ff023 100644 --- a/apps/applications/api/application.py +++ b/apps/applications/api/application.py @@ -3,18 +3,17 @@ from orgs.mixins.api import OrgBulkModelViewSet -from .mixin import ApplicationAttrsSerializerViewMixin from ..hands import IsOrgAdminOrAppUser from .. import models, serializers -__all__ = [ - 'ApplicationViewSet', -] + +__all__ = ['ApplicationViewSet'] -class ApplicationViewSet(ApplicationAttrsSerializerViewMixin, OrgBulkModelViewSet): +class ApplicationViewSet(OrgBulkModelViewSet): model = models.Application - filter_fields = ('name', 'type', 'category') - search_fields = filter_fields + filterset_fields = ('name', 'type', 'category') + search_fields = filterset_fields permission_classes = (IsOrgAdminOrAppUser,) serializer_class = serializers.ApplicationSerializer + diff --git a/apps/applications/api/database_app.py b/apps/applications/api/database_app.py deleted file mode 100644 index af5810e0f..000000000 --- a/apps/applications/api/database_app.py +++ /dev/null @@ -1,20 +0,0 @@ -# coding: utf-8 -# - -from orgs.mixins.api import OrgBulkModelViewSet - -from .. import models -from .. import serializers -from ..hands import IsOrgAdminOrAppUser - -__all__ = [ - 'DatabaseAppViewSet', -] - - -class DatabaseAppViewSet(OrgBulkModelViewSet): - model = models.DatabaseApp - filter_fields = ('name',) - search_fields = filter_fields - permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = serializers.DatabaseAppSerializer diff --git a/apps/applications/api/k8s_app.py b/apps/applications/api/k8s_app.py deleted file mode 100644 index 5cc63b546..000000000 --- a/apps/applications/api/k8s_app.py +++ /dev/null @@ -1,20 +0,0 @@ -# coding: utf-8 -# - -from orgs.mixins.api import OrgBulkModelViewSet - -from .. import models -from .. import serializers -from ..hands import IsOrgAdminOrAppUser - -__all__ = [ - 'K8sAppViewSet', -] - - -class K8sAppViewSet(OrgBulkModelViewSet): - model = models.K8sApp - filter_fields = ('name',) - search_fields = filter_fields - permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = serializers.K8sAppSerializer diff --git a/apps/applications/api/mixin.py b/apps/applications/api/mixin.py index 6bab179a0..dd8bc64c2 100644 --- a/apps/applications/api/mixin.py +++ b/apps/applications/api/mixin.py @@ -1,57 +1,7 @@ -import uuid - -from common.exceptions import JMSException from orgs.models import Organization -from .. import models -class ApplicationAttrsSerializerViewMixin: - - def get_serializer_class(self): - serializer_class = super().get_serializer_class() - if getattr(self, 'swagger_fake_view', False): - return serializer_class - app_type = self.request.query_params.get('type') - app_category = self.request.query_params.get('category') - type_options = list(dict(models.Category.get_all_type_serializer_mapper()).keys()) - category_options = list(dict(models.Category.get_category_serializer_mapper()).keys()) - - # ListAPIView 没有 action 属性 - # 不使用method属性,因为options请求时为method为post - action = getattr(self, 'action', 'list') - - if app_type and app_type not in type_options: - raise JMSException( - 'Invalid query parameter `type`, select from the following options: {}' - ''.format(type_options) - ) - if app_category and app_category not in category_options: - raise JMSException( - 'Invalid query parameter `category`, select from the following options: {}' - ''.format(category_options) - ) - - if action in [ - 'create', 'update', 'partial_update', 'bulk_update', 'partial_bulk_update' - ] and not app_type: - # action: create / update - raise JMSException( - 'The `{}` action must take the `type` query parameter'.format(action) - ) - - if app_type: - # action: create / update / list / retrieve / metadata - attrs_cls = models.Category.get_type_serializer_cls(app_type) - class_name = 'ApplicationDynamicSerializer{}'.format(app_type.title()) - elif app_category: - # action: list / retrieve / metadata - attrs_cls = models.Category.get_category_serializer_cls(app_category) - class_name = 'ApplicationDynamicSerializer{}'.format(app_category.title()) - else: - attrs_cls = models.Category.get_no_password_serializer_cls() - class_name = 'ApplicationDynamicSerializer' - cls = type(class_name, (serializer_class,), {'attrs': attrs_cls()}) - return cls +__all__ = ['SerializeApplicationToTreeNodeMixin'] class SerializeApplicationToTreeNodeMixin: diff --git a/apps/applications/api/remote_app.py b/apps/applications/api/remote_app.py index 9b983449b..c68679bab 100644 --- a/apps/applications/api/remote_app.py +++ b/apps/applications/api/remote_app.py @@ -1,40 +1,19 @@ # coding: utf-8 # -from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics -from common.exceptions import JMSException -from ..hands import IsOrgAdmin, IsAppUser +from ..hands import IsAppUser from .. import models -from ..serializers import RemoteAppSerializer, RemoteAppConnectionInfoSerializer +from ..serializers import RemoteAppConnectionInfoSerializer +from ..permissions import IsRemoteApp __all__ = [ - 'RemoteAppViewSet', 'RemoteAppConnectionInfoApi', + 'RemoteAppConnectionInfoApi', ] -class RemoteAppViewSet(OrgBulkModelViewSet): - model = models.RemoteApp - filter_fields = ('name', 'type', 'comment') - search_fields = filter_fields - permission_classes = (IsOrgAdmin,) - serializer_class = RemoteAppSerializer - - class RemoteAppConnectionInfoApi(generics.RetrieveAPIView): model = models.Application - permission_classes = (IsAppUser, ) + permission_classes = (IsAppUser, IsRemoteApp) serializer_class = RemoteAppConnectionInfoSerializer - - @staticmethod - def check_category_allowed(obj): - if not obj.category_is_remote_app: - raise JMSException( - 'The request instance(`{}`) is not of category `remote_app`'.format(obj.category) - ) - - def get_object(self): - obj = super().get_object() - self.check_category_allowed(obj) - return obj diff --git a/apps/applications/const.py b/apps/applications/const.py index 773f30b9f..151a65b28 100644 --- a/apps/applications/const.py +++ b/apps/applications/const.py @@ -1,64 +1,49 @@ # coding: utf-8 # +from django.db.models import TextChoices from django.utils.translation import ugettext_lazy as _ -# RemoteApp +class ApplicationCategoryChoices(TextChoices): + db = 'db', _('Database') + remote_app = 'remote_app', _('Remote app') + cloud = 'cloud', 'Cloud' -REMOTE_APP_BOOT_PROGRAM_NAME = '||jmservisor' - -REMOTE_APP_TYPE_CHROME = 'chrome' -REMOTE_APP_TYPE_MYSQL_WORKBENCH = 'mysql_workbench' -REMOTE_APP_TYPE_VMWARE_CLIENT = 'vmware_client' -REMOTE_APP_TYPE_CUSTOM = 'custom' - -# Fields attribute write_only default => False - -REMOTE_APP_TYPE_CHROME_FIELDS = [ - {'name': 'chrome_target'}, - {'name': 'chrome_username'}, - {'name': 'chrome_password', 'write_only': True} -] -REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS = [ - {'name': 'mysql_workbench_ip'}, - {'name': 'mysql_workbench_name'}, - {'name': 'mysql_workbench_port'}, - {'name': 'mysql_workbench_username'}, - {'name': 'mysql_workbench_password', 'write_only': True} -] -REMOTE_APP_TYPE_VMWARE_CLIENT_FIELDS = [ - {'name': 'vmware_target'}, - {'name': 'vmware_username'}, - {'name': 'vmware_password', 'write_only': True} -] -REMOTE_APP_TYPE_CUSTOM_FIELDS = [ - {'name': 'custom_cmdline'}, - {'name': 'custom_target'}, - {'name': 'custom_username'}, - {'name': 'custom_password', 'write_only': True} -] - -REMOTE_APP_TYPE_FIELDS_MAP = { - REMOTE_APP_TYPE_CHROME: REMOTE_APP_TYPE_CHROME_FIELDS, - REMOTE_APP_TYPE_MYSQL_WORKBENCH: REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS, - REMOTE_APP_TYPE_VMWARE_CLIENT: REMOTE_APP_TYPE_VMWARE_CLIENT_FIELDS, - REMOTE_APP_TYPE_CUSTOM: REMOTE_APP_TYPE_CUSTOM_FIELDS -} - -REMOTE_APP_TYPE_CHOICES = ( - (REMOTE_APP_TYPE_CHROME, 'Chrome'), - (REMOTE_APP_TYPE_MYSQL_WORKBENCH, 'MySQL Workbench'), - (REMOTE_APP_TYPE_VMWARE_CLIENT, 'vSphere Client'), - (REMOTE_APP_TYPE_CUSTOM, _('Custom')), -) + @classmethod + def get_label(cls, category): + return dict(cls.choices).get(category, '') -# DatabaseApp +class ApplicationTypeChoices(TextChoices): + # db category + mysql = 'mysql', 'MySQL' + oracle = 'oracle', 'Oracle' + pgsql = 'postgresql', 'PostgreSQL' + mariadb = 'mariadb', 'MariaDB' + # remote-app category + chrome = 'chrome', 'Chrome' + mysql_workbench = 'mysql_workbench', 'MySQL Workbench' + vmware_client = 'vmware_client', 'vSphere Client' + custom = 'custom', _('Custom') -DATABASE_APP_TYPE_MYSQL = 'mysql' + # cloud category + k8s = 'k8s', 'Kubernetes' + + @classmethod + def get_label(cls, tp): + return dict(cls.choices).get(tp, '') + + @classmethod + def db_types(cls): + return [cls.mysql.value, cls.oracle.value, cls.pgsql.value, cls.mariadb.value] + + @classmethod + def remote_app_types(cls): + return [cls.chrome.value, cls.mysql_workbench.value, cls.vmware_client.value, cls.custom.value] + + @classmethod + def cloud_types(cls): + return [cls.k8s.value] -DATABASE_APP_TYPE_CHOICES = ( - (DATABASE_APP_TYPE_MYSQL, 'MySQL'), -) diff --git a/apps/applications/migrations/0008_auto_20210104_0435.py b/apps/applications/migrations/0008_auto_20210104_0435.py new file mode 100644 index 000000000..2942ecf6e --- /dev/null +++ b/apps/applications/migrations/0008_auto_20210104_0435.py @@ -0,0 +1,28 @@ +# Generated by Django 3.1 on 2021-01-03 20:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('perms', '0017_auto_20210104_0435'), + ('applications', '0007_auto_20201119_1110'), + ] + + operations = [ + migrations.DeleteModel( + name='DatabaseApp', + ), + migrations.DeleteModel( + name='K8sApp', + ), + migrations.AlterField( + model_name='application', + name='attrs', + field=models.JSONField(default=dict, verbose_name='Attrs'), + ), + migrations.DeleteModel( + name='RemoteApp', + ), + ] diff --git a/apps/applications/models/__init__.py b/apps/applications/models/__init__.py index b232a1ece..a12310aa4 100644 --- a/apps/applications/models/__init__.py +++ b/apps/applications/models/__init__.py @@ -1,4 +1 @@ from .application import * -from .remote_app import * -from .database_app import * -from .k8s_app import * diff --git a/apps/applications/models/application.py b/apps/applications/models/application.py index 1c8ae98f0..12bc4ff0a 100644 --- a/apps/applications/models/application.py +++ b/apps/applications/models/application.py @@ -1,128 +1,24 @@ -from itertools import chain - from django.db import models from django.utils.translation import ugettext_lazy as _ from orgs.mixins.models import OrgModelMixin from common.mixins import CommonModelMixin -from common.db.models import ChoiceSet - - -class DBType(ChoiceSet): - mysql = 'mysql', 'MySQL' - oracle = 'oracle', 'Oracle' - pgsql = 'postgresql', 'PostgreSQL' - mariadb = 'mariadb', 'MariaDB' - - @classmethod - def get_type_serializer_cls_mapper(cls): - from ..serializers import database_app - mapper = { - cls.mysql: database_app.MySQLAttrsSerializer, - cls.oracle: database_app.OracleAttrsSerializer, - cls.pgsql: database_app.PostgreAttrsSerializer, - cls.mariadb: database_app.MariaDBAttrsSerializer, - } - return mapper - - -class RemoteAppType(ChoiceSet): - chrome = 'chrome', 'Chrome' - mysql_workbench = 'mysql_workbench', 'MySQL Workbench' - vmware_client = 'vmware_client', 'vSphere Client' - custom = 'custom', _('Custom') - - @classmethod - def get_type_serializer_cls_mapper(cls): - from ..serializers import remote_app - mapper = { - cls.chrome: remote_app.ChromeAttrsSerializer, - cls.mysql_workbench: remote_app.MySQLWorkbenchAttrsSerializer, - cls.vmware_client: remote_app.VMwareClientAttrsSerializer, - cls.custom: remote_app.CustomRemoteAppAttrsSeralizers, - } - return mapper - - -class CloudType(ChoiceSet): - k8s = 'k8s', 'Kubernetes' - - @classmethod - def get_type_serializer_cls_mapper(cls): - from ..serializers import k8s_app - mapper = { - cls.k8s: k8s_app.K8sAttrsSerializer, - } - return mapper - - -class Category(ChoiceSet): - db = 'db', _('Database') - remote_app = 'remote_app', _('Remote app') - cloud = 'cloud', 'Cloud' - - @classmethod - def get_category_type_mapper(cls): - return { - cls.db: DBType, - cls.remote_app: RemoteAppType, - cls.cloud: CloudType - } - - @classmethod - def get_category_type_choices_mapper(cls): - return { - name: tp.choices - for name, tp in cls.get_category_type_mapper().items() - } - - @classmethod - def get_type_choices(cls, category): - return cls.get_category_type_choices_mapper().get(category, []) - - @classmethod - def get_all_type_choices(cls): - all_grouped_choices = tuple(cls.get_category_type_choices_mapper().values()) - return tuple(chain(*all_grouped_choices)) - - @classmethod - def get_all_type_serializer_mapper(cls): - mapper = {} - for tp in cls.get_category_type_mapper().values(): - mapper.update(tp.get_type_serializer_cls_mapper()) - return mapper - - @classmethod - def get_type_serializer_cls(cls, tp): - mapper = cls.get_all_type_serializer_mapper() - return mapper.get(tp, None) - - @classmethod - def get_category_serializer_mapper(cls): - from ..serializers import remote_app, database_app, k8s_app - return { - cls.db: database_app.DBAttrsSerializer, - cls.remote_app: remote_app.RemoteAppAttrsSerializer, - cls.cloud: k8s_app.CloudAttrsSerializer, - } - - @classmethod - def get_category_serializer_cls(cls, cg): - mapper = cls.get_category_serializer_mapper() - return mapper.get(cg, None) - - @classmethod - def get_no_password_serializer_cls(cls): - from ..serializers import common - return common.NoPasswordSerializer +from .. import const class Application(CommonModelMixin, OrgModelMixin): name = models.CharField(max_length=128, verbose_name=_('Name')) - domain = models.ForeignKey('assets.Domain', null=True, blank=True, related_name='applications', verbose_name=_("Domain"), on_delete=models.SET_NULL) - category = models.CharField(max_length=16, choices=Category.choices, verbose_name=_('Category')) - type = models.CharField(max_length=16, choices=Category.get_all_type_choices(), verbose_name=_('Type')) - attrs = models.JSONField() + category = models.CharField( + max_length=16, choices=const.ApplicationCategoryChoices.choices, verbose_name=_('Category') + ) + type = models.CharField( + max_length=16, choices=const.ApplicationTypeChoices.choices, verbose_name=_('Type') + ) + domain = models.ForeignKey( + 'assets.Domain', null=True, blank=True, related_name='applications', + on_delete=models.SET_NULL, verbose_name=_("Domain"), + ) + attrs = models.JSONField(default=dict, verbose_name=_('Attrs')) comment = models.TextField( max_length=128, default='', blank=True, verbose_name=_('Comment') ) @@ -136,5 +32,6 @@ class Application(CommonModelMixin, OrgModelMixin): type_display = self.get_type_display() return f'{self.name}({type_display})[{category_display}]' - def category_is_remote_app(self): - return self.category == Category.remote_app + @property + def category_remote_app(self): + return self.category == const.ApplicationCategoryChoices.remote_app.value diff --git a/apps/applications/models/database_app.py b/apps/applications/models/database_app.py deleted file mode 100644 index 3317f06a4..000000000 --- a/apps/applications/models/database_app.py +++ /dev/null @@ -1,42 +0,0 @@ -# coding: utf-8 -# - -import uuid -from django.db import models -from django.utils.translation import ugettext_lazy as _ - -from orgs.mixins.models import OrgModelMixin -from common.mixins import CommonModelMixin -from .. import const - - -__all__ = ['DatabaseApp'] - - -class DatabaseApp(CommonModelMixin, OrgModelMixin): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - name = models.CharField(max_length=128, verbose_name=_('Name')) - type = models.CharField( - default=const.DATABASE_APP_TYPE_MYSQL, - choices=const.DATABASE_APP_TYPE_CHOICES, - max_length=128, verbose_name=_('Type') - ) - host = models.CharField( - max_length=128, verbose_name=_('Host'), db_index=True - ) - port = models.IntegerField(default=3306, verbose_name=_('Port')) - database = models.CharField( - max_length=128, blank=True, null=True, verbose_name=_('Database'), - db_index=True - ) - comment = models.TextField( - max_length=128, default='', blank=True, verbose_name=_('Comment') - ) - - def __str__(self): - return self.name - - class Meta: - unique_together = [('org_id', 'name'), ] - verbose_name = _("DatabaseApp") - ordering = ('name', ) diff --git a/apps/applications/models/k8s_app.py b/apps/applications/models/k8s_app.py deleted file mode 100644 index c4f0591ca..000000000 --- a/apps/applications/models/k8s_app.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -from common.db import models -from orgs.mixins.models import OrgModelMixin - - -class K8sApp(OrgModelMixin, models.JMSModel): - class TYPE(models.ChoiceSet): - K8S = 'k8s', _('Kubernetes') - - name = models.CharField(max_length=128, verbose_name=_('Name')) - type = models.CharField( - default=TYPE.K8S, choices=TYPE.choices, - max_length=128, verbose_name=_('Type') - ) - cluster = models.CharField(max_length=1024, verbose_name=_('Cluster')) - comment = models.TextField( - max_length=128, default='', blank=True, verbose_name=_('Comment') - ) - - def __str__(self): - return self.name - - class Meta: - unique_together = [('org_id', 'name'), ] - verbose_name = _('KubernetesApp') - ordering = ('name', ) diff --git a/apps/applications/models/remote_app.py b/apps/applications/models/remote_app.py deleted file mode 100644 index b9aee0ade..000000000 --- a/apps/applications/models/remote_app.py +++ /dev/null @@ -1,78 +0,0 @@ -# coding: utf-8 -# - -import uuid -from django.db import models -from django.utils.translation import ugettext_lazy as _ - -from orgs.mixins.models import OrgModelMixin -from common.fields.model import EncryptJsonDictTextField - -from .. import const - - -__all__ = [ - 'RemoteApp', -] - - -class RemoteApp(OrgModelMixin): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - name = models.CharField(max_length=128, verbose_name=_('Name')) - asset = models.ForeignKey( - 'assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset') - ) - type = models.CharField( - default=const.REMOTE_APP_TYPE_CHROME, - choices=const.REMOTE_APP_TYPE_CHOICES, - max_length=128, verbose_name=_('App type') - ) - path = models.CharField( - max_length=128, blank=False, null=False, - verbose_name=_('App path') - ) - params = EncryptJsonDictTextField( - max_length=4096, default={}, blank=True, null=True, - verbose_name=_('Parameters') - ) - created_by = models.CharField( - max_length=32, null=True, blank=True, verbose_name=_('Created by') - ) - date_created = models.DateTimeField( - auto_now_add=True, null=True, blank=True, verbose_name=_('Date created') - ) - comment = models.TextField( - max_length=128, default='', blank=True, verbose_name=_('Comment') - ) - - class Meta: - verbose_name = _("RemoteApp") - unique_together = [('org_id', 'name')] - ordering = ('name', ) - - def __str__(self): - return self.name - - @property - def parameters(self): - """ - 返回Guacamole需要的RemoteApp配置参数信息中的parameters参数 - """ - _parameters = list() - _parameters.append(self.type) - path = '\"%s\"' % self.path - _parameters.append(path) - for field in const.REMOTE_APP_TYPE_FIELDS_MAP[self.type]: - value = self.params.get(field['name']) - if value is None: - continue - _parameters.append(value) - _parameters = ' '.join(_parameters) - return _parameters - - @property - def asset_info(self): - return { - 'id': self.asset.id, - 'hostname': self.asset.hostname - } diff --git a/apps/applications/permissions.py b/apps/applications/permissions.py new file mode 100644 index 000000000..e56fd3652 --- /dev/null +++ b/apps/applications/permissions.py @@ -0,0 +1,9 @@ +from rest_framework import permissions + + +__all__ = ['IsRemoteApp'] + + +class IsRemoteApp(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + return obj.category_remote_app diff --git a/apps/applications/serializers/__init__.py b/apps/applications/serializers/__init__.py index 272b91982..3785f035d 100644 --- a/apps/applications/serializers/__init__.py +++ b/apps/applications/serializers/__init__.py @@ -1,5 +1,2 @@ from .application import * from .remote_app import * -from .database_app import * -from .k8s_app import * -from .common import * diff --git a/apps/applications/serializers/application.py b/apps/applications/serializers/application.py index 5ffcc65fb..8f6d103c0 100644 --- a/apps/applications/serializers/application.py +++ b/apps/applications/serializers/application.py @@ -4,15 +4,40 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from common.drf.serializers import MethodSerializer +from .attrs import category_serializer_classes_mapping, type_serializer_classes_mapping from .. import models __all__ = [ - 'ApplicationSerializer', + 'ApplicationSerializer', 'ApplicationSerializerMixin', ] -class ApplicationSerializer(BulkOrgResourceModelSerializer): +class ApplicationSerializerMixin(serializers.Serializer): + attrs = MethodSerializer() + + def get_attrs_serializer(self): + serializer_class = None + if isinstance(self.instance, models.Application): + instance_type = self.instance.type + serializer_class = type_serializer_classes_mapping.get(instance_type) + else: + 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() + return serializer + + +class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSerializer): category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category')) type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type')) @@ -26,17 +51,8 @@ class ApplicationSerializer(BulkOrgResourceModelSerializer): 'created_by', 'date_created', 'date_updated', 'get_type_display', ] - def create(self, validated_data): - validated_data['attrs'] = validated_data.pop('attrs', {}) - instance = super().create(validated_data) - return instance - - def update(self, instance, validated_data): - new_attrs = validated_data.pop('attrs', {}) - instance = super().update(instance, validated_data) - attrs = instance.attrs - attrs.update(new_attrs) - instance.attrs = attrs - instance.save() - return instance + def validate_attrs(self, attrs): + _attrs = self.instance.attrs if self.instance else {} + _attrs.update(attrs) + return _attrs diff --git a/apps/applications/serializers/attrs/__init__.py b/apps/applications/serializers/attrs/__init__.py new file mode 100644 index 000000000..bbbffd064 --- /dev/null +++ b/apps/applications/serializers/attrs/__init__.py @@ -0,0 +1 @@ +from .attrs import * diff --git a/apps/applications/serializers/attrs/application_category/__init__.py b/apps/applications/serializers/attrs/application_category/__init__.py new file mode 100644 index 000000000..1fd0beb3d --- /dev/null +++ b/apps/applications/serializers/attrs/application_category/__init__.py @@ -0,0 +1,3 @@ +from .remote_app import * +from .db import * +from .cloud import * diff --git a/apps/applications/serializers/attrs/application_category/cloud.py b/apps/applications/serializers/attrs/application_category/cloud.py new file mode 100644 index 000000000..f5fa71810 --- /dev/null +++ b/apps/applications/serializers/attrs/application_category/cloud.py @@ -0,0 +1,9 @@ +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + + +__all__ = ['CloudSerializer'] + + +class CloudSerializer(serializers.Serializer): + 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 new file mode 100644 index 000000000..f2967963e --- /dev/null +++ b/apps/applications/serializers/attrs/application_category/db.py @@ -0,0 +1,15 @@ +# coding: utf-8 +# +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + + +__all__ = ['DBSerializer'] + + +class DBSerializer(serializers.Serializer): + 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 new file mode 100644 index 000000000..9fa691976 --- /dev/null +++ b/apps/applications/serializers/attrs/application_category/remote_app.py @@ -0,0 +1,52 @@ +# coding: utf-8 +# + +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ +from django.core.exceptions import ObjectDoesNotExist + +from common.utils import get_logger, is_uuid +from assets.models import Asset + +logger = get_logger(__file__) + + +__all__ = ['RemoteAppSerializer'] + + +class CharPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField): + + def to_internal_value(self, data): + instance = super().to_internal_value(data) + return str(instance.id) + + def to_representation(self, value): + # value is instance.id + if self.pk_field is not None: + return self.pk_field.to_representation(value) + return value + + +class RemoteAppSerializer(serializers.Serializer): + asset_info = serializers.SerializerMethodField() + 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): + asset_id = obj.get('asset') + if not asset_id or is_uuid(asset_id): + return {} + try: + asset = Asset.objects.filter(id=str(asset_id)).values_list('id', 'hostname') + except ObjectDoesNotExist as e: + logger.error(e) + return {} + if not asset: + return {} + asset_info = {'id': str(asset[0]), 'hostname': asset[1]} + return asset_info diff --git a/apps/applications/serializers/attrs/application_type/__init__.py b/apps/applications/serializers/attrs/application_type/__init__.py new file mode 100644 index 000000000..aaa597136 --- /dev/null +++ b/apps/applications/serializers/attrs/application_type/__init__.py @@ -0,0 +1,12 @@ + +from .mysql import * +from .mariadb import * +from .oracle import * +from .pgsql import * + +from .chrome import * +from .mysql_workbench import * +from .vmware_client import * +from .custom import * + +from .k8s import * diff --git a/apps/applications/serializers/attrs/application_type/chrome.py b/apps/applications/serializers/attrs/application_type/chrome.py new file mode 100644 index 000000000..5b9e5147a --- /dev/null +++ b/apps/applications/serializers/attrs/application_type/chrome.py @@ -0,0 +1,26 @@ +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from ..application_category import RemoteAppSerializer + + +__all__ = ['ChromeSerializer'] + + +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, allow_null=True, + ) + chrome_target = serializers.CharField( + 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'), allow_null=True, + ) + chrome_password = serializers.CharField( + 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 new file mode 100644 index 000000000..0cbc09cf9 --- /dev/null +++ b/apps/applications/serializers/attrs/application_type/custom.py @@ -0,0 +1,27 @@ + +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from ..application_category import RemoteAppSerializer + + +__all__ = ['CustomSerializer'] + + +class CustomSerializer(RemoteAppSerializer): + custom_cmdline = serializers.CharField( + 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'), + allow_null=True, + ) + custom_username = serializers.CharField( + 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'), + allow_null=True, + ) diff --git a/apps/applications/serializers/attrs/application_type/k8s.py b/apps/applications/serializers/attrs/application_type/k8s.py new file mode 100644 index 000000000..ac1e023d3 --- /dev/null +++ b/apps/applications/serializers/attrs/application_type/k8s.py @@ -0,0 +1,8 @@ +from ..application_category import CloudSerializer + + +__all__ = ['K8SSerializer'] + + +class K8SSerializer(CloudSerializer): + pass diff --git a/apps/applications/serializers/attrs/application_type/mariadb.py b/apps/applications/serializers/attrs/application_type/mariadb.py new file mode 100644 index 000000000..d2bcb546f --- /dev/null +++ b/apps/applications/serializers/attrs/application_type/mariadb.py @@ -0,0 +1,8 @@ +from .mysql import MySQLSerializer + + +__all__ = ['MariaDBSerializer'] + + +class MariaDBSerializer(MySQLSerializer): + pass diff --git a/apps/applications/serializers/attrs/application_type/mysql.py b/apps/applications/serializers/attrs/application_type/mysql.py new file mode 100644 index 000000000..bb4e56c33 --- /dev/null +++ b/apps/applications/serializers/attrs/application_type/mysql.py @@ -0,0 +1,15 @@ +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + +from ..application_category import DBSerializer + + +__all__ = ['MySQLSerializer'] + + +class MySQLSerializer(DBSerializer): + 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 new file mode 100644 index 000000000..8025ec80f --- /dev/null +++ b/apps/applications/serializers/attrs/application_type/mysql_workbench.py @@ -0,0 +1,36 @@ +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from ..application_category import RemoteAppSerializer + + +__all__ = ['MySQLWorkbenchSerializer'] + + +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, + allow_null=True, + ) + mysql_workbench_ip = serializers.CharField( + max_length=128, allow_blank=True, required=False, label=_('IP'), + allow_null=True, + ) + mysql_workbench_port = serializers.IntegerField( + required=False, label=_('Port'), + allow_null=True, + ) + mysql_workbench_name = serializers.CharField( + 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'), + allow_null=True, + ) + mysql_workbench_password = serializers.CharField( + 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 new file mode 100644 index 000000000..63b905714 --- /dev/null +++ b/apps/applications/serializers/attrs/application_type/oracle.py @@ -0,0 +1,12 @@ +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + +from ..application_category import DBSerializer + + +__all__ = ['OracleSerializer'] + + +class OracleSerializer(DBSerializer): + 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 new file mode 100644 index 000000000..1b3cc2ef4 --- /dev/null +++ b/apps/applications/serializers/attrs/application_type/pgsql.py @@ -0,0 +1,12 @@ +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + +from ..application_category import DBSerializer + + +__all__ = ['PostgreSerializer'] + + +class PostgreSerializer(DBSerializer): + 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 new file mode 100644 index 000000000..1ed44e656 --- /dev/null +++ b/apps/applications/serializers/attrs/application_type/vmware_client.py @@ -0,0 +1,32 @@ +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from ..application_category import RemoteAppSerializer + + +__all__ = ['VMwareClientSerializer'] + + +class VMwareClientSerializer(RemoteAppSerializer): + PATH = r''' + C:\Program Files (x86)\VMware\Infrastructure\Virtual Infrastructure Client\Launcher\VpxClient + .exe + ''' + VMWARE_CLIENT_PATH = ''.join(PATH.split()) + + path = serializers.CharField( + 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'), + allow_null=True + ) + vmware_username = serializers.CharField( + 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'), + allow_null=True + ) diff --git a/apps/applications/serializers/attrs/attrs.py b/apps/applications/serializers/attrs/attrs.py new file mode 100644 index 000000000..18d420fd1 --- /dev/null +++ b/apps/applications/serializers/attrs/attrs.py @@ -0,0 +1,42 @@ +from rest_framework import serializers +from applications import const +from . import application_category, application_type + + +__all__ = [ + 'category_serializer_classes_mapping', + 'type_serializer_classes_mapping', + 'get_serializer_class_by_application_type', +] + + +# define `attrs` field `category serializers mapping` +# --------------------------------------------------- + +category_serializer_classes_mapping = { + const.ApplicationCategoryChoices.db.value: application_category.DBSerializer, + const.ApplicationCategoryChoices.remote_app.value: application_category.RemoteAppSerializer, + const.ApplicationCategoryChoices.cloud.value: application_category.CloudSerializer, +} + +# define `attrs` field `type serializers mapping` +# ----------------------------------------------- + +type_serializer_classes_mapping = { + # db + const.ApplicationTypeChoices.mysql.value: application_type.MySQLSerializer, + const.ApplicationTypeChoices.mariadb.value: application_type.MariaDBSerializer, + const.ApplicationTypeChoices.oracle.value: application_type.OracleSerializer, + const.ApplicationTypeChoices.pgsql.value: application_type.PostgreSerializer, + # remote-app + const.ApplicationTypeChoices.chrome.value: application_type.ChromeSerializer, + const.ApplicationTypeChoices.mysql_workbench.value: application_type.MySQLWorkbenchSerializer, + const.ApplicationTypeChoices.vmware_client.value: application_type.VMwareClientSerializer, + const.ApplicationTypeChoices.custom.value: application_type.CustomSerializer, + # cloud + const.ApplicationTypeChoices.k8s.value: application_type.K8SSerializer +} + + +def get_serializer_class_by_application_type(_application_type): + return type_serializer_classes_mapping.get(_application_type) diff --git a/apps/applications/serializers/common.py b/apps/applications/serializers/common.py deleted file mode 100644 index 31c0ba2ac..000000000 --- a/apps/applications/serializers/common.py +++ /dev/null @@ -1,11 +0,0 @@ -from rest_framework import serializers - - -class NoPasswordSerializer(serializers.JSONField): - def to_representation(self, value): - new_value = {} - for k, v in value.items(): - if 'password' not in k: - new_value[k] = v - return new_value - diff --git a/apps/applications/serializers/database_app.py b/apps/applications/serializers/database_app.py deleted file mode 100644 index 8b983ed87..000000000 --- a/apps/applications/serializers/database_app.py +++ /dev/null @@ -1,50 +0,0 @@ -# coding: utf-8 -# -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from common.serializers import AdaptedBulkListSerializer - -from .. import models - - -class DBAttrsSerializer(serializers.Serializer): - host = serializers.CharField(max_length=128, label=_('Host')) - port = serializers.IntegerField(label=_('Port')) - # 添加allow_null=True,兼容之前数据库中database字段为None的情况 - database = serializers.CharField(max_length=128, required=True, allow_null=True, label=_('Database')) - - -class MySQLAttrsSerializer(DBAttrsSerializer): - port = serializers.IntegerField(default=3306, label=_('Port')) - - -class PostgreAttrsSerializer(DBAttrsSerializer): - port = serializers.IntegerField(default=5432, label=_('Port')) - - -class OracleAttrsSerializer(DBAttrsSerializer): - port = serializers.IntegerField(default=1521, label=_('Port')) - - -class MariaDBAttrsSerializer(MySQLAttrsSerializer): - pass - - -class DatabaseAppSerializer(BulkOrgResourceModelSerializer): - - class Meta: - model = models.DatabaseApp - list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'name', 'type', 'get_type_display', 'host', 'port', - 'database', 'comment', 'created_by', 'date_created', 'date_updated', - ] - read_only_fields = [ - 'created_by', 'date_created', 'date_updated' - 'get_type_display', - ] - extra_kwargs = { - 'get_type_display': {'label': _('Type for display')}, - } diff --git a/apps/applications/serializers/k8s_app.py b/apps/applications/serializers/k8s_app.py deleted file mode 100644 index 6bf685468..000000000 --- a/apps/applications/serializers/k8s_app.py +++ /dev/null @@ -1,27 +0,0 @@ -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from .. import models - - -class CloudAttrsSerializer(serializers.Serializer): - cluster = serializers.CharField(max_length=1024, label=_('Cluster')) - - -class K8sAttrsSerializer(CloudAttrsSerializer): - pass - - -class K8sAppSerializer(BulkOrgResourceModelSerializer): - type_display = serializers.CharField(source='get_type_display', read_only=True, label=_('Type for display')) - - class Meta: - model = models.K8sApp - fields = [ - 'id', 'name', 'type', 'type_display', 'comment', 'created_by', - 'date_created', 'date_updated', 'cluster' - ] - read_only_fields = [ - 'id', 'created_by', 'date_created', 'date_updated', - ] diff --git a/apps/applications/serializers/remote_app.py b/apps/applications/serializers/remote_app.py index 7e5a69c68..13343ee53 100644 --- a/apps/applications/serializers/remote_app.py +++ b/apps/applications/serializers/remote_app.py @@ -1,89 +1,14 @@ # coding: utf-8 # - -import copy -from django.utils.translation import ugettext_lazy as _ -from django.core.exceptions import ObjectDoesNotExist from rest_framework import serializers - -from common.serializers import AdaptedBulkListSerializer -from common.fields.serializer import CustomMetaDictField from common.utils import get_logger -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from assets.models import Asset +from ..models import Application -from .. import const -from ..models import RemoteApp, Category, Application logger = get_logger(__file__) -class CharPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField): - - def to_internal_value(self, data): - instance = super().to_internal_value(data) - return str(instance.id) - - def to_representation(self, value): - # value is instance.id - if self.pk_field is not None: - return self.pk_field.to_representation(value) - return value - - -class RemoteAppAttrsSerializer(serializers.Serializer): - asset_info = serializers.SerializerMethodField() - asset = CharPrimaryKeyRelatedField(queryset=Asset.objects, required=False, label=_("Asset")) - path = serializers.CharField(max_length=128, label=_('Application path')) - - @staticmethod - def get_asset_info(obj): - asset_info = {} - asset_id = obj.get('asset') - if not asset_id: - return asset_info - try: - asset = Asset.objects.get(id=asset_id) - asset_info.update({ - 'id': str(asset.id), - 'hostname': asset.hostname - }) - except ObjectDoesNotExist as e: - logger.error(e) - return asset_info - - -class ChromeAttrsSerializer(RemoteAppAttrsSerializer): - REMOTE_APP_PATH = 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe' - path = serializers.CharField(max_length=128, label=_('Application path'), default=REMOTE_APP_PATH) - chrome_target = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Target URL')) - chrome_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username')) - chrome_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password')) - - -class MySQLWorkbenchAttrsSerializer(RemoteAppAttrsSerializer): - REMOTE_APP_PATH = 'C:\Program Files\MySQL\MySQL Workbench 8.0 CE\MySQLWorkbench.exe' - path = serializers.CharField(max_length=128, label=_('Application path'), default=REMOTE_APP_PATH) - mysql_workbench_ip = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('IP')) - mysql_workbench_port = serializers.IntegerField(required=False, label=_('Port')) - mysql_workbench_name = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Database')) - mysql_workbench_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username')) - mysql_workbench_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password')) - - -class VMwareClientAttrsSerializer(RemoteAppAttrsSerializer): - REMOTE_APP_PATH = 'C:\Program Files (x86)\VMware\Infrastructure\Virtual Infrastructure Client\Launcher\VpxClient.exe' - path = serializers.CharField(max_length=128, label=_('Application path'), default=REMOTE_APP_PATH) - vmware_target = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Target URL')) - vmware_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username')) - vmware_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password')) - - -class CustomRemoteAppAttrsSeralizers(RemoteAppAttrsSerializer): - custom_cmdline = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Operating parameter')) - custom_target = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Target url')) - custom_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username')) - custom_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password')) +__all__ = ['RemoteAppConnectionInfoSerializer'] class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer): @@ -97,94 +22,36 @@ class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer): ] read_only_fields = ['parameter_remote_app'] + @staticmethod + def get_asset(obj): + return obj.attrs.get('asset') + @staticmethod def get_parameters(obj): """ 返回Guacamole需要的RemoteApp配置参数信息中的parameters参数 """ - serializer_cls = Category.get_type_serializer_cls(obj.type) - fields = serializer_cls().get_fields() - fields.pop('asset', None) - fields_name = list(fields.keys()) - attrs = obj.attrs - _parameters = list() - _parameters.append(obj.type) - for field_name in list(fields_name): - value = attrs.get(field_name, None) + from .attrs import get_serializer_class_by_application_type + serializer_class = get_serializer_class_by_application_type(obj.type) + fields = serializer_class().get_fields() + + parameters = [obj.type] + for field_name in list(fields.keys()): + if field_name in ['asset']: + continue + value = obj.attrs.get(field_name) if not value: continue if field_name == 'path': value = '\"%s\"' % value - _parameters.append(str(value)) - _parameters = ' '.join(_parameters) - return _parameters + parameters.append(str(value)) + + parameters = ' '.join(parameters) + return parameters def get_parameter_remote_app(self, obj): - parameters = self.get_parameters(obj) - parameter = { - 'program': const.REMOTE_APP_BOOT_PROGRAM_NAME, + return { + 'program': '||jmservisor', 'working_directory': '', - 'parameters': parameters, + 'parameters': self.get_parameters(obj) } - return parameter - - @staticmethod - def get_asset(obj): - return obj.attrs.get('asset') - - -# TODO: DELETE -class RemoteAppParamsDictField(CustomMetaDictField): - type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP - default_type = const.REMOTE_APP_TYPE_CHROME - convert_key_remove_type_prefix = False - convert_key_to_upper = False - - -# TODO: DELETE -class RemoteAppSerializer(BulkOrgResourceModelSerializer): - params = RemoteAppParamsDictField(label=_('Parameters')) - type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP - - class Meta: - model = RemoteApp - list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'name', 'asset', 'asset_info', 'type', 'get_type_display', - 'path', 'params', 'date_created', 'created_by', 'comment', - ] - read_only_fields = [ - 'created_by', 'date_created', 'asset_info', - 'get_type_display' - ] - extra_kwargs = { - 'asset_info': {'label': _('Asset info')}, - 'get_type_display': {'label': _('Type for display')}, - } - - def process_params(self, instance, validated_data): - new_params = copy.deepcopy(validated_data.get('params', {})) - tp = validated_data.get('type', '') - - if tp != instance.type: - return new_params - - old_params = instance.params - 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_params.get(field_name, '') - old_value = old_params.get(field_name, '') - field_value = new_value if new_value else old_value - new_params[field_name] = field_value - - return new_params - - def update(self, instance, validated_data): - params = self.process_params(instance, validated_data) - validated_data['params'] = params - return super().update(instance, validated_data) - - diff --git a/apps/applications/urls/api_urls.py b/apps/applications/urls/api_urls.py index bfe685d4c..ab463a401 100644 --- a/apps/applications/urls/api_urls.py +++ b/apps/applications/urls/api_urls.py @@ -1,26 +1,20 @@ # coding:utf-8 # - -from django.urls import path, re_path +from django.urls import path from rest_framework_bulk.routes import BulkRouter - -from common import api as capi from .. import api + app_name = 'applications' + router = BulkRouter() router.register(r'applications', api.ApplicationViewSet, 'application') -router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app') -router.register(r'database-apps', api.DatabaseAppViewSet, 'database-app') -router.register(r'k8s-apps', api.K8sAppViewSet, 'k8s-app') + urlpatterns = [ path('remote-apps//connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'), ] -old_version_urlpatterns = [ - re_path('(?Premote-app)/.*', capi.redirect_plural_name_api) -] -urlpatterns += router.urls + old_version_urlpatterns +urlpatterns += router.urls diff --git a/apps/applications/urls/views_urls.py b/apps/applications/urls/views_urls.py deleted file mode 100644 index 2b09baf9f..000000000 --- a/apps/applications/urls/views_urls.py +++ /dev/null @@ -1,7 +0,0 @@ -# coding:utf-8 -from django.urls import path - -app_name = 'applications' - -urlpatterns = [ -] diff --git a/apps/assets/api/admin_user.py b/apps/assets/api/admin_user.py index 9893d014f..bdbae55dd 100644 --- a/apps/assets/api/admin_user.py +++ b/apps/assets/api/admin_user.py @@ -29,8 +29,8 @@ class AdminUserViewSet(OrgBulkModelViewSet): Admin user api set, for add,delete,update,list,retrieve resource """ model = AdminUser - filter_fields = ("name", "username") - search_fields = filter_fields + filterset_fields = ("name", "username") + search_fields = filterset_fields serializer_class = serializers.AdminUserSerializer permission_classes = (IsOrgAdmin,) @@ -93,8 +93,8 @@ class AdminUserTestConnectiveApi(generics.RetrieveAPIView): class AdminUserAssetsListView(generics.ListAPIView): permission_classes = (IsOrgAdmin,) serializer_class = serializers.AssetSimpleSerializer - filter_fields = ("hostname", "ip") - search_fields = filter_fields + filterset_fields = ("hostname", "ip") + search_fields = filterset_fields def get_object(self): pk = self.kwargs.get('pk') diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index 7ce3b1f8e..4de9fb899 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -3,6 +3,8 @@ from assets.api import FilterAssetByNodeMixin from rest_framework.viewsets import ModelViewSet from rest_framework.generics import RetrieveAPIView +from rest_framework.response import Response +from rest_framework import status from django.shortcuts import get_object_or_404 from common.utils import get_logger, get_object_or_none @@ -12,7 +14,7 @@ from orgs.mixins import generics from ..models import Asset, Node, Platform from .. import serializers from ..tasks import ( - update_asset_hardware_info_manual, test_asset_connectivity_manual + update_assets_hardware_info_manual, test_assets_connectivity_manual ) from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend @@ -21,7 +23,7 @@ logger = get_logger(__file__) __all__ = [ 'AssetViewSet', 'AssetPlatformRetrieveApi', 'AssetGatewayListApi', 'AssetPlatformViewSet', - 'AssetTaskCreateApi', + 'AssetTaskCreateApi', 'AssetsTaskCreateApi', ] @@ -30,10 +32,15 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet): API endpoint that allows Asset to be viewed or edited. """ model = Asset - filter_fields = ( - "hostname", "ip", "systemuser__id", "admin_user__id", "platform__base", - "is_active" - ) + filterset_fields = { + 'hostname': ['exact'], + 'ip': ['exact'], + 'systemuser__id': ['exact'], + 'admin_user__id': ['exact'], + 'platform__base': ['exact'], + 'is_active': ['exact'], + 'protocols': ['exact', 'icontains'] + } search_fields = ("hostname", "ip") ordering_fields = ("hostname", "ip", "port", "cpu_cores") serializer_classes = { @@ -74,7 +81,7 @@ class AssetPlatformViewSet(ModelViewSet): queryset = Platform.objects.all() permission_classes = (IsSuperUser,) serializer_class = serializers.PlatformSerializer - filter_fields = ['name', 'base'] + filterset_fields = ['name', 'base'] search_fields = ['name'] def get_permissions(self): @@ -90,26 +97,38 @@ class AssetPlatformViewSet(ModelViewSet): return super().check_object_permissions(request, obj) -class AssetTaskCreateApi(generics.CreateAPIView): +class AssetsTaskMixin: + def perform_assets_task(self, serializer): + data = serializer.validated_data + assets = data['assets'] + action = data['action'] + if action == "refresh": + task = update_assets_hardware_info_manual.delay(assets) + else: + task = test_assets_connectivity_manual.delay(assets) + data = getattr(serializer, '_data', {}) + data["task"] = task.id + setattr(serializer, '_data', data) + + def perform_create(self, serializer): + self.perform_assets_task(serializer) + + +class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView): model = Asset serializer_class = serializers.AssetTaskSerializer permission_classes = (IsOrgAdmin,) - def get_object(self): - pk = self.kwargs.get("pk") - instance = get_object_or_404(Asset, pk=pk) - return instance + def create(self, request, *args, **kwargs): + pk = self.kwargs.get('pk') + request.data['assets'] = [pk] + return super().create(request, *args, **kwargs) - def perform_create(self, serializer): - asset = self.get_object() - action = serializer.validated_data["action"] - if action == "refresh": - task = update_asset_hardware_info_manual.delay(asset) - else: - task = test_asset_connectivity_manual.delay(asset) - data = getattr(serializer, '_data', {}) - data["task"] = task.id - setattr(serializer, '_data', data) + +class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView): + model = Asset + serializer_class = serializers.AssetTaskSerializer + permission_classes = (IsOrgAdmin,) class AssetGatewayListApi(generics.ListAPIView): diff --git a/apps/assets/api/asset_user.py b/apps/assets/api/asset_user.py index 932d5b77e..7c78f3f51 100644 --- a/apps/assets/api/asset_user.py +++ b/apps/assets/api/asset_user.py @@ -78,7 +78,7 @@ class AssetUserViewSet(CommonApiMixin, BulkModelViewSet): 'retrieve': serializers.AssetUserReadSerializer, } permission_classes = [IsOrgAdminOrAppUser] - filter_fields = [ + filterset_fields = [ "id", "ip", "hostname", "username", "asset_id", "node_id", "prefer", "prefer_id", @@ -131,7 +131,7 @@ class AssetUserTaskCreateAPI(generics.CreateAPIView): permission_classes = (IsOrgAdminOrAppUser,) serializer_class = serializers.AssetUserTaskSerializer filter_backends = AssetUserViewSet.filter_backends - filter_fields = AssetUserViewSet.filter_fields + filterset_fields = AssetUserViewSet.filterset_fields def get_asset_users(self): manager = AssetUserManager() diff --git a/apps/assets/api/cmd_filter.py b/apps/assets/api/cmd_filter.py index b4ae67c72..95aac8af9 100644 --- a/apps/assets/api/cmd_filter.py +++ b/apps/assets/api/cmd_filter.py @@ -14,16 +14,16 @@ __all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet'] class CommandFilterViewSet(OrgBulkModelViewSet): model = CommandFilter - filter_fields = ("name",) - search_fields = filter_fields + filterset_fields = ("name",) + search_fields = filterset_fields permission_classes = (IsOrgAdmin,) serializer_class = serializers.CommandFilterSerializer class CommandFilterRuleViewSet(OrgBulkModelViewSet): model = CommandFilterRule - filter_fields = ("content",) - search_fields = filter_fields + filterset_fields = ("content",) + search_fields = filterset_fields permission_classes = (IsOrgAdmin,) serializer_class = serializers.CommandFilterRuleSerializer diff --git a/apps/assets/api/domain.py b/apps/assets/api/domain.py index 7c32883d8..d444aa2ff 100644 --- a/apps/assets/api/domain.py +++ b/apps/assets/api/domain.py @@ -18,8 +18,8 @@ __all__ = ['DomainViewSet', 'GatewayViewSet', "GatewayTestConnectionApi"] class DomainViewSet(OrgBulkModelViewSet): model = Domain - filter_fields = ("name", ) - search_fields = filter_fields + filterset_fields = ("name", ) + search_fields = filterset_fields permission_classes = (IsOrgAdminOrAppUser,) serializer_class = serializers.DomainSerializer @@ -31,7 +31,7 @@ class DomainViewSet(OrgBulkModelViewSet): class GatewayViewSet(OrgBulkModelViewSet): model = Gateway - filter_fields = ("domain__name", "name", "username", "ip", "domain") + filterset_fields = ("domain__name", "name", "username", "ip", "domain") search_fields = ("domain__name", "name", "username", "ip") permission_classes = (IsOrgAdmin,) serializer_class = serializers.GatewaySerializer diff --git a/apps/assets/api/favorite_asset.py b/apps/assets/api/favorite_asset.py index 174c77330..cf85d96f2 100644 --- a/apps/assets/api/favorite_asset.py +++ b/apps/assets/api/favorite_asset.py @@ -13,7 +13,7 @@ __all__ = ['FavoriteAssetViewSet'] class FavoriteAssetViewSet(BulkModelViewSet): serializer_class = FavoriteAssetSerializer permission_classes = (IsValidUser,) - filter_fields = ['asset'] + filterset_fields = ['asset'] def dispatch(self, request, *args, **kwargs): with tmp_to_root_org(): diff --git a/apps/assets/api/gathered_user.py b/apps/assets/api/gathered_user.py index 896dab7e1..959259799 100644 --- a/apps/assets/api/gathered_user.py +++ b/apps/assets/api/gathered_user.py @@ -18,5 +18,5 @@ class GatheredUserViewSet(OrgModelViewSet): permission_classes = [IsOrgAdmin] extra_filter_backends = [AssetRelatedByNodeFilterBackend] - filter_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id'] + filterset_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id'] search_fields = ['username', 'asset__ip', 'asset__hostname'] diff --git a/apps/assets/api/label.py b/apps/assets/api/label.py index fe298169a..06dff8b8a 100644 --- a/apps/assets/api/label.py +++ b/apps/assets/api/label.py @@ -28,8 +28,8 @@ __all__ = ['LabelViewSet'] class LabelViewSet(OrgBulkModelViewSet): model = Label - filter_fields = ("name", "value") - search_fields = filter_fields + filterset_fields = ("name", "value") + search_fields = filterset_fields permission_classes = (IsOrgAdmin,) serializer_class = serializers.LabelSerializer diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 924529ce1..a64326042 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -45,7 +45,7 @@ __all__ = [ class NodeViewSet(OrgModelViewSet): model = Node - filter_fields = ('value', 'key', 'id') + filterset_fields = ('value', 'key', 'id') search_fields = ('value', ) permission_classes = (IsOrgAdmin,) serializer_class = serializers.NodeSerializer diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index 70bbe376e..af7590579 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -29,8 +29,12 @@ class SystemUserViewSet(OrgBulkModelViewSet): System user api set, for add,delete,update,list,retrieve resource """ model = SystemUser - filter_fields = ("name", "username", "protocol") - search_fields = filter_fields + filterset_fields = { + 'name': ['exact'], + 'username': ['exact'], + 'protocol': ['exact', 'in'] + } + search_fields = filterset_fields serializer_class = serializers.SystemUserSerializer serializer_classes = { 'default': serializers.SystemUserSerializer, @@ -136,8 +140,8 @@ class SystemUserCommandFilterRuleListApi(generics.ListAPIView): class SystemUserAssetsListView(generics.ListAPIView): permission_classes = (IsOrgAdmin,) serializer_class = serializers.AssetSimpleSerializer - filter_fields = ("hostname", "ip") - search_fields = filter_fields + filterset_fields = ("hostname", "ip") + search_fields = filterset_fields def get_object(self): pk = self.kwargs.get('pk') diff --git a/apps/assets/api/system_user_relation.py b/apps/assets/api/system_user_relation.py index 411a19be0..90a12eaec 100644 --- a/apps/assets/api/system_user_relation.py +++ b/apps/assets/api/system_user_relation.py @@ -65,7 +65,7 @@ class SystemUserAssetRelationViewSet(BaseRelationViewSet): serializer_class = serializers.SystemUserAssetRelationSerializer model = models.SystemUser.assets.through permission_classes = (IsOrgAdmin,) - filter_fields = [ + filterset_fields = [ 'id', 'asset', 'systemuser', ] search_fields = [ @@ -91,7 +91,7 @@ class SystemUserNodeRelationViewSet(BaseRelationViewSet): serializer_class = serializers.SystemUserNodeRelationSerializer model = models.SystemUser.nodes.through permission_classes = (IsOrgAdmin,) - filter_fields = [ + filterset_fields = [ 'id', 'node', 'systemuser', ] search_fields = [ @@ -112,7 +112,7 @@ class SystemUserUserRelationViewSet(BaseRelationViewSet): serializer_class = serializers.SystemUserUserRelationSerializer model = models.SystemUser.users.through permission_classes = (IsOrgAdmin,) - filter_fields = [ + filterset_fields = [ 'id', 'user', 'systemuser', ] search_fields = [ diff --git a/apps/assets/models/domain.py b/apps/assets/models/domain.py index c0c3f4805..1a2dbbc4e 100644 --- a/apps/assets/models/domain.py +++ b/apps/assets/models/domain.py @@ -26,6 +26,7 @@ class Domain(OrgModelMixin): class Meta: verbose_name = _("Domain") unique_together = [('org_id', 'name')] + ordering = ('name',) def __str__(self): return self.name diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index 710586e6f..58e267f70 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -38,6 +38,7 @@ class FamilyMixin: __children = None __all_children = None is_node = True + child_mark: int @staticmethod def clean_children_keys(nodes_keys): @@ -121,11 +122,22 @@ class FamilyMixin: created = True return child, created + def get_valid_child_mark(self): + key = "{}:{}".format(self.key, self.child_mark) + if not self.__class__.objects.filter(key=key).exists(): + return self.child_mark + children_keys = self.get_children().values_list('key', flat=True) + children_keys_last = [key.split(':')[-1] for key in children_keys] + children_keys_last = [int(k) for k in children_keys_last if k.strip().isdigit()] + max_key_last = max(children_keys_last) if children_keys_last else 1 + return max_key_last + 1 + def get_next_child_key(self): - mark = self.child_mark - self.child_mark += 1 + child_mark = self.get_valid_child_mark() + key = "{}:{}".format(self.key, child_mark) + self.child_mark = child_mark + 1 self.save() - return "{}:{}".format(self.key, mark) + return key def get_next_child_preset_name(self): name = ugettext("New node") diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 5a1f47284..5b800311b 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -87,6 +87,23 @@ class SystemUser(BaseUser): (PROTOCOL_POSTGRESQL, 'postgresql'), (PROTOCOL_K8S, 'k8s'), ) + ASSET_CATEGORY_PROTOCOLS = [ + PROTOCOL_SSH, PROTOCOL_RDP, PROTOCOL_TELNET, PROTOCOL_VNC + ] + APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS = [ + PROTOCOL_RDP + ] + APPLICATION_CATEGORY_DB_PROTOCOLS = [ + PROTOCOL_MYSQL, PROTOCOL_ORACLE, PROTOCOL_MARIADB, PROTOCOL_POSTGRESQL + ] + APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [ + PROTOCOL_K8S + ] + APPLICATION_CATEGORY_PROTOCOLS = [ + *APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS, + *APPLICATION_CATEGORY_DB_PROTOCOLS, + *APPLICATION_CATEGORY_CLOUD_PROTOCOLS + ] LOGIN_AUTO = 'auto' LOGIN_MANUAL = 'manual' @@ -133,24 +150,6 @@ class SystemUser(BaseUser): def login_mode_display(self): return self.get_login_mode_display() - @property - def db_application_protocols(self): - return [ - self.PROTOCOL_MYSQL, self.PROTOCOL_ORACLE, self.PROTOCOL_MARIADB, - self.PROTOCOL_POSTGRESQL - ] - - @property - def cloud_application_protocols(self): - return [self.PROTOCOL_K8S] - - @property - def application_category_protocols(self): - protocols = [] - protocols.extend(self.db_application_protocols) - protocols.extend(self.cloud_application_protocols) - return protocols - def is_need_push(self): if self.auto_push and self.protocol in [self.PROTOCOL_SSH, self.PROTOCOL_RDP]: return True @@ -163,7 +162,7 @@ class SystemUser(BaseUser): @property def is_need_test_asset_connective(self): - return self.protocol not in self.application_category_protocols + return self.protocol in self.ASSET_CATEGORY_PROTOCOLS def has_special_auth(self, asset=None, username=None): if username is None and self.username_same_with_user: @@ -172,7 +171,7 @@ class SystemUser(BaseUser): @property def can_perm_to_asset(self): - return self.protocol not in self.application_category_protocols + return self.protocol in self.ASSET_CATEGORY_PROTOCOLS def _merge_auth(self, other): super()._merge_auth(other) @@ -205,6 +204,17 @@ class SystemUser(BaseUser): assets = Asset.objects.filter(id__in=assets_ids) return assets + @classmethod + def get_protocol_by_application_type(cls, app_type): + from applications.const import ApplicationTypeChoices + if app_type in cls.APPLICATION_CATEGORY_PROTOCOLS: + protocol = app_type + elif app_type in ApplicationTypeChoices.remote_app_types(): + protocol = cls.PROTOCOL_RDP + else: + protocol = None + return protocol + class Meta: ordering = ['name'] unique_together = [('name', 'org_id')] diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py index 5e27b0c64..b8b086205 100644 --- a/apps/assets/serializers/admin_user.py +++ b/apps/assets/serializers/admin_user.py @@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from ..models import Node, AdminUser from orgs.mixins.serializers import BulkOrgResourceModelSerializer diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index ce8b54ca9..66effa20b 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -204,3 +204,6 @@ class AssetTaskSerializer(serializers.Serializer): ) task = serializers.CharField(read_only=True) action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True) + assets = serializers.PrimaryKeyRelatedField( + queryset=Asset.objects, required=False, allow_empty=True, many=True + ) diff --git a/apps/assets/serializers/asset_user.py b/apps/assets/serializers/asset_user.py index 1c598cbf5..7eba6ba41 100644 --- a/apps/assets/serializers/asset_user.py +++ b/apps/assets/serializers/asset_user.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import AuthBook, Asset from ..backends import AssetUserManager diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py index 5a045df9c..0c8eca3d4 100644 --- a/apps/assets/serializers/cmd_filter.py +++ b/apps/assets/serializers/cmd_filter.py @@ -3,8 +3,7 @@ import re from rest_framework import serializers -from common.fields import ChoiceDisplayField -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from ..models import CommandFilter, CommandFilterRule, SystemUser from orgs.mixins.serializers import BulkOrgResourceModelSerializer @@ -26,7 +25,6 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer): class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer): - # serializer_choice_field = ChoiceDisplayField invalid_pattern = re.compile(r'[\.\*\+\[\\\?\{\}\^\$\|\(\)\#\<\>]') type_display = serializers.ReadOnlyField(source='get_type_display') action_display = serializers.ReadOnlyField(source='get_action_display') diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index 0a16f3910..00df0b91d 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -3,7 +3,7 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer from common.validators import NoSpecialChars from ..models import Domain, Gateway diff --git a/apps/assets/serializers/favorite_asset.py b/apps/assets/serializers/favorite_asset.py index 8429d959e..067655b0a 100644 --- a/apps/assets/serializers/favorite_asset.py +++ b/apps/assets/serializers/favorite_asset.py @@ -4,7 +4,7 @@ from rest_framework import serializers from orgs.utils import tmp_to_root_org -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from common.mixins import BulkSerializerMixin from ..models import FavoriteAsset diff --git a/apps/assets/serializers/label.py b/apps/assets/serializers/label.py index 448018eab..bfce6e2a6 100644 --- a/apps/assets/serializers/label.py +++ b/apps/assets/serializers/label.py @@ -2,7 +2,7 @@ # from rest_framework import serializers -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import Label diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index fb8d4df97..5e8d884dd 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -2,7 +2,7 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from django.db.models import Count -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from common.mixins.serializers import BulkSerializerMixin from common.utils import ssh_pubkey_gen from orgs.mixins.serializers import BulkOrgResourceModelSerializer diff --git a/apps/assets/tasks/asset_connectivity.py b/apps/assets/tasks/asset_connectivity.py index 8c02d0db1..ea4b90ea6 100644 --- a/apps/assets/tasks/asset_connectivity.py +++ b/apps/assets/tasks/asset_connectivity.py @@ -14,7 +14,7 @@ from .utils import clean_ansible_task_hosts, group_asset_by_platform logger = get_logger(__file__) __all__ = [ 'test_asset_connectivity_util', 'test_asset_connectivity_manual', - 'test_node_assets_connectivity_manual', + 'test_node_assets_connectivity_manual', 'test_assets_connectivity_manual', ] @@ -82,6 +82,17 @@ def test_asset_connectivity_manual(asset): return True, "" +@shared_task(queue="ansible") +def test_assets_connectivity_manual(assets): + task_name = _("Test assets connectivity: {}").format([asset.hostname for asset in assets]) + summary = test_asset_connectivity_util(assets, task_name=task_name) + + if summary.get('dark'): + return False, summary['dark'] + else: + return True, "" + + @shared_task(queue="ansible") def test_node_assets_connectivity_manual(node): task_name = _("Test if the assets under the node are connectable: {}".format(node.name)) diff --git a/apps/assets/tasks/common.py b/apps/assets/tasks/common.py index 2c352b64e..b743300e1 100644 --- a/apps/assets/tasks/common.py +++ b/apps/assets/tasks/common.py @@ -3,10 +3,13 @@ from celery import shared_task +from orgs.utils import tmp_to_root_org + __all__ = ['add_nodes_assets_to_system_users'] @shared_task +@tmp_to_root_org() def add_nodes_assets_to_system_users(nodes_keys, system_users): from ..models import Node assets = Node.get_nodes_all_assets(nodes_keys).values_list('id', flat=True) diff --git a/apps/assets/tasks/gather_asset_hardware_info.py b/apps/assets/tasks/gather_asset_hardware_info.py index aa79e1655..daad3d694 100644 --- a/apps/assets/tasks/gather_asset_hardware_info.py +++ b/apps/assets/tasks/gather_asset_hardware_info.py @@ -19,6 +19,7 @@ disk_pattern = re.compile(r'^hd|sd|xvd|vd|nv') __all__ = [ 'update_assets_hardware_info_util', 'update_asset_hardware_info_manual', 'update_assets_hardware_info_period', 'update_node_assets_hardware_info_manual', + 'update_assets_hardware_info_manual', ] @@ -114,6 +115,12 @@ def update_asset_hardware_info_manual(asset): update_assets_hardware_info_util([asset], task_name=task_name) +@shared_task(queue="ansible") +def update_assets_hardware_info_manual(assets): + task_name = _("Update assets hardware info: {}").format([asset.hostname for asset in assets]) + update_assets_hardware_info_util(assets, task_name=task_name) + + @shared_task(queue="ansible") def update_assets_hardware_info_period(): """ diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index d81520b65..707a8e73d 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -36,6 +36,7 @@ urlpatterns = [ path('assets//gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'), path('assets//platform/', api.AssetPlatformRetrieveApi.as_view(), name='asset-platform-detail'), path('assets//tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'), + path('assets/tasks/', api.AssetsTaskCreateApi.as_view(), name='assets-task-create'), path('asset-users/tasks/', api.AssetUserTaskCreateAPI.as_view(), name='asset-user-task-create'), diff --git a/apps/audits/api.py b/apps/audits/api.py index 71b15ceee..f36724011 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -25,8 +25,8 @@ class FTPLogViewSet(CreateModelMixin, date_range_filter_fields = [ ('date_start', ('date_from', 'date_to')) ] - filter_fields = ['user', 'asset', 'system_user', 'filename'] - search_fields = filter_fields + filterset_fields = ['user', 'asset', 'system_user', 'filename'] + search_fields = filterset_fields ordering = ['-date_start'] @@ -38,7 +38,7 @@ class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet): date_range_filter_fields = [ ('datetime', ('date_from', 'date_to')) ] - filter_fields = ['username', 'ip', 'city', 'type', 'status', 'mfa'] + filterset_fields = ['username', 'ip', 'city', 'type', 'status', 'mfa'] search_fields =['username', 'ip', 'city'] @staticmethod @@ -62,7 +62,7 @@ class OperateLogViewSet(ListModelMixin, OrgGenericViewSet): date_range_filter_fields = [ ('datetime', ('date_from', 'date_to')) ] - filter_fields = ['user', 'action', 'resource_type', 'resource', 'remote_addr'] + filterset_fields = ['user', 'action', 'resource_type', 'resource', 'remote_addr'] search_fields = ['resource'] ordering = ['-datetime'] @@ -75,7 +75,7 @@ class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet): date_range_filter_fields = [ ('datetime', ('date_from', 'date_to')) ] - filter_fields = ['user', 'change_by', 'remote_addr'] + filterset_fields = ['user', 'change_by', 'remote_addr'] ordering = ['-datetime'] def get_queryset(self): @@ -94,7 +94,7 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet): date_range_filter_fields = [ ('date_start', ('date_from', 'date_to')) ] - filter_fields = ['user__name', 'command', 'run_as__name', 'is_finished'] + filterset_fields = ['user__name', 'command', 'run_as__name', 'is_finished'] search_fields = ['command', 'user__name', 'run_as__name'] ordering = ['-date_created'] @@ -108,7 +108,7 @@ class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet) serializer_class = CommandExecutionHostsRelationSerializer m2m_field = CommandExecution.hosts.field permission_classes = [IsOrgAdmin | IsOrgAuditor] - filter_fields = [ + filterset_fields = [ 'id', 'asset', 'commandexecution' ] search_fields = ('asset__hostname', ) diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index 74c8f598c..53a17298d 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -5,7 +5,7 @@ from rest_framework import serializers from django.db.models import F from common.mixins import BulkSerializerMixin -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from terminal.models import Session from ops.models import CommandExecution from . import models diff --git a/apps/audits/tasks.py b/apps/audits/tasks.py index 0091d250b..467967b82 100644 --- a/apps/audits/tasks.py +++ b/apps/audits/tasks.py @@ -40,6 +40,6 @@ def clean_ftp_log_period(): @register_as_period_task(interval=3600*24) @shared_task def clean_audits_log_period(): - clean_audits_log_period() + clean_login_log_period() clean_operation_log_period() clean_ftp_log_period() diff --git a/apps/authentication/api/auth.py b/apps/authentication/api/auth.py index cc77058ee..310aa3d4a 100644 --- a/apps/authentication/api/auth.py +++ b/apps/authentication/api/auth.py @@ -54,12 +54,3 @@ class UserConnectionTokenApi(RootOrgViewMixin, APIView): return Response(value) else: return Response({'user': value['user']}) - - def get_permissions(self): - if self.request.query_params.get('user-only', None): - self.permission_classes = (AllowAny,) - return super().get_permissions() - - - - diff --git a/apps/authentication/api/login_confirm.py b/apps/authentication/api/login_confirm.py index 4fbf3d5d3..6561962a9 100644 --- a/apps/authentication/api/login_confirm.py +++ b/apps/authentication/api/login_confirm.py @@ -45,5 +45,5 @@ class TicketStatusApi(mixins.AuthMixin, APIView): ticket = self.get_ticket() if ticket: request.session.pop('auth_ticket_id', '') - ticket.perform_status('closed', request.user) + ticket.close(processor=request.user) return Response('', status=200) diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 87b989283..0466a9ee2 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -187,12 +187,12 @@ class AuthMixin: if not ticket_id: ticket = None else: - ticket = Ticket.origin_objects.get(pk=ticket_id) + ticket = Ticket.all().filter(id=ticket_id).first() return ticket def get_ticket_or_create(self, confirm_setting): ticket = self.get_ticket() - if not ticket or ticket.status == ticket.STATUS.CLOSED: + if not ticket or ticket.status_closed: ticket = confirm_setting.create_confirm_ticket(self.request) self.request.session['auth_ticket_id'] = str(ticket.id) return ticket @@ -201,12 +201,16 @@ class AuthMixin: ticket = self.get_ticket() if not ticket: raise errors.LoginConfirmOtherError('', "Not found") - if ticket.status == ticket.STATUS.OPEN: + if ticket.status_open: raise errors.LoginConfirmWaitError(ticket.id) - elif ticket.action == ticket.ACTION.APPROVE: + elif ticket.action_approve: self.request.session["auth_confirm"] = "1" return - elif ticket.action == ticket.ACTION.REJECT: + elif ticket.action_reject: + raise errors.LoginConfirmOtherError( + ticket.id, ticket.get_action_display() + ) + elif ticket.action_close: raise errors.LoginConfirmOtherError( ticket.id, ticket.get_action_display() ) diff --git a/apps/authentication/models.py b/apps/authentication/models.py index a205a5190..b2f6ad602 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -49,29 +49,37 @@ class LoginConfirmSetting(CommonModelMixin): def get_user_confirm_setting(cls, user): return get_object_or_none(cls, user=user) - def create_confirm_ticket(self, request=None): - from tickets.models import Ticket - title = _('Login confirm') + ' {}'.format(self.user) + @staticmethod + def construct_confirm_ticket_meta(request=None): if request: - remote_addr = get_request_ip(request) - city = get_ip_city(remote_addr) - datetime = timezone.now().strftime('%Y-%m-%d %H:%M:%S') - body = __("{user_key}: {username}
" - "IP: {ip}
" - "{city_key}: {city}
" - "{date_key}: {date}
").format( - user_key=__("User"), username=self.user, - ip=remote_addr, city_key=_("City"), city=city, - date_key=__("Datetime"), date=datetime - ) + login_ip = get_request_ip(request) else: - body = '' - reviewer = self.reviewers.all() - ticket = Ticket.objects.create( - user=self.user, title=title, body=body, - type=Ticket.TYPE.LOGIN_CONFIRM, - ) - ticket.assignees.set(reviewer) + login_ip = '' + login_ip = login_ip or '0.0.0.0' + login_city = get_ip_city(login_ip) + login_datetime = timezone.now().strftime('%Y-%m-%d %H:%M:%S') + ticket_meta = { + 'apply_login_ip': login_ip, + 'apply_login_city': login_city, + 'apply_login_datetime': login_datetime, + } + return ticket_meta + + def create_confirm_ticket(self, request=None): + from tickets import const + from tickets.models import Ticket + ticket_title = _('Login confirm') + ' {}'.format(self.user) + ticket_applicant = self.user + ticket_meta = self.construct_confirm_ticket_meta(request) + ticket_assignees = self.reviewers.all() + data = { + 'title': ticket_title, + 'type': const.TicketTypeChoices.login_confirm.value, + 'applicant': ticket_applicant, + 'meta': ticket_meta, + } + ticket = Ticket.objects.create(**data) + ticket.assignees.set(ticket_assignees) return ticket def __str__(self): diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html index 521f3dbd2..e06567aa4 100644 --- a/apps/authentication/templates/authentication/login.html +++ b/apps/authentication/templates/authentication/login.html @@ -1,82 +1,179 @@ -{% extends '_base_only_msg_content.html' %} {% load static %} {% load i18n %} + + + + + + + + + {{ JMS_TITLE }} + + + + + + + -{% block content_title %} - {% trans 'Login' %} -{% endblock %} + + + + + -{% block content %} -
- {% csrf_token %} - {% if form.non_field_errors %} -
-

{{ form.non_field_errors.as_text }}

+ + + + +
+
+
+
- {% elif form.errors.captcha %} -

{% trans 'Captcha invalid' %}

- {% endif %} - -
- - {% if form.errors.username %} -
-

{{ form.errors.username.as_text }}

-
- {% endif %} -
-
- - - {% if form.errors.password %} -
-

{{ form.errors.password.as_text }}

-
- {% endif %} -
- {% if form.challenge %} -
- - {% if form.errors.challenge %} -
-

{{ form.errors.challenge.as_text }}

+
+
+ {% if form.challenge %} +
+ {% else %} +
+ {% endif %} + {{ JMS_TITLE }} +
+
+ {% trans 'Welcome back, please enter username and password to login' %}
- {% endif %} -
- {% endif %} -
- {{ form.captcha }} -
- +
+
+
+
+ + {% csrf_token %} + {% if form.non_field_errors %} + {% if form.challenge %} +
+ {% else %} +
+ {% endif %} +

{{ form.non_field_errors.as_text }}

+
+ {% elif form.errors.captcha %} +

{% trans 'Captcha invalid' %}

+ {% else %} +
+ {% endif %} - {% if demo_mode %} -

- Demo账号: admin 密码: admin -

- {% endif %} +
+ + {% if form.errors.username %} +
+

{{ form.errors.username.as_text }}

+
+ {% endif %} +
+
+ + + {% if form.errors.password %} +
+

{{ form.errors.password.as_text }}

+
+ {% endif %} +
+ {% if form.challenge %} +
+ + {% if form.errors.challenge %} +
+

{{ form.errors.challenge.as_text }}

+
+ {% endif %} +
+ {% endif %} + {% if form.captcha %} +
+ {{ form.captcha }} +
+ {% else %} +
+ {% endif %} +
+ +
-
-
- - {% trans 'Forgot password' %}? - +
+ {% if AUTH_OPENID or AUTH_CAS %} +
+
+ {% trans "More login options" %} + {% if AUTH_OPENID %} + + {% trans 'OpenID' %} + + {% endif %} + {% if AUTH_CAS %} + + {% trans 'CAS' %} + + {% endif %} +
+
+ {% else %} + +
+ +
+
+
+
+
+
+
- {% if AUTH_OPENID %} -
-

{% trans "More login options" %}

-
- -
- {% endif %} - - - - + -{% endblock %} + + + diff --git a/apps/authentication/templates/authentication/xpack_login.html b/apps/authentication/templates/authentication/xpack_login.html deleted file mode 100644 index d566ce1f8..000000000 --- a/apps/authentication/templates/authentication/xpack_login.html +++ /dev/null @@ -1,179 +0,0 @@ -{% load static %} -{% load i18n %} - - - - - - - - - {{ JMS_TITLE }} - - - - - - - - - - - - - - - - - - -
-
-
- -
-
-
- {% if form.challenge %} -
- {% else %} -
- {% endif %} - {{ JMS_TITLE }} -
-
- {% trans 'Welcome back, please enter username and password to login' %} -
-
-
-
-
-
- {% csrf_token %} - {% if form.non_field_errors %} - {% if form.challenge %} -
- {% else %} -
- {% endif %} -

{{ form.non_field_errors.as_text }}

-
- {% elif form.errors.captcha %} -

{% trans 'Captcha invalid' %}

- {% else %} -
- {% endif %} - -
- - {% if form.errors.username %} -
-

{{ form.errors.username.as_text }}

-
- {% endif %} -
-
- - - {% if form.errors.password %} -
-

{{ form.errors.password.as_text }}

-
- {% endif %} -
- {% if form.challenge %} -
- - {% if form.errors.challenge %} -
-

{{ form.errors.challenge.as_text }}

-
- {% endif %} -
- {% endif %} -
- {{ form.captcha }} -
-
- -
- - -
-
-
-
-
-
-
-
-
- - - - - - diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index dcff66905..7d7f1e8da 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -19,7 +19,6 @@ from django.conf import settings from django.urls import reverse_lazy from django.contrib.auth import BACKEND_SESSION_KEY -from common.const.front_urls import TICKET_DETAIL from common.utils import get_request_ip, get_object_or_none from users.utils import ( redirect_user_first_login_or_index @@ -42,42 +41,13 @@ __all__ = [ class UserLoginView(mixins.AuthMixin, FormView): key_prefix_captcha = "_LOGIN_INVALID_{}" redirect_field_name = 'next' - - def get_template_names(self): - template_name = 'authentication/login.html' - if not settings.XPACK_ENABLED: - return template_name - - from xpack.plugins.license.models import License - if not License.has_valid_license(): - return template_name - - template_name = 'authentication/xpack_login.html' - return template_name - - def get_redirect_url_if_need(self, request): - redirect_url = '' - # show jumpserver login page if request http://{JUMP-SERVER}/?admin=1 - if self.request.GET.get("admin", 0): - return None - if settings.AUTH_OPENID: - redirect_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME) - elif settings.AUTH_CAS: - redirect_url = reverse(settings.CAS_LOGIN_URL_NAME) - - if redirect_url: - query_string = request.GET.urlencode() - redirect_url = "{}?{}".format(redirect_url, query_string) - return redirect_url + template_name = 'authentication/login.html' def get(self, request, *args, **kwargs): if request.user.is_staff: return redirect(redirect_user_first_login_or_index( request, self.redirect_field_name) ) - redirect_url = self.get_redirect_url_if_need(request) - if redirect_url: - return redirect(redirect_url) request.session.set_test_cookie() return super().get(request, *args, **kwargs) @@ -132,8 +102,8 @@ class UserLoginView(mixins.AuthMixin, FormView): context = { 'demo_mode': os.environ.get("DEMO_MODE"), 'AUTH_OPENID': settings.AUTH_OPENID, + 'AUTH_CAS': settings.AUTH_CAS, 'rsa_public_key': rsa_public_key, - 'AUTH_DB': settings.AUTH_DB } kwargs.update(context) return super().get_context_data(**kwargs) @@ -181,6 +151,7 @@ class UserLoginWaitConfirmView(TemplateView): def get_context_data(self, **kwargs): from tickets.models import Ticket + from tickets.const import TICKET_DETAIL_URL ticket_id = self.request.session.get("auth_ticket_id") if not ticket_id: ticket = None @@ -189,7 +160,7 @@ class UserLoginWaitConfirmView(TemplateView): context = super().get_context_data(**kwargs) if ticket: timestamp_created = datetime.datetime.timestamp(ticket.date_created) - ticket_detail_url = TICKET_DETAIL.format(id=ticket_id) + ticket_detail_url = TICKET_DETAIL_URL.format(id=ticket_id) msg = _("""Wait for {} confirm, You also can copy link to her/him
Don't close this page""").format(ticket.assignees_display) else: diff --git a/apps/common/cache.py b/apps/common/cache.py new file mode 100644 index 000000000..f7c0b16cd --- /dev/null +++ b/apps/common/cache.py @@ -0,0 +1,187 @@ +import json +from django.core.cache import cache + +from common.utils.lock import DistributedLock +from common.utils import lazyproperty +from common.utils import get_logger + +logger = get_logger(__file__) + + +class CacheFieldBase: + field_type = str + + def __init__(self, queryset=None, compute_func_name=None): + assert None in (queryset, compute_func_name), f'queryset and compute_func_name can only have one' + self.compute_func_name = compute_func_name + self.queryset = queryset + + +class CharField(CacheFieldBase): + field_type = str + + +class IntegerField(CacheFieldBase): + field_type = int + + +class CacheBase(type): + def __new__(cls, name, bases, attrs: dict): + to_update = {} + field_desc_mapper = {} + + for k, v in attrs.items(): + if isinstance(v, CacheFieldBase): + desc = CacheValueDesc(k, v) + to_update[k] = desc + field_desc_mapper[k] = desc + + attrs.update(to_update) + attrs['field_desc_mapper'] = field_desc_mapper + return type.__new__(cls, name, bases, attrs) + + +class Cache(metaclass=CacheBase): + field_desc_mapper: dict + timeout = None + + def __init__(self): + self._data = None + + @lazyproperty + def key_suffix(self): + return self.get_key_suffix() + + @property + def key_prefix(self): + clz = self.__class__ + return f'cache.{clz.__module__}.{clz.__name__}' + + @property + def key(self): + return f'{self.key_prefix}.{self.key_suffix}' + + @property + def data(self): + if self._data is None: + data = self.get_data() + if data is None: + # 缓存中没有数据时,去数据库获取 + self.compute_and_set_all_data() + return self._data + + def get_data(self) -> dict: + data = cache.get(self.key) + logger.debug(f'CACHE: get {self.key} = {data}') + if data is not None: + data = json.loads(data) + self._data = data + return data + + def set_data(self, data): + self._data = data + to_json = json.dumps(data) + logger.info(f'CACHE: set {self.key} = {to_json}, timeout={self.timeout}') + cache.set(self.key, to_json, timeout=self.timeout) + + def _compute_data(self, *fields): + field_descs = [] + if not fields: + field_descs = self.field_desc_mapper.values() + else: + for field in fields: + assert field in self.field_desc_mapper, f'{field} is not a valid field' + field_descs.append(self.field_desc_mapper[field]) + data = { + field_desc.field_name: field_desc.compute_value(self) + for field_desc in field_descs + } + return data + + def compute_and_set_all_data(self, computed_data: dict = None): + """ + TODO 怎样防止并发更新全部数据,浪费数据库资源 + """ + uncomputed_keys = () + if computed_data: + computed_keys = computed_data.keys() + all_keys = self.field_desc_mapper.keys() + uncomputed_keys = all_keys - computed_keys + else: + computed_data = {} + data = self._compute_data(*uncomputed_keys) + data.update(computed_data) + self.set_data(data) + return data + + def refresh_part_data_with_lock(self, refresh_data): + with DistributedLock(name=f'{self.key}.refresh'): + data = self.get_data() + if data is not None: + data.update(refresh_data) + self.set_data(data) + return data + + def refresh(self, *fields): + if not fields: + # 没有指定 field 要刷新所有的值 + self.compute_and_set_all_data() + return + + data = self.get_data() + if data is None: + # 缓存中没有数据,设置所有的值 + self.compute_and_set_all_data() + return + + refresh_data = self._compute_data(*fields) + if not self.refresh_part_data_with_lock(refresh_data): + # 刷新部分失败,缓存中没有数据,更新所有的值 + self.compute_and_set_all_data(refresh_data) + return + + def get_key_suffix(self): + raise NotImplementedError + + def reload(self): + self._data = None + + def delete(self): + self._data = None + logger.info(f'CACHE: delete {self.key}') + cache.delete(self.key) + + +class CacheValueDesc: + def __init__(self, field_name, field_type: CacheFieldBase): + self.field_name = field_name + self.field_type = field_type + self._data = None + + def __repr__(self): + clz = self.__class__ + return f'<{clz.__name__} {self.field_name} {self.field_type}>' + + def __get__(self, instance: Cache, owner): + if instance is None: + return self + if self.field_name not in instance.data: + instance.refresh(self.field_name) + value = instance.data[self.field_name] + return value + + def compute_value(self, instance: Cache): + if self.field_type.queryset is not None: + new_value = self.field_type.queryset.count() + else: + compute_func_name = self.field_type.compute_func_name + if not compute_func_name: + compute_func_name = f'compute_{self.field_name}' + compute_func = getattr(instance, compute_func_name, None) + assert compute_func is not None, \ + f'Define `{compute_func_name}` method in {instance.__class__}' + new_value = compute_func() + + new_value = self.field_type.field_type(new_value) + logger.info(f'CACHE: compute {instance.key}.{self.field_name} = {new_value}') + return new_value diff --git a/apps/common/const/choices.py b/apps/common/const/choices.py index 8de0c5fc8..6bff02254 100644 --- a/apps/common/const/choices.py +++ b/apps/common/const/choices.py @@ -1,7 +1,3 @@ -from django.utils.translation import ugettext_lazy as _ - -from common.db.models import ChoiceSet - ADMIN = 'Admin' USER = 'User' diff --git a/apps/common/const/front_urls.py b/apps/common/const/front_urls.py deleted file mode 100644 index 12d47ed17..000000000 --- a/apps/common/const/front_urls.py +++ /dev/null @@ -1,2 +0,0 @@ - -TICKET_DETAIL = '/ui/#/tickets/tickets/{id}' diff --git a/apps/common/const/signals.py b/apps/common/const/signals.py index b28c1310b..5d35518ab 100644 --- a/apps/common/const/signals.py +++ b/apps/common/const/signals.py @@ -12,3 +12,6 @@ PRE_REMOVE = 'pre_remove' POST_REMOVE = 'post_remove' PRE_CLEAR = 'pre_clear' POST_CLEAR = 'post_clear' + +POST_PREFIX = 'post' +PRE_PREFIX = 'pre' diff --git a/apps/common/db/utils.py b/apps/common/db/utils.py index e8b6f8872..d3d735fea 100644 --- a/apps/common/db/utils.py +++ b/apps/common/db/utils.py @@ -35,6 +35,6 @@ def get_objects(model, pks): if len(objs) != len(pks): pks = set(pks) exists_pks = {o.id for o in objs} - not_found_pks = ','.join(pks - exists_pks) + not_found_pks = pks - exists_pks logger.error(f'DoesNotExist: <{model.__name__}: {not_found_pks}>') return objs diff --git a/apps/common/drf/fields.py b/apps/common/drf/fields.py index e3b333d56..9b92af381 100644 --- a/apps/common/drf/fields.py +++ b/apps/common/drf/fields.py @@ -1,43 +1,25 @@ -from uuid import UUID +# -*- coding: utf-8 -*- +# -from rest_framework.fields import get_attribute -from rest_framework.relations import ManyRelatedField, PrimaryKeyRelatedField, MANY_RELATION_KWARGS +from rest_framework import serializers -class GroupConcatedManyRelatedField(ManyRelatedField): - def get_attribute(self, instance): - if hasattr(instance, 'pk') and instance.pk is None: - return [] - - attr = self.source_attrs[-1] - - # `gc` 是 `GroupConcat` 的缩写 - gc_attr = f'gc_{attr}' - if hasattr(instance, gc_attr): - gc_value = getattr(instance, gc_attr) - if isinstance(gc_value, str): - return [UUID(pk) for pk in set(gc_value.split(','))] - else: - return '' - - relationship = get_attribute(instance, self.source_attrs) - return relationship.all() if hasattr(relationship, 'all') else relationship +__all__ = [ + 'ReadableHiddenField', +] -class GroupConcatedPrimaryKeyRelatedField(PrimaryKeyRelatedField): - @classmethod - def many_init(cls, *args, **kwargs): - list_kwargs = {'child_relation': cls(*args, **kwargs)} - for key in kwargs: - if key in MANY_RELATION_KWARGS: - list_kwargs[key] = kwargs[key] - return GroupConcatedManyRelatedField(**list_kwargs) +# ReadableHiddenField +# ------------------- + + +class ReadableHiddenField(serializers.HiddenField): + """ 可读的 HiddenField """ + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.write_only = False def to_representation(self, value): - if self.pk_field is not None: - return self.pk_field.to_representation(value.pk) - - if hasattr(value, 'pk'): - return value.pk - else: - return value + if hasattr(value, 'id'): + return getattr(value, 'id') + return value diff --git a/apps/common/drf/metadata.py b/apps/common/drf/metadata.py index 6020e66fb..eda1dd561 100644 --- a/apps/common/drf/metadata.py +++ b/apps/common/drf/metadata.py @@ -92,6 +92,13 @@ class SimpleMetadataWithFilters(SimpleMetadata): fields = view.get_filter_fields(request) elif hasattr(view, 'filter_fields'): fields = view.filter_fields + elif hasattr(view, 'filterset_fields'): + fields = view.filterset_fields + elif hasattr(view, 'get_filterset_fields'): + fields = view.get_filterset_fields(request) + + if isinstance(fields, dict): + fields = list(fields.keys()) return fields def get_ordering_fields(self, request, view): @@ -104,12 +111,12 @@ class SimpleMetadataWithFilters(SimpleMetadata): def determine_metadata(self, request, view): metadata = super(SimpleMetadataWithFilters, self).determine_metadata(request, view) - filter_fields = self.get_filters_fields(request, view) + filterset_fields = self.get_filters_fields(request, view) order_fields = self.get_ordering_fields(request, view) meta_get = metadata.get("actions", {}).get("GET", {}) for k, v in meta_get.items(): - if k in filter_fields: + if k in filterset_fields: v["filter"] = True if k in order_fields: v["order"] = True diff --git a/apps/common/drf/parsers/base.py b/apps/common/drf/parsers/base.py index 605dcdd08..c8619b34e 100644 --- a/apps/common/drf/parsers/base.py +++ b/apps/common/drf/parsers/base.py @@ -1,6 +1,7 @@ import abc import json import codecs +from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from rest_framework.parsers import BaseParser from rest_framework import status @@ -83,14 +84,17 @@ class BaseFileParser(BaseParser): new_row.append(col) return new_row - @staticmethod - def process_row_data(row_data): + def process_row_data(self, row_data): """ 构建json数据后的行数据处理 """ new_row_data = {} + serializer_fields = self.serializer_cls().fields for k, v in row_data.items(): if isinstance(v, list) or isinstance(v, dict) or isinstance(v, str) and k.strip() and v.strip(): + # 解决类似disk_info为字符串的'{}'的问题 + if not isinstance(v, str) and isinstance(serializer_fields[k], serializers.CharField): + v = str(v) new_row_data[k] = v return new_row_data diff --git a/apps/common/drf/serializers.py b/apps/common/drf/serializers.py index e767c32aa..4beb59fe7 100644 --- a/apps/common/drf/serializers.py +++ b/apps/common/drf/serializers.py @@ -1,12 +1,70 @@ +import copy +from rest_framework import serializers from rest_framework.serializers import Serializer from rest_framework.serializers import ModelSerializer -from rest_framework import serializers from rest_framework_bulk.serializers import BulkListSerializer -from common.mixins.serializers import BulkSerializerMixin from common.mixins import BulkListSerializerMixin +from django.utils.functional import cached_property +from rest_framework.utils.serializer_helpers import BindingDict +from common.mixins.serializers import BulkSerializerMixin -__all__ = ['EmptySerializer', 'BulkModelSerializer'] +__all__ = [ + 'MethodSerializer', + 'EmptySerializer', 'BulkModelSerializer', 'AdaptedBulkListSerializer', 'CeleryTaskSerializer' +] + + +# MethodSerializer +# ---------------- + + +class MethodSerializer(serializers.Serializer): + + def __init__(self, method_name=None, **kwargs): + self.method_name = method_name + super().__init__(**kwargs) + + class Meta: + # 生成swagger时使用 + ref_name = None + + def bind(self, field_name, parent): + if self.method_name is None: + method_name = 'get_{field_name}_serializer'.format(field_name=field_name) + self.method_name = method_name + + super().bind(field_name, parent) + + @cached_property + def serializer(self) -> serializers.Serializer: + method = getattr(self.parent, self.method_name) + _serializer = method() + # 设置serializer的parent值,否则在serializer实例中获取parent会出现断层 + setattr(_serializer, 'parent', self.parent) + return _serializer + + @cached_property + def fields(self): + """ + 重写此方法因为在 BindingDict 中要设置每一个 field 的 parent 为 `serializer`, + 这样在调用 field.parent 时, 才会达到预期的结果, + 比如: serializers.SerializerMethodField + """ + 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 +# ---------------- class EmptySerializer(Serializer): @@ -23,3 +81,5 @@ class AdaptedBulkListSerializer(BulkListSerializerMixin, BulkListSerializer): class CeleryTaskSerializer(serializers.Serializer): task = serializers.CharField(read_only=True) + + diff --git a/apps/common/fields/__init__.py b/apps/common/fields/__init__.py index f918a3279..cb3ff2081 100644 --- a/apps/common/fields/__init__.py +++ b/apps/common/fields/__init__.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- # -from .form import * from .model import * -from .serializer import * diff --git a/apps/common/fields/form.py b/apps/common/fields/form.py deleted file mode 100644 index fd144ec92..000000000 --- a/apps/common/fields/form.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- -# -import json - -from django import forms -import six -from django.core.exceptions import ValidationError -from django.utils.translation import ugettext as _ -from ..utils import signer - - -__all__ = [ - 'FormDictField', 'FormEncryptCharField', 'FormEncryptDictField', - 'FormEncryptMixin', -] - - -class FormDictField(forms.Field): - widget = forms.Textarea - - def to_python(self, value): - """Returns a Python boolean object.""" - # Explicitly check for the string 'False', which is what a hidden field - # will submit for False. Also check for '0', since this is what - # RadioSelect will provide. Because bool("True") == bool('1') == True, - # we don't need to handle that explicitly. - if isinstance(value, six.string_types): - value = value.replace("'", '"') - try: - value = json.loads(value) - return value - except json.JSONDecodeError: - return ValidationError(_("Not a valid json")) - else: - return ValidationError(_("Not a string type")) - - def validate(self, value): - if isinstance(value, ValidationError): - raise value - if not value and self.required: - raise ValidationError(self.error_messages['required'], code='required') - - def has_changed(self, initial, data): - # Sometimes data or initial may be a string equivalent of a boolean - # so we should run it through to_python first to get a boolean value - return self.to_python(initial) != self.to_python(data) - - -class FormEncryptMixin: - pass - - -class FormEncryptCharField(FormEncryptMixin, forms.CharField): - pass - - -class FormEncryptDictField(FormEncryptMixin, FormDictField): - pass - - - - diff --git a/apps/common/fields/serializer.py b/apps/common/fields/serializer.py deleted file mode 100644 index 9cd630650..000000000 --- a/apps/common/fields/serializer.py +++ /dev/null @@ -1,121 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from rest_framework import serializers -import six - - -__all__ = [ - 'StringIDField', 'StringManyToManyField', 'ChoiceDisplayField', - 'CustomMetaDictField' -] - - -class StringIDField(serializers.Field): - def to_representation(self, value): - return {"pk": value.pk, "name": value.__str__()} - - -class StringManyToManyField(serializers.RelatedField): - def to_representation(self, value): - return value.__str__() - - -class ChoiceDisplayField(serializers.ChoiceField): - def __init__(self, *args, **kwargs): - super(ChoiceDisplayField, self).__init__(*args, **kwargs) - self.choice_strings_to_display = { - six.text_type(key): value for key, value in self.choices.items() - } - - def to_representation(self, value): - if value is None: - return value - return { - 'value': self.choice_strings_to_values.get(six.text_type(value), value), - 'display': self.choice_strings_to_display.get(six.text_type(value), value), - } - - -class DictField(serializers.DictField): - def to_representation(self, value): - if not value or not isinstance(value, dict): - value = {} - return super().to_representation(value) - - -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/mixins/api.py b/apps/common/mixins/api.py index 6754bf178..4aa10deec 100644 --- a/apps/common/mixins/api.py +++ b/apps/common/mixins/api.py @@ -28,19 +28,39 @@ class JSONResponseMixin(object): return JsonResponse(context) +# SerializerMixin +# ---------------------- + + class SerializerMixin: - def get_serializer_class(self): + """ 根据用户请求动作的不同,获取不同的 `serializer_class `""" + + serializer_classes = None + + def get_serializer_class_by_view_action(self): + if not hasattr(self, 'serializer_classes'): + return None + if not isinstance(self.serializer_classes, dict): + return None + action = self.request.query_params.get('action') + serializer_class = None - if hasattr(self, 'serializer_classes') and isinstance(self.serializer_classes, dict): - if self.action in ['list', 'metadata'] and self.request.query_params.get('draw'): - serializer_class = self.serializer_classes.get('display') - if serializer_class is None: - serializer_class = self.serializer_classes.get( - self.action, self.serializer_classes.get('default') - ) - if serializer_class: - return serializer_class - return super().get_serializer_class() + if action: + # metadata方法 使用 action 参数获取 + serializer_class = self.serializer_classes.get(action) + if serializer_class is None: + serializer_class = self.serializer_classes.get(self.action) + if serializer_class is None: + serializer_class = self.serializer_classes.get('display') + if serializer_class is None: + serializer_class = self.serializer_classes.get('default') + return serializer_class + + def get_serializer_class(self): + serializer_class = self.get_serializer_class_by_view_action() + if serializer_class is None: + serializer_class = super().get_serializer_class() + return serializer_class class ExtraFilterFieldsMixin: diff --git a/apps/common/mixins/serializers.py b/apps/common/mixins/serializers.py index d9df17e1d..020af68d7 100644 --- a/apps/common/mixins/serializers.py +++ b/apps/common/mixins/serializers.py @@ -124,6 +124,22 @@ class BulkListSerializerMixin(object): return ret + def create(self, validated_data): + ModelClass = self.child.Meta.model + use_model_bulk_create = getattr(self.child.Meta, 'use_model_bulk_create', False) + model_bulk_create_kwargs = getattr(self.child.Meta, 'model_bulk_create_kwargs', {}) + + if use_model_bulk_create: + to_create = [ + ModelClass(**attrs) for attrs in validated_data + ] + objs = ModelClass._default_manager.bulk_create( + to_create, **model_bulk_create_kwargs + ) + return objs + else: + return super().create(validated_data) + class BaseDynamicFieldsPlugin: def __init__(self, serializer): diff --git a/apps/common/serializers.py b/apps/common/serializers.py deleted file mode 100644 index 971060641..000000000 --- a/apps/common/serializers.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -老的代码统一到 `apps/common/drf/serializers.py` 中, -之后此文件废弃 -""" - -from common.drf.serializers import AdaptedBulkListSerializer, CeleryTaskSerializer diff --git a/apps/common/tasks.py b/apps/common/tasks.py index eeeee7214..715be5103 100644 --- a/apps/common/tasks.py +++ b/apps/common/tasks.py @@ -4,7 +4,6 @@ from celery import shared_task from .utils import get_logger - logger = get_logger(__file__) diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index 25b2e771b..8bc7377e5 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # import re +import data_tree from collections import OrderedDict from itertools import chain import logging @@ -10,6 +11,8 @@ from functools import wraps import time import ipaddress import psutil +from django.utils.translation import ugettext_lazy as _ +from ..exceptions import JMSException UUID_PATTERN = re.compile(r'\w{8}(-\w{4}){3}-\w{12}') diff --git a/apps/common/utils/lock.py b/apps/common/utils/lock.py index 9041a2578..04ee1520f 100644 --- a/apps/common/utils/lock.py +++ b/apps/common/utils/lock.py @@ -1,4 +1,5 @@ from functools import wraps +import threading from redis_lock import Lock as RedisLock from redis import Redis @@ -35,11 +36,16 @@ class DistributedLock(RedisLock): self._blocking = blocking def __enter__(self): + thread_id = threading.current_thread().ident + logger.debug(f'DISTRIBUTED_LOCK: attempt to acquire ...') acquired = self.acquire(blocking=self._blocking) if self._blocking and not acquired: + logger.debug(f'DISTRIBUTED_LOCK: was not acquired , but blocking=True') raise EnvironmentError("Lock wasn't acquired, but blocking=True") if not acquired: + logger.debug(f'DISTRIBUTED_LOCK: acquire failed') raise AcquireFailed + logger.debug(f'DISTRIBUTED_LOCK: acquire ok') return self def __exit__(self, exc_type=None, exc_value=None, traceback=None): diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 4d8b7c0ce..602916cb6 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -461,9 +461,6 @@ class DynamicConfig: backends.insert(0, 'authentication.backends.api.SSOAuthentication') return backends - def AUTH_DB(self): - return len(self.AUTHENTICATION_BACKENDS()) == 2 - def XPACK_LICENSE_IS_VALID(self): if not HAS_XPACK: return False diff --git a/apps/jumpserver/context_processor.py b/apps/jumpserver/context_processor.py index cf0ea559d..7fdc7eab2 100644 --- a/apps/jumpserver/context_processor.py +++ b/apps/jumpserver/context_processor.py @@ -13,6 +13,7 @@ def jumpserver_processor(request): 'LOGO_TEXT_URL': static('img/logo_text.png'), 'LOGIN_IMAGE_URL': static('img/login_image.png'), 'FAVICON_URL': static('img/facio.ico'), + 'LOGIN_CAS_LOGO_URL': static('img/login_cas_logo.png'), 'JMS_TITLE': 'JumpServer', 'VERSION': settings.VERSION, 'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2020', diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 9f91cdb1d..4430aae2f 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -9,9 +9,6 @@ from ..const import CONFIG, DYNAMIC, PROJECT_DIR OTP_ISSUER_NAME = CONFIG.OTP_ISSUER_NAME OTP_VALID_WINDOW = CONFIG.OTP_VALID_WINDOW -# Auth DB -AUTH_DB = DYNAMIC.AUTH_DB - # Auth LDAP settings AUTH_LDAP = DYNAMIC.AUTH_LDAP AUTH_LDAP_SERVER_URI = DYNAMIC.AUTH_LDAP_SERVER_URI diff --git a/apps/jumpserver/settings/libs.py b/apps/jumpserver/settings/libs.py index 8a8df7ca4..aac0c4d50 100644 --- a/apps/jumpserver/settings/libs.py +++ b/apps/jumpserver/settings/libs.py @@ -125,3 +125,5 @@ CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = "INFO" # CELERY_WORKER_HIJACK_ROOT_LOGGER = True # CELERY_WORKER_MAX_TASKS_PER_CHILD = 40 CELERY_TASK_SOFT_TIME_LIMIT = 3600 + +ANSIBLE_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'ansible') diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 8a1afc835..b346f4804 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 4e118555d..88119e6f5 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-12-17 18:22+0800\n" +"POT-Creation-Date: 2021-01-17 16:12+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -17,31 +17,29 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: applications/const.py:53 applications/models/application.py:33 -msgid "Custom" -msgstr "自定义" - -#: applications/models/application.py:60 applications/models/database_app.py:29 -#: applications/serializers/database_app.py:16 -#: applications/serializers/remote_app.py:69 +#: applications/const.py:9 +#: applications/serializers/attrs/application_category/db.py:14 +#: applications/serializers/attrs/application_type/mysql_workbench.py:26 #: users/templates/users/user_granted_database_app.html:37 msgid "Database" msgstr "数据库" -#: applications/models/application.py:61 +#: applications/const.py:10 msgid "Remote app" msgstr "远程应用" -#: applications/models/application.py:121 -#: applications/models/database_app.py:18 applications/models/k8s_app.py:11 -#: applications/models/remote_app.py:21 assets/models/asset.py:149 +#: applications/const.py:29 +msgid "Custom" +msgstr "自定义" + +#: applications/models/application.py:10 assets/models/asset.py:149 #: assets/models/base.py:234 assets/models/cluster.py:18 #: assets/models/cmd_filter.py:21 assets/models/domain.py:21 #: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24 #: orgs/models.py:23 perms/models/base.py:48 settings/models.py:27 -#: terminal/models/storage.py:21 terminal/models/storage.py:58 +#: terminal/models/storage.py:15 terminal/models/storage.py:55 #: terminal/models/task.py:16 terminal/models/terminal.py:131 -#: users/forms/profile.py:20 users/models/group.py:15 users/models/user.py:495 +#: users/forms/profile.py:20 users/models/group.py:15 users/models/user.py:518 #: users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 @@ -59,45 +57,49 @@ msgstr "远程应用" msgid "Name" msgstr "名称" -#: applications/models/application.py:122 assets/models/asset.py:198 -#: assets/models/domain.py:27 assets/models/domain.py:54 -msgid "Domain" -msgstr "网域" - -#: applications/models/application.py:123 -#: applications/serializers/application.py:16 assets/models/label.py:21 -#: perms/models/application_permission.py:19 +#: applications/models/application.py:12 +#: applications/serializers/application.py:41 assets/models/label.py:21 +#: perms/models/application_permission.py:20 #: perms/serializers/application/permission.py:16 #: perms/serializers/application/user_permission.py:33 +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:18 msgid "Category" -msgstr "分类" +msgstr "种类" -#: applications/models/application.py:124 -#: applications/models/database_app.py:22 applications/models/k8s_app.py:14 -#: applications/serializers/application.py:17 assets/models/cmd_filter.py:52 -#: perms/models/application_permission.py:20 +#: applications/models/application.py:15 +#: applications/serializers/application.py:42 assets/models/cmd_filter.py:52 +#: perms/models/application_permission.py:23 #: perms/serializers/application/permission.py:17 #: perms/serializers/application/user_permission.py:34 -#: terminal/models/storage.py:23 terminal/models/storage.py:60 -#: tickets/models/ticket.py:40 +#: terminal/models/storage.py:18 terminal/models/storage.py:58 +#: tickets/models/ticket.py:38 +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:25 +#: tickets/serializers/ticket/ticket.py:19 #: users/templates/users/user_granted_database_app.html:35 msgid "Type" msgstr "类型" +#: applications/models/application.py:19 assets/models/asset.py:198 +#: assets/models/domain.py:27 assets/models/domain.py:55 +msgid "Domain" +msgstr "网域" + +#: applications/models/application.py:21 +msgid "Attrs" +msgstr "" + # msgid "Date created" # msgstr "创建日期" -#: applications/models/application.py:127 -#: applications/models/database_app.py:33 applications/models/k8s_app.py:18 -#: applications/models/remote_app.py:45 assets/models/asset.py:154 +#: applications/models/application.py:23 assets/models/asset.py:154 #: assets/models/asset.py:230 assets/models/base.py:239 #: assets/models/cluster.py:29 assets/models/cmd_filter.py:23 #: assets/models/cmd_filter.py:57 assets/models/domain.py:22 -#: assets/models/domain.py:55 assets/models/group.py:23 +#: assets/models/domain.py:56 assets/models/group.py:23 #: assets/models/label.py:23 ops/models/adhoc.py:37 orgs/models.py:26 -#: perms/models/base.py:56 settings/models.py:32 terminal/models/storage.py:28 -#: terminal/models/storage.py:65 terminal/models/terminal.py:145 -#: tickets/models/ticket.py:43 users/models/group.py:16 -#: users/models/user.py:528 users/templates/users/user_detail.html:115 +#: perms/models/base.py:56 settings/models.py:32 terminal/models/storage.py:21 +#: terminal/models/storage.py:61 terminal/models/terminal.py:145 +#: tickets/models/ticket.py:73 users/models/group.py:16 +#: users/models/user.py:551 users/templates/users/user_detail.html:115 #: users/templates/users/user_granted_database_app.html:38 #: users/templates/users/user_granted_remote_app.html:37 #: users/templates/users/user_group_detail.html:62 @@ -108,53 +110,32 @@ msgstr "类型" msgid "Comment" msgstr "备注" -#: applications/models/database_app.py:25 -#: applications/serializers/database_app.py:13 ops/models/adhoc.py:146 -#: users/templates/users/user_granted_database_app.html:36 -msgid "Host" -msgstr "主机" - -#: applications/models/database_app.py:27 -#: applications/serializers/database_app.py:14 -#: applications/serializers/database_app.py:20 -#: applications/serializers/database_app.py:24 -#: applications/serializers/database_app.py:28 -#: applications/serializers/remote_app.py:68 assets/models/asset.py:195 -#: assets/models/domain.py:52 -msgid "Port" -msgstr "端口" - -#: applications/models/database_app.py:41 -#: perms/forms/database_app_permission.py:44 -#: perms/models/database_app_permission.py:18 -#: perms/utils/database_app_permission.py:77 templates/_nav.html:66 -#: templates/_nav.html:86 templates/_nav_user.html:22 -#: users/templates/users/user_database_app_permission.html:39 -#: users/templates/users/user_database_app_permission.html:64 -msgid "DatabaseApp" -msgstr "数据库应用" - -#: applications/models/k8s_app.py:9 -msgid "Kubernetes" -msgstr "" - -#: applications/models/k8s_app.py:16 applications/serializers/k8s_app.py:9 +#: applications/serializers/attrs/application_category/cloud.py:9 #: assets/models/cluster.py:40 msgid "Cluster" msgstr "集群" -#: applications/models/k8s_app.py:26 perms/models/k8s_app_permission.py:18 -#: perms/utils/k8s_app_permission.py:70 -msgid "KubernetesApp" -msgstr "Kubernetes应用" +#: applications/serializers/attrs/application_category/db.py:11 +#: ops/models/adhoc.py:146 +#: users/templates/users/user_granted_database_app.html:36 +msgid "Host" +msgstr "主机" -#: applications/models/remote_app.py:23 -#: applications/serializers/remote_app.py:36 assets/models/asset.py:363 -#: assets/models/authbook.py:26 assets/models/gathered_user.py:14 -#: assets/serializers/admin_user.py:32 assets/serializers/asset_user.py:47 -#: assets/serializers/asset_user.py:84 assets/serializers/system_user.py:191 -#: audits/models.py:38 perms/forms/asset_permission.py:89 -#: perms/models/asset_permission.py:94 templates/index.html:82 +#: applications/serializers/attrs/application_category/db.py:12 +#: applications/serializers/attrs/application_type/mysql.py:11 +#: applications/serializers/attrs/application_type/mysql_workbench.py:22 +#: applications/serializers/attrs/application_type/oracle.py:11 +#: applications/serializers/attrs/application_type/pgsql.py:11 +#: assets/models/asset.py:195 assets/models/domain.py:53 +msgid "Port" +msgstr "端口" + +#: applications/serializers/attrs/application_category/remote_app.py:33 +#: assets/models/asset.py:363 assets/models/authbook.py:26 +#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:32 +#: assets/serializers/asset_user.py:47 assets/serializers/asset_user.py:84 +#: assets/serializers/system_user.py:191 audits/models.py:38 +#: perms/models/asset_permission.py:96 templates/index.html:82 #: terminal/backends/command/models.py:19 #: terminal/backends/command/serializers.py:13 terminal/models/session.py:39 #: users/templates/users/user_asset_permission.html:40 @@ -165,81 +146,26 @@ msgstr "Kubernetes应用" msgid "Asset" msgstr "资产" -#: applications/models/remote_app.py:28 -#: users/templates/users/user_granted_remote_app.html:35 -msgid "App type" -msgstr "应用类型" - -#: applications/models/remote_app.py:32 -msgid "App path" -msgstr "应用路径" - -#: applications/models/remote_app.py:36 -#: applications/serializers/remote_app.py:146 -msgid "Parameters" -msgstr "参数" - -#: applications/models/remote_app.py:39 assets/models/asset.py:228 -#: assets/models/base.py:242 assets/models/cluster.py:28 -#: assets/models/cmd_filter.py:26 assets/models/cmd_filter.py:60 -#: assets/models/group.py:21 common/db/models.py:67 common/mixins/models.py:49 -#: orgs/models.py:24 orgs/models.py:400 perms/models/base.py:54 -#: users/models/user.py:536 users/serializers/group.py:35 -#: users/templates/users/user_detail.html:97 -#: xpack/plugins/change_auth_plan/models.py:81 xpack/plugins/cloud/models.py:58 -#: xpack/plugins/cloud/models.py:156 xpack/plugins/gathered_user/models.py:30 -msgid "Created by" -msgstr "创建者" - -# msgid "Created by" -# msgstr "创建者" -#: applications/models/remote_app.py:42 assets/models/asset.py:229 -#: assets/models/base.py:240 assets/models/cluster.py:26 -#: assets/models/domain.py:24 assets/models/gathered_user.py:19 -#: assets/models/group.py:22 assets/models/label.py:25 common/db/models.py:69 -#: common/mixins/models.py:50 ops/models/adhoc.py:38 ops/models/command.py:27 -#: orgs/models.py:25 orgs/models.py:398 perms/models/base.py:55 -#: users/models/group.py:18 users/templates/users/user_group_detail.html:58 -#: xpack/plugins/cloud/models.py:61 xpack/plugins/cloud/models.py:159 -msgid "Date created" -msgstr "创建日期" - -#: applications/models/remote_app.py:49 perms/forms/remote_app_permission.py:46 -#: perms/models/remote_app_permission.py:15 -#: perms/utils/remote_app_permission.py:76 templates/_nav.html:64 -#: templates/_nav.html:82 templates/_nav_user.html:16 -#: users/templates/users/user_remote_app_permission.html:39 -#: users/templates/users/user_remote_app_permission.html:64 -msgid "RemoteApp" -msgstr "远程应用" - -#: applications/serializers/database_app.py:49 -#: applications/serializers/k8s_app.py:17 -#: applications/serializers/remote_app.py:162 audits/serializers.py:26 -msgid "Type for display" -msgstr "类型(显示名称)" - -#: applications/serializers/remote_app.py:37 -#: applications/serializers/remote_app.py:58 -#: applications/serializers/remote_app.py:66 -#: applications/serializers/remote_app.py:76 +#: applications/serializers/attrs/application_category/remote_app.py:36 +#: applications/serializers/attrs/application_type/chrome.py:14 +#: applications/serializers/attrs/application_type/mysql_workbench.py:14 +#: applications/serializers/attrs/application_type/vmware_client.py:18 msgid "Application path" msgstr "应用路径" -#: applications/serializers/remote_app.py:59 -#: applications/serializers/remote_app.py:77 +#: applications/serializers/attrs/application_type/chrome.py:17 +#: applications/serializers/attrs/application_type/vmware_client.py:22 msgid "Target URL" msgstr "目标URL" -#: applications/serializers/remote_app.py:60 -#: applications/serializers/remote_app.py:70 -#: applications/serializers/remote_app.py:78 -#: applications/serializers/remote_app.py:85 assets/models/base.py:235 -#: assets/models/gathered_user.py:15 audits/models.py:99 -#: authentication/forms.py:11 -#: authentication/templates/authentication/login.html:21 -#: authentication/templates/authentication/xpack_login.html:101 -#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:493 +#: applications/serializers/attrs/application_type/chrome.py:20 +#: applications/serializers/attrs/application_type/custom.py:21 +#: applications/serializers/attrs/application_type/mysql_workbench.py:30 +#: applications/serializers/attrs/application_type/vmware_client.py:26 +#: assets/models/base.py:235 assets/models/gathered_user.py:15 +#: audits/models.py:99 authentication/forms.py:11 +#: authentication/templates/authentication/login.html:101 +#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:516 #: users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:53 #: users/templates/users/user_list.html:15 @@ -249,14 +175,13 @@ msgstr "目标URL" msgid "Username" msgstr "用户名" -#: applications/serializers/remote_app.py:61 -#: applications/serializers/remote_app.py:71 -#: applications/serializers/remote_app.py:79 -#: applications/serializers/remote_app.py:86 assets/models/base.py:236 -#: assets/serializers/asset_user.py:71 audits/signals_handler.py:42 -#: authentication/forms.py:13 -#: authentication/templates/authentication/login.html:29 -#: authentication/templates/authentication/xpack_login.html:109 +#: applications/serializers/attrs/application_type/chrome.py:23 +#: applications/serializers/attrs/application_type/custom.py:25 +#: applications/serializers/attrs/application_type/mysql_workbench.py:34 +#: applications/serializers/attrs/application_type/vmware_client.py:30 +#: assets/models/base.py:236 assets/serializers/asset_user.py:71 +#: audits/signals_handler.py:42 authentication/forms.py:13 +#: authentication/templates/authentication/login.html:109 #: users/forms/user.py:22 users/forms/user.py:193 #: users/templates/users/user_otp_check_password.html:13 #: users/templates/users/user_password_update.html:43 @@ -270,26 +195,22 @@ msgstr "用户名" msgid "Password" msgstr "密码" -#: applications/serializers/remote_app.py:67 assets/models/asset.py:190 -#: assets/models/domain.py:51 assets/serializers/asset_user.py:46 -#: settings/serializers/settings.py:52 +#: applications/serializers/attrs/application_type/custom.py:13 +msgid "Operating parameter" +msgstr "运行参数" + +#: applications/serializers/attrs/application_type/custom.py:17 +msgid "Target url" +msgstr "目标URL" + +#: applications/serializers/attrs/application_type/mysql_workbench.py:18 +#: assets/models/asset.py:190 assets/models/domain.py:52 +#: assets/serializers/asset_user.py:46 settings/serializers/settings.py:52 #: users/templates/users/_granted_assets.html:26 #: users/templates/users/user_asset_permission.html:156 msgid "IP" msgstr "IP" -#: applications/serializers/remote_app.py:83 -msgid "Operating parameter" -msgstr "运行参数" - -#: applications/serializers/remote_app.py:84 -msgid "Target url" -msgstr "目标URL" - -#: applications/serializers/remote_app.py:161 -msgid "Asset info" -msgstr "资产信息" - #: assets/api/admin_user.py:46 msgid "Deleted failed, There are related assets" msgstr "删除失败,存在关联资产" @@ -326,7 +247,7 @@ msgstr "基础" msgid "Charset" msgstr "编码" -#: assets/models/asset.py:152 tickets/models/ticket.py:35 +#: assets/models/asset.py:152 tickets/models/ticket.py:40 msgid "Meta" msgstr "元数据" @@ -341,15 +262,14 @@ msgstr "系统平台" #: assets/models/asset.py:191 assets/serializers/asset_user.py:45 #: assets/serializers/gathered_user.py:20 settings/serializers/settings.py:51 -#: tickets/api/request_asset_perm.py:67 -#: tickets/serializers/request_asset_perm.py:23 #: users/templates/users/_granted_assets.html:25 #: users/templates/users/user_asset_permission.html:157 msgid "Hostname" msgstr "主机名" -#: assets/models/asset.py:194 assets/models/domain.py:53 -#: assets/models/user.py:103 terminal/serializers/session.py:29 +#: assets/models/asset.py:194 assets/models/domain.py:54 +#: assets/models/user.py:120 terminal/serializers/session.py:29 +#: terminal/serializers/storage.py:59 msgid "Protocol" msgstr "协议" @@ -358,22 +278,22 @@ msgstr "协议" msgid "Protocols" msgstr "协议组" -#: assets/models/asset.py:199 assets/models/user.py:98 -#: perms/models/asset_permission.py:95 +#: assets/models/asset.py:199 assets/models/user.py:115 +#: perms/models/asset_permission.py:97 #: xpack/plugins/change_auth_plan/models.py:56 #: xpack/plugins/gathered_user/models.py:24 msgid "Nodes" msgstr "节点" #: assets/models/asset.py:200 assets/models/cmd_filter.py:22 -#: assets/models/domain.py:56 assets/models/label.py:22 +#: assets/models/domain.py:57 assets/models/label.py:22 #: authentication/models.py:46 msgid "Is active" msgstr "激活" #: assets/models/asset.py:203 assets/models/cluster.py:19 #: assets/models/user.py:66 templates/_nav.html:44 -#: xpack/plugins/cloud/models.py:143 xpack/plugins/cloud/serializers.py:114 +#: xpack/plugins/cloud/models.py:143 xpack/plugins/cloud/serializers.py:126 msgid "Admin user" msgstr "管理用户" @@ -445,6 +365,30 @@ msgstr "主机名原始" msgid "Labels" msgstr "标签管理" +#: assets/models/asset.py:228 assets/models/base.py:242 +#: assets/models/cluster.py:28 assets/models/cmd_filter.py:26 +#: assets/models/cmd_filter.py:60 assets/models/group.py:21 +#: common/db/models.py:67 common/mixins/models.py:49 orgs/models.py:24 +#: orgs/models.py:427 perms/models/base.py:54 users/models/user.py:559 +#: users/serializers/group.py:35 users/templates/users/user_detail.html:97 +#: xpack/plugins/change_auth_plan/models.py:81 xpack/plugins/cloud/models.py:58 +#: xpack/plugins/cloud/models.py:156 xpack/plugins/gathered_user/models.py:30 +msgid "Created by" +msgstr "创建者" + +# msgid "Created by" +# msgstr "创建者" +#: assets/models/asset.py:229 assets/models/base.py:240 +#: assets/models/cluster.py:26 assets/models/domain.py:24 +#: assets/models/gathered_user.py:19 assets/models/group.py:22 +#: assets/models/label.py:25 common/db/models.py:69 common/mixins/models.py:50 +#: ops/models/adhoc.py:38 ops/models/command.py:27 orgs/models.py:25 +#: orgs/models.py:425 perms/models/base.py:55 users/models/group.py:18 +#: users/templates/users/user_group_detail.html:58 +#: xpack/plugins/cloud/models.py:61 xpack/plugins/cloud/models.py:159 +msgid "Date created" +msgstr "创建日期" + #: assets/models/authbook.py:17 msgid "Bulk delete deny" msgstr "拒绝批量删除" @@ -475,7 +419,7 @@ msgstr "SSH公钥" #: assets/models/base.py:241 assets/models/gathered_user.py:20 #: common/db/models.py:70 common/mixins/models.py:51 ops/models/adhoc.py:39 -#: orgs/models.py:399 +#: orgs/models.py:426 msgid "Date updated" msgstr "更新日期" @@ -487,7 +431,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:514 +#: assets/models/cluster.py:22 users/models/user.py:537 #: users/templates/users/user_detail.html:62 msgid "Phone" msgstr "手机" @@ -513,7 +457,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:655 +#: users/models/user.py:678 msgid "System" msgstr "系统" @@ -521,7 +465,7 @@ msgstr "系统" msgid "Default Cluster" msgstr "默认Cluster" -#: assets/models/cmd_filter.py:33 assets/models/user.py:108 +#: assets/models/cmd_filter.py:33 assets/models/user.py:125 msgid "Command filter" msgstr "命令过滤器" @@ -546,7 +490,7 @@ msgstr "允许" msgid "Filter" msgstr "过滤器" -#: assets/models/cmd_filter.py:53 assets/models/user.py:102 +#: assets/models/cmd_filter.py:53 assets/models/user.py:119 msgid "Priority" msgstr "优先级" @@ -564,9 +508,7 @@ msgstr "每行一个命令" #: assets/models/cmd_filter.py:56 audits/models.py:57 #: authentication/templates/authentication/_access_key_modal.html:34 -#: perms/forms/asset_permission.py:20 -#: tickets/serializers/request_asset_perm.py:63 -#: tickets/serializers/ticket.py:30 +#: tickets/models/ticket.py:43 tickets/serializers/ticket/ticket.py:20 #: users/templates/users/_granted_assets.html:29 #: users/templates/users/user_asset_permission.html:44 #: users/templates/users/user_asset_permission.html:79 @@ -581,11 +523,11 @@ msgstr "动作" msgid "Command filter rule" msgstr "命令过滤规则" -#: assets/models/domain.py:63 +#: assets/models/domain.py:64 msgid "Gateway" msgstr "网关" -#: assets/models/domain.py:69 +#: assets/models/domain.py:70 msgid "Password should not contains special characters" msgstr "密码不能包含特殊字符" @@ -615,16 +557,12 @@ msgstr "默认资产组" #: assets/models/label.py:15 audits/models.py:36 audits/models.py:56 #: audits/models.py:69 audits/serializers.py:81 authentication/models.py:44 -#: authentication/models.py:88 orgs/models.py:18 orgs/models.py:396 -#: perms/forms/asset_permission.py:83 perms/forms/database_app_permission.py:38 -#: perms/forms/remote_app_permission.py:40 perms/models/asset_permission.py:171 -#: perms/models/base.py:49 templates/index.html:78 -#: terminal/backends/command/models.py:18 +#: authentication/models.py:96 orgs/models.py:18 orgs/models.py:423 +#: perms/models/asset_permission.py:173 perms/models/base.py:49 +#: templates/index.html:78 terminal/backends/command/models.py:18 #: terminal/backends/command/serializers.py:12 terminal/models/session.py:37 -#: tickets/models/ticket.py:30 tickets/models/ticket.py:136 -#: tickets/serializers/request_asset_perm.py:64 -#: tickets/serializers/ticket.py:31 users/forms/group.py:15 -#: users/models/user.py:159 users/models/user.py:643 +#: tickets/models/comment.py:17 users/forms/group.py:15 +#: users/models/user.py:159 users/models/user.py:666 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -638,109 +576,102 @@ msgstr "默认资产组" msgid "User" msgstr "用户" -#: assets/models/label.py:19 assets/models/node.py:401 settings/models.py:28 +#: assets/models/label.py:19 assets/models/node.py:413 settings/models.py:28 msgid "Value" msgstr "值" -#: assets/models/node.py:131 +#: assets/models/node.py:143 msgid "New node" msgstr "新节点" -#: assets/models/node.py:304 users/templates/users/_granted_assets.html:130 +#: assets/models/node.py:316 users/templates/users/_granted_assets.html:130 msgid "empty" msgstr "空" -#: assets/models/node.py:400 perms/models/asset_permission.py:146 +#: assets/models/node.py:412 perms/models/asset_permission.py:148 msgid "Key" msgstr "键" -#: assets/models/node.py:402 +#: assets/models/node.py:414 msgid "Full value" msgstr "全称" -#: assets/models/node.py:405 perms/models/asset_permission.py:150 +#: assets/models/node.py:417 perms/models/asset_permission.py:152 msgid "Parent key" msgstr "ssh私钥" -#: assets/models/node.py:414 assets/serializers/system_user.py:190 -#: perms/forms/asset_permission.py:92 perms/forms/asset_permission.py:99 +#: assets/models/node.py:426 assets/serializers/system_user.py:190 #: users/templates/users/user_asset_permission.html:41 #: users/templates/users/user_asset_permission.html:73 #: users/templates/users/user_asset_permission.html:158 -#: xpack/plugins/cloud/models.py:139 xpack/plugins/cloud/serializers.py:115 +#: xpack/plugins/cloud/models.py:139 xpack/plugins/cloud/serializers.py:127 msgid "Node" msgstr "节点" -#: assets/models/user.py:94 +#: assets/models/user.py:111 msgid "Automatic login" msgstr "自动登录" -#: assets/models/user.py:95 +#: assets/models/user.py:112 msgid "Manually login" msgstr "手动登录" -#: assets/models/user.py:97 +#: assets/models/user.py:114 msgid "Username same with user" msgstr "用户名与用户相同" -#: assets/models/user.py:99 assets/serializers/domain.py:30 +#: assets/models/user.py:116 assets/serializers/domain.py:30 #: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:52 msgid "Assets" msgstr "资产" -#: assets/models/user.py:100 templates/_nav.html:17 +#: assets/models/user.py:117 templates/_nav.html:17 #: users/views/profile/password.py:42 users/views/profile/pubkey.py:36 msgid "Users" msgstr "用户管理" -#: assets/models/user.py:101 users/templates/users/user_group_list.html:90 +#: assets/models/user.py:118 users/templates/users/user_group_list.html:90 #: users/templates/users/user_profile.html:124 msgid "User groups" msgstr "用户组" -#: assets/models/user.py:104 +#: assets/models/user.py:121 msgid "Auto push" msgstr "自动推送" -#: assets/models/user.py:105 +#: assets/models/user.py:122 msgid "Sudo" msgstr "Sudo" -#: assets/models/user.py:106 +#: assets/models/user.py:123 msgid "Shell" msgstr "Shell" -#: assets/models/user.py:107 +#: assets/models/user.py:124 msgid "Login mode" msgstr "登录模式" -#: assets/models/user.py:109 +#: assets/models/user.py:126 msgid "SFTP Root" msgstr "SFTP根路径" -#: assets/models/user.py:110 authentication/models.py:86 +#: assets/models/user.py:127 authentication/models.py:94 msgid "Token" msgstr "" -#: assets/models/user.py:111 +#: assets/models/user.py:128 msgid "Home" msgstr "家目录" -#: assets/models/user.py:112 +#: assets/models/user.py:129 msgid "System groups" msgstr "用户组" -#: assets/models/user.py:211 audits/models.py:39 -#: perms/forms/asset_permission.py:95 perms/forms/remote_app_permission.py:49 -#: perms/models/application_permission.py:22 -#: perms/models/asset_permission.py:96 -#: perms/models/database_app_permission.py:22 -#: perms/models/k8s_app_permission.py:22 -#: perms/models/remote_app_permission.py:16 templates/_nav.html:45 +#: assets/models/user.py:221 audits/models.py:39 +#: perms/models/application_permission.py:31 +#: perms/models/asset_permission.py:98 templates/_nav.html:45 #: terminal/backends/command/models.py:20 #: terminal/backends/command/serializers.py:14 terminal/models/session.py:41 -#: tickets/api/request_asset_perm.py:68 -#: tickets/serializers/request_asset_perm.py:25 #: users/templates/users/_granted_assets.html:27 #: users/templates/users/user_asset_permission.html:42 #: users/templates/users/user_asset_permission.html:76 @@ -812,14 +743,14 @@ msgid "Backend" msgstr "后端" #: assets/serializers/asset_user.py:75 users/forms/profile.py:148 -#: users/models/user.py:525 users/templates/users/user_password_update.html:48 +#: users/models/user.py:548 users/templates/users/user_password_update.html:48 #: users/templates/users/user_profile.html:69 #: users/templates/users/user_profile_update.html:46 #: users/templates/users/user_pubkey_update.html:46 msgid "Public key" msgstr "SSH公钥" -#: assets/serializers/asset_user.py:79 users/models/user.py:522 +#: assets/serializers/asset_user.py:79 users/models/user.py:545 msgid "Private key" msgstr "ssh私钥" @@ -903,11 +834,11 @@ msgstr "测试管理帐号可连接性: {}" msgid "Test assets connectivity" msgstr "测试资产可连接性" -#: assets/tasks/asset_connectivity.py:76 +#: assets/tasks/asset_connectivity.py:76 assets/tasks/asset_connectivity.py:87 msgid "Test assets connectivity: {}" msgstr "测试资产可连接性: {}" -#: assets/tasks/asset_connectivity.py:87 +#: assets/tasks/asset_connectivity.py:98 msgid "Test if the assets under the node are connectable: {}" msgstr "测试节点下资产是否可连接: {}" @@ -919,19 +850,23 @@ msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务" msgid "Test asset user connectivity: {}" msgstr "测试资产用户可连接性: {}" -#: assets/tasks/gather_asset_hardware_info.py:45 +#: assets/tasks/gather_asset_hardware_info.py:46 msgid "Get asset info failed: {}" msgstr "获取资产信息失败:{}" -#: assets/tasks/gather_asset_hardware_info.py:96 +#: assets/tasks/gather_asset_hardware_info.py:97 msgid "Update some assets hardware info" msgstr "更新资产硬件信息" -#: assets/tasks/gather_asset_hardware_info.py:113 +#: assets/tasks/gather_asset_hardware_info.py:114 msgid "Update asset hardware info: {}" msgstr "更新资产硬件信息: {}" -#: assets/tasks/gather_asset_hardware_info.py:130 +#: assets/tasks/gather_asset_hardware_info.py:120 +msgid "Update assets hardware info: {}" +msgstr "更新资产硬件信息: {}" + +#: assets/tasks/gather_asset_hardware_info.py:137 msgid "Update node asset hardware information: {}" msgstr "更新节点资产硬件信息: {}" @@ -1060,7 +995,11 @@ msgid "Success" msgstr "成功" #: audits/models.py:43 ops/models/command.py:28 perms/models/base.py:52 -#: terminal/models/session.py:51 tickets/serializers/request_asset_perm.py:27 +#: terminal/models/session.py:51 +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:41 +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:69 +#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:38 +#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:73 #: xpack/plugins/change_auth_plan/models.py:177 #: xpack/plugins/change_auth_plan/models.py:307 #: xpack/plugins/gathered_user/models.py:76 @@ -1126,10 +1065,12 @@ msgid "Login type" msgstr "登录方式" #: audits/models.py:101 +#: tickets/serializers/ticket/meta/ticket_type/login_confirm.py:14 msgid "Login ip" msgstr "登录IP" #: audits/models.py:102 +#: tickets/serializers/ticket/meta/ticket_type/login_confirm.py:17 msgid "Login city" msgstr "登录城市" @@ -1140,7 +1081,7 @@ msgstr "用户代理" #: audits/models.py:104 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/login_otp.html:6 -#: users/forms/profile.py:52 users/models/user.py:517 +#: users/forms/profile.py:52 users/models/user.py:540 #: users/serializers/user.py:232 users/templates/users/user_detail.html:77 #: users/templates/users/user_profile.html:87 msgid "MFA" @@ -1151,8 +1092,8 @@ msgstr "多因子认证" msgid "Reason" msgstr "原因" -#: audits/models.py:106 tickets/serializers/request_asset_perm.py:62 -#: tickets/serializers/ticket.py:29 xpack/plugins/cloud/models.py:224 +#: audits/models.py:106 tickets/models/ticket.py:47 +#: tickets/serializers/ticket/ticket.py:21 xpack/plugins/cloud/models.py:224 #: xpack/plugins/cloud/models.py:282 msgid "Status" msgstr "状态" @@ -1169,6 +1110,10 @@ msgstr "认证方式" msgid "Operate for display" msgstr "操作(显示名称)" +#: audits/serializers.py:26 +msgid "Type for display" +msgstr "类型(显示名称)" + #: audits/serializers.py:27 msgid "Status for display" msgstr "状态(显示名称)" @@ -1177,7 +1122,7 @@ msgstr "状态(显示名称)" msgid "MFA for display" msgstr "多因子认证状态(显示名称)" -#: audits/serializers.py:66 audits/serializers.py:78 ops/models/adhoc.py:246 +#: audits/serializers.py:66 audits/serializers.py:78 ops/models/adhoc.py:247 #: terminal/serializers/session.py:34 msgid "Is success" msgstr "是否成功" @@ -1187,7 +1132,7 @@ msgstr "是否成功" msgid "Result" msgstr "结果" -#: audits/serializers.py:79 +#: audits/serializers.py:79 terminal/serializers/storage.py:157 msgid "Hosts" msgstr "主机" @@ -1347,17 +1292,16 @@ msgstr "登录复核 {}" msgid "SSO auth closed" msgstr "SSO 认证关闭了" -#: authentication/errors.py:218 authentication/views/login.py:247 +#: authentication/errors.py:218 authentication/views/login.py:218 msgid "Your password is too simple, please change it for security" msgstr "你的密码过于简单,为了安全,请修改" -#: authentication/errors.py:227 authentication/views/login.py:262 +#: authentication/errors.py:227 authentication/views/login.py:233 msgid "Your password has expired, please reset before logging in" msgstr "您的密码已过期,先修改再登录" #: authentication/forms.py:26 authentication/forms.py:38 -#: authentication/templates/authentication/login.html:39 -#: authentication/templates/authentication/xpack_login.html:119 +#: authentication/templates/authentication/login.html:119 #: users/forms/user.py:199 msgid "MFA code" msgstr "多因子认证验证码" @@ -1378,16 +1322,12 @@ msgstr "SSH密钥" msgid "Reviewers" msgstr "审批人" -#: authentication/models.py:54 tickets/models/ticket.py:23 +#: authentication/models.py:71 tickets/const.py:9 #: users/templates/users/user_detail.html:250 msgid "Login confirm" msgstr "登录复核" -#: authentication/models.py:64 -msgid "City" -msgstr "城市" - -#: authentication/models.py:87 +#: authentication/models.py:95 msgid "Expired" msgstr "过期时间" @@ -1417,7 +1357,7 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:421 users/serializers/user.py:229 +#: users/models/user.py:444 users/serializers/user.py:229 #: users/templates/users/user_profile.html:94 #: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:166 @@ -1426,7 +1366,7 @@ msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:422 users/serializers/user.py:230 +#: users/models/user.py:445 users/serializers/user.py:230 #: users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:170 msgid "Enable" @@ -1438,7 +1378,7 @@ msgstr "删除成功" #: authentication/templates/authentication/_access_key_modal.html:155 #: authentication/templates/authentication/_mfa_confirm_modal.html:53 -#: templates/_modal.html:22 tickets/models/ticket.py:68 +#: templates/_modal.html:22 tickets/const.py:18 msgid "Close" msgstr "关闭" @@ -1475,40 +1415,37 @@ msgstr "确认" msgid "Code error" msgstr "代码错误" -#: authentication/templates/authentication/login.html:6 -#: authentication/templates/authentication/login.html:50 -#: authentication/templates/authentication/xpack_login.html:131 -#: templates/_base_only_msg_content.html:51 templates/_header_bar.html:83 -msgid "Login" -msgstr "登录" +#: authentication/templates/authentication/login.html:78 +msgid "Welcome back, please enter username and password to login" +msgstr "欢迎回来,请输入用户名和密码登录" -#: authentication/templates/authentication/login.html:17 -#: authentication/templates/authentication/xpack_login.html:95 +#: authentication/templates/authentication/login.html:95 msgid "Captcha invalid" msgstr "验证码错误" -#: authentication/templates/authentication/login.html:61 -#: authentication/templates/authentication/xpack_login.html:135 +#: authentication/templates/authentication/login.html:135 +#: templates/_header_bar.html:83 +msgid "Login" +msgstr "登录" + +#: authentication/templates/authentication/login.html:142 +msgid "More login options" +msgstr "更多登录方式" + +#: authentication/templates/authentication/login.html:145 +msgid "OpenID" +msgstr "OpenID" + +#: authentication/templates/authentication/login.html:150 +msgid "CAS" +msgstr "" + +#: authentication/templates/authentication/login.html:159 #: users/templates/users/forgot_password.html:7 #: users/templates/users/forgot_password.html:8 msgid "Forgot password" msgstr "忘记密码" -#: authentication/templates/authentication/login.html:68 -msgid "More login options" -msgstr "更多登录方式" - -#: authentication/templates/authentication/login.html:72 -msgid "OpenID" -msgstr "OpenID" - -#: authentication/templates/authentication/login.html:102 -#: authentication/templates/authentication/xpack_login.html:174 -msgid "" -"You are using another authentication server, please contact your " -"administrator" -msgstr "正在使用其他认证服务器,请联系管理员" - #: authentication/templates/authentication/login_otp.html:17 msgid "One-time password" msgstr "一次性密码" @@ -1547,15 +1484,11 @@ msgstr "返回" msgid "Copy success" msgstr "复制成功" -#: authentication/templates/authentication/xpack_login.html:78 -msgid "Welcome back, please enter username and password to login" -msgstr "欢迎回来,请输入用户名和密码登录" - -#: authentication/views/login.py:86 +#: authentication/views/login.py:56 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:193 +#: authentication/views/login.py:164 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -1563,19 +1496,19 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\n" " 不要关闭本页面" -#: authentication/views/login.py:198 +#: authentication/views/login.py:169 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:230 +#: authentication/views/login.py:201 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:231 +#: authentication/views/login.py:202 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/login.py:246 authentication/views/login.py:261 +#: authentication/views/login.py:217 authentication/views/login.py:232 msgid "Please change your password" msgstr "请修改密码" @@ -1593,7 +1526,7 @@ msgstr "%(name)s 更新成功" msgid "Updated by" msgstr "更新人" -#: common/drf/parsers/base.py:16 +#: common/drf/parsers/base.py:17 msgid "The file content overflowed (The maximum length `{}` bytes)" msgstr "文件内容益处 (最大长度 `{}` 字节)" @@ -1618,14 +1551,6 @@ msgstr "" msgid "Is referenced by other objects and cannot be deleted" msgstr "被其他对象关联,不能删除" -#: common/fields/form.py:33 -msgid "Not a valid json" -msgstr "不是合法json" - -#: common/fields/form.py:35 -msgid "Not a string type" -msgstr "不是字符类型" - #: common/fields/model.py:80 msgid "Marshal dict data to char field" msgstr "" @@ -1678,8 +1603,8 @@ msgstr "字段必须唯一" msgid "Should not contains special characters" msgstr "不能包含特殊字符" -#: jumpserver/conf.py:477 templates/_base_only_msg_content.html:27 -#: xpack/plugins/interface/api.py:18 xpack/plugins/interface/models.py:36 +#: jumpserver/conf.py:474 xpack/plugins/interface/api.py:18 +#: xpack/plugins/interface/models.py:36 msgid "Welcome to the JumpServer open source fortress" msgstr "欢迎使用JumpServer开源堡垒机" @@ -1710,7 +1635,7 @@ msgstr "" "div>
如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运" -#: ops/api/celery.py:57 +#: ops/api/celery.py:61 ops/api/celery.py:76 msgid "Waiting task start" msgstr "等待任务开始" @@ -1794,42 +1719,42 @@ msgstr "创建者" msgid "Task display" msgstr "任务展示" -#: ops/models/adhoc.py:240 +#: ops/models/adhoc.py:241 msgid "Host amount" msgstr "主机数量" -#: ops/models/adhoc.py:242 +#: ops/models/adhoc.py:243 msgid "Start time" msgstr "开始时间" -#: ops/models/adhoc.py:243 +#: ops/models/adhoc.py:244 msgid "End time" msgstr "完成时间" -#: ops/models/adhoc.py:244 xpack/plugins/change_auth_plan/models.py:180 +#: ops/models/adhoc.py:245 xpack/plugins/change_auth_plan/models.py:180 #: xpack/plugins/change_auth_plan/models.py:310 #: xpack/plugins/gathered_user/models.py:79 msgid "Time" msgstr "时间" -#: ops/models/adhoc.py:245 ops/models/command.py:26 +#: ops/models/adhoc.py:246 ops/models/command.py:26 #: terminal/serializers/session.py:38 msgid "Is finished" msgstr "是否完成" -#: ops/models/adhoc.py:247 +#: ops/models/adhoc.py:248 msgid "Adhoc raw result" msgstr "结果" -#: ops/models/adhoc.py:248 +#: ops/models/adhoc.py:249 msgid "Adhoc result summary" msgstr "汇总" -#: ops/models/adhoc.py:288 xpack/plugins/change_auth_plan/utils.py:137 +#: ops/models/adhoc.py:290 xpack/plugins/change_auth_plan/utils.py:137 msgid "{} Start task: {}" msgstr "{} 任务开始: {}" -#: ops/models/adhoc.py:297 xpack/plugins/change_auth_plan/utils.py:149 +#: ops/models/adhoc.py:299 xpack/plugins/change_auth_plan/utils.py:149 msgid "{} Task finish" msgstr "{} 任务结束" @@ -1861,11 +1786,11 @@ msgstr "定期清除Celery日志" msgid "Task log" msgstr "任务列表" -#: ops/utils.py:60 +#: ops/utils.py:62 msgid "Update task content: {}" msgstr "更新任务内容: {}" -#: ops/utils.py:70 +#: ops/utils.py:72 msgid "Disk used more than 80%: {} => {}" msgstr "磁盘使用率超过 80%: {} => {}" @@ -1878,7 +1803,8 @@ msgid "The current organization cannot be deleted" msgstr "当前组织不能被删除" #: orgs/mixins/models.py:56 orgs/mixins/serializers.py:25 orgs/models.py:41 -#: orgs/models.py:395 orgs/serializers.py:79 +#: orgs/models.py:422 orgs/serializers.py:100 +#: tickets/serializers/ticket/ticket.py:74 msgid "Organization" msgstr "组织" @@ -1890,7 +1816,7 @@ msgstr "组织管理员" msgid "Organization auditor" msgstr "组织审计员" -#: orgs/models.py:397 users/forms/user.py:27 users/models/user.py:505 +#: orgs/models.py:424 users/forms/user.py:27 users/models/user.py:528 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:73 #: users/templates/users/user_list.html:16 @@ -1914,16 +1840,58 @@ msgstr "管理员正在修改授权,请稍等" msgid "The authorization cannot be revoked for the time being" msgstr "该授权暂时不能撤销" -#: perms/forms/asset_permission.py:23 -msgid "" -"Tips: The RDP protocol does not support separate controls for uploading or " -"downloading files" -msgstr "提示:RDP 协议不支持单独控制上传或下载文件" +#: perms/models/application_permission.py:27 users/models/user.py:160 +msgid "Application" +msgstr "应用程序" -#: perms/forms/asset_permission.py:86 perms/forms/database_app_permission.py:41 -#: perms/forms/remote_app_permission.py:43 perms/models/base.py:50 -#: templates/_nav.html:21 users/forms/user.py:168 users/models/group.py:31 -#: users/models/user.py:501 users/templates/users/_select_user_modal.html:16 +#: perms/models/application_permission.py:36 +msgid "Application permission" +msgstr "应用管理" + +#: perms/models/asset_permission.py:34 settings/serializers/settings.py:56 +msgid "All" +msgstr "全部" + +#: perms/models/asset_permission.py:35 +msgid "Connect" +msgstr "连接" + +#: perms/models/asset_permission.py:36 +msgid "Upload file" +msgstr "上传文件" + +#: perms/models/asset_permission.py:37 +msgid "Download file" +msgstr "下载文件" + +#: perms/models/asset_permission.py:38 +msgid "Upload download" +msgstr "上传下载" + +#: perms/models/asset_permission.py:39 +msgid "Clipboard copy" +msgstr "剪贴板复制" + +#: perms/models/asset_permission.py:40 +msgid "Clipboard paste" +msgstr "剪贴板粘贴" + +#: perms/models/asset_permission.py:41 +msgid "Clipboard copy paste" +msgstr "剪贴板复制粘贴" + +#: perms/models/asset_permission.py:99 perms/serializers/asset/permission.py:60 +msgid "Actions" +msgstr "动作" + +#: perms/models/asset_permission.py:103 templates/_nav.html:78 +#: users/templates/users/_user_detail_nav_header.html:31 +msgid "Asset permission" +msgstr "资产授权" + +#: perms/models/base.py:50 templates/_nav.html:21 users/forms/user.py:168 +#: users/models/group.py:31 users/models/user.py:524 +#: users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_asset_permission.html:39 #: users/templates/users/user_asset_permission.html:67 #: users/templates/users/user_database_app_permission.html:38 @@ -1935,87 +1903,16 @@ msgstr "提示:RDP 协议不支持单独控制上传或下载文件" msgid "User group" msgstr "用户组" -#: perms/forms/asset_permission.py:108 -msgid "User or group at least one required" -msgstr "用户和用户组至少选一个" - -#: perms/forms/asset_permission.py:117 -msgid "Asset or group at least one required" -msgstr "资产和节点至少选一个" - -#: perms/forms/database_app_permission.py:47 -msgid "System users" -msgstr "系统用户" - -#: perms/models/application_permission.py:21 users/models/user.py:160 -msgid "Application" -msgstr "应用程序" - -#: perms/models/application_permission.py:26 -msgid "Application permission" -msgstr "应用管理" - -#: perms/models/asset_permission.py:37 settings/serializers/settings.py:56 -msgid "All" -msgstr "全部" - -#: perms/models/asset_permission.py:38 -msgid "Connect" -msgstr "连接" - -#: perms/models/asset_permission.py:39 -msgid "Upload file" -msgstr "上传文件" - -#: perms/models/asset_permission.py:40 -msgid "Download file" -msgstr "下载文件" - -#: perms/models/asset_permission.py:41 -msgid "Upload download" -msgstr "上传下载" - -#: perms/models/asset_permission.py:42 -msgid "Clipboard copy" -msgstr "剪贴板复制" - -#: perms/models/asset_permission.py:43 -msgid "Clipboard paste" -msgstr "剪贴板粘贴" - -#: perms/models/asset_permission.py:44 -msgid "Clipboard copy paste" -msgstr "剪贴板复制粘贴" - -#: perms/models/asset_permission.py:97 perms/serializers/asset/permission.py:60 -msgid "Actions" -msgstr "动作" - -#: perms/models/asset_permission.py:101 templates/_nav.html:78 -#: users/templates/users/_user_detail_nav_header.html:31 -msgid "Asset permission" -msgstr "资产授权" - -#: perms/models/base.py:53 tickets/serializers/request_asset_perm.py:29 -#: users/models/user.py:533 users/templates/users/user_detail.html:93 +#: perms/models/base.py:53 +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:44 +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:72 +#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:41 +#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:76 +#: users/models/user.py:556 users/templates/users/user_detail.html:93 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" -#: perms/models/database_app_permission.py:27 -#: users/templates/users/_user_detail_nav_header.html:61 -msgid "DatabaseApp permission" -msgstr "数据库应用授权" - -#: perms/models/k8s_app_permission.py:27 -msgid "KubernetesApp permission" -msgstr "Kubernetes应用授权" - -#: perms/models/remote_app_permission.py:20 -#: users/templates/users/_user_detail_nav_header.html:47 -msgid "RemoteApp permission" -msgstr "远程应用授权" - #: perms/serializers/application/permission.py:53 msgid "" "The application list contains applications that are different from the " @@ -2026,56 +1923,22 @@ msgstr "应用列表中包含与授权类型不同的应用。({})" msgid "Is expired" msgstr "是否过期" -#: perms/serializers/asset/permission.py:59 -#: perms/serializers/database_app_permission.py:43 -#: perms/serializers/database_app_permission.py:62 -#: perms/serializers/k8s_app_permission.py:41 -#: perms/serializers/k8s_app_permission.py:60 -#: perms/serializers/remote_app_permission.py:36 users/serializers/user.py:79 +#: perms/serializers/asset/permission.py:59 users/serializers/user.py:79 msgid "Is valid" msgstr "账户是否有效" -#: perms/serializers/asset/permission.py:61 -#: perms/serializers/database_app_permission.py:44 -#: perms/serializers/database_app_permission.py:63 -#: perms/serializers/k8s_app_permission.py:42 -#: perms/serializers/k8s_app_permission.py:61 -#: perms/serializers/remote_app_permission.py:37 users/serializers/group.py:36 +#: perms/serializers/asset/permission.py:61 users/serializers/group.py:36 msgid "Users amount" msgstr "用户数量" #: perms/serializers/asset/permission.py:62 -#: perms/serializers/database_app_permission.py:45 -#: perms/serializers/database_app_permission.py:64 -#: perms/serializers/k8s_app_permission.py:43 -#: perms/serializers/k8s_app_permission.py:62 -#: perms/serializers/remote_app_permission.py:38 msgid "User groups amount" msgstr "用户组数量" #: perms/serializers/asset/permission.py:65 -#: perms/serializers/database_app_permission.py:46 -#: perms/serializers/database_app_permission.py:65 -#: perms/serializers/k8s_app_permission.py:44 -#: perms/serializers/k8s_app_permission.py:63 -#: perms/serializers/remote_app_permission.py:39 msgid "System users amount" msgstr "系统用户数量" -#: perms/serializers/database_app_permission.py:47 -#: perms/serializers/database_app_permission.py:66 -#: perms/serializers/k8s_app_permission.py:45 -msgid "Database apps amount" -msgstr "数据库应用数量" - -#: perms/serializers/k8s_app_permission.py:64 -msgid "K8s apps amount" -msgstr "K8S 应用数量" - -#: perms/serializers/remote_app_permission.py:40 -msgid "Remote apps amount" -msgstr "远程应用数量" - #: perms/utils/asset/user_permission.py:30 msgid "Favorite" msgstr "收藏夹" @@ -2206,38 +2069,6 @@ msgstr "认证成功: {}" msgid "Search" msgstr "搜索" -#: templates/_base_only_msg_content.html:32 -msgid "" -"The world's first fully open source fortress, using the GNU GPL v2.0 open " -"source protocol, is a professional operation and maintenance audit system in " -"compliance with 4A." -msgstr "" -"全球首款完全开源的堡垒机,使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计" -"系统。" - -#: templates/_base_only_msg_content.html:35 -msgid "" -"Developed using Python/Django, following the Web 2.0 specification and " -"equipped with industry-leading Web Terminal solutions, with beautiful " -"interactive interface and good user experience." -msgstr "" -"使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web " -"Terminal 解决方案,交互界面美观、用户体验好。" - -#: templates/_base_only_msg_content.html:38 -msgid "" -"Distributed architecture is adopted to support multi-machine room deployment " -"across regions, central node provides API, and each machine room deploys " -"login node, which can be extended horizontally and without concurrent access " -"restrictions." -msgstr "" -"采纳分布式架构,支持多机房跨区域部署,中心节点提供 API,各机房部署登录节点," -"可横向扩展、无并发访问限制。" - -#: templates/_base_only_msg_content.html:41 -msgid "Changes the world, starting with a little bit." -msgstr "改变世界,从一点点开始。" - #: templates/_csv_import_export.html:8 msgid "Export" msgstr "导出" @@ -2410,6 +2241,18 @@ msgstr "平台列表" msgid "Applications" msgstr "应用管理" +#: templates/_nav.html:64 templates/_nav.html:82 templates/_nav_user.html:16 +#: users/templates/users/user_remote_app_permission.html:39 +#: users/templates/users/user_remote_app_permission.html:64 +msgid "RemoteApp" +msgstr "远程应用" + +#: templates/_nav.html:66 templates/_nav.html:86 templates/_nav_user.html:22 +#: users/templates/users/user_database_app_permission.html:39 +#: users/templates/users/user_database_app_permission.html:64 +msgid "DatabaseApp" +msgstr "数据库应用" + #: templates/_nav.html:75 msgid "Perms" msgstr "权限管理" @@ -2751,15 +2594,15 @@ msgstr "风险等级(显示名称)" msgid "Timestamp" msgstr "时间戳" -#: terminal/const.py:117 +#: terminal/const.py:31 msgid "Critical" msgstr "严重" -#: terminal/const.py:118 +#: terminal/const.py:32 msgid "High" msgstr "较高" -#: terminal/const.py:119 users/templates/users/reset_password.html:50 +#: terminal/const.py:33 users/templates/users/reset_password.html:50 #: users/templates/users/user_create.html:35 #: users/templates/users/user_password_update.html:104 #: users/templates/users/user_update.html:57 @@ -2874,6 +2717,63 @@ msgstr "是否可重放" msgid "Can join" msgstr "是否可加入" +#: terminal/serializers/storage.py:19 +msgid "Bucket" +msgstr "桶名称" + +#: terminal/serializers/storage.py:22 +msgid "Access key" +msgstr "" + +#: terminal/serializers/storage.py:26 +msgid "Secret key" +msgstr "" + +#: terminal/serializers/storage.py:30 terminal/serializers/storage.py:41 +#: terminal/serializers/storage.py:70 +msgid "Endpoint" +msgstr "端点" + +#: terminal/serializers/storage.py:56 xpack/plugins/cloud/models.py:276 +msgid "Region" +msgstr "地域" + +#: terminal/serializers/storage.py:80 +msgid "Container name" +msgstr "容器名称" + +#: terminal/serializers/storage.py:82 +msgid "Account name" +msgstr "账户名称" + +#: terminal/serializers/storage.py:83 +msgid "Account key" +msgstr "账户密钥" + +#: terminal/serializers/storage.py:86 +msgid "Endpoint suffix" +msgstr "端点后缀" + +#: terminal/serializers/storage.py:135 +msgid "The address format is incorrect" +msgstr "地址格式不正确" + +#: terminal/serializers/storage.py:142 +msgid "Host invalid" +msgstr "主机无效" + +#: terminal/serializers/storage.py:145 +msgid "Port invalid" +msgstr "端口无效" + +#: terminal/serializers/storage.py:161 +msgid "Index" +msgstr "索引" + +#: terminal/serializers/storage.py:164 +msgid "Doc type" +msgstr "文档类型" + #: terminal/serializers/terminal.py:44 terminal/serializers/terminal.py:52 msgid "Not found" msgstr "没有发现" @@ -2914,224 +2814,219 @@ msgstr "" "
\n" " " -#: tickets/api/request_asset_perm.py:47 -#, python-format -msgid "Ticket has %s" -msgstr "工单已%s" - -#: tickets/api/request_asset_perm.py:66 -#: tickets/serializers/request_asset_perm.py:21 -msgid "IP group" -msgstr "IP组" - -#: tickets/api/request_asset_perm.py:69 -#: tickets/serializers/request_asset_perm.py:33 -msgid "Confirmed assets" -msgstr "确认的资产" - -#: tickets/api/request_asset_perm.py:70 -msgid "Confirmed system users" -msgstr "确认的系统用户" - -#: tickets/api/request_asset_perm.py:91 -msgid "Confirm assets first" -msgstr "请先确认资产" - -#: tickets/api/request_asset_perm.py:94 -msgid "Confirmed assets changed" -msgstr "确认的资产变更了" - -#: tickets/api/request_asset_perm.py:98 -msgid "Confirm system-users first" -msgstr "请先确认系统用户" - -#: tickets/api/request_asset_perm.py:102 -msgid "Confirmed system-users changed" -msgstr "确认的系统用户变更了" - -#: tickets/api/request_asset_perm.py:108 tickets/api/request_asset_perm.py:115 -#: xpack/plugins/cloud/models.py:215 -msgid "Succeed" -msgstr "成功" - -#: tickets/api/request_asset_perm.py:122 -msgid "From request ticket: {} {}" -msgstr "来自工单申请: {} {}" - -#: tickets/api/request_asset_perm.py:124 -msgid "{} request assets, approved by {}" -msgstr "{} 申请资产,通过人 {}" - -#: tickets/exceptions.py:23 -msgid "Ticket closed" -msgstr "工单已关闭" - -#: tickets/exceptions.py:32 -msgid "Only assignee can operate ticket" -msgstr "只有审批人可以操作工单" - -#: tickets/exceptions.py:37 -msgid "Ticket can not be operated" -msgstr "不能操作该工单" - -#: tickets/models/ticket.py:18 tickets/models/ticket.py:70 -msgid "Open" -msgstr "待处理" - -#: tickets/models/ticket.py:19 -msgid "Closed" -msgstr "已完成" - -#: tickets/models/ticket.py:22 +#: tickets/const.py:8 msgid "General" msgstr "一般" -#: tickets/models/ticket.py:24 -msgid "Request asset permission" -msgstr "申请资产权限" +#: tickets/const.py:10 +msgid "Apply for asset" +msgstr "申请资产" -#: tickets/models/ticket.py:27 +#: tickets/const.py:11 +msgid "Apply for application" +msgstr "申请应用" + +#: tickets/const.py:15 tickets/const.py:22 +msgid "Open" +msgstr "打开" + +#: tickets/const.py:16 msgid "Approve" msgstr "同意" -#: tickets/models/ticket.py:28 +#: tickets/const.py:17 msgid "Reject" msgstr "拒绝" -#: tickets/models/ticket.py:31 tickets/models/ticket.py:137 +#: tickets/const.py:23 +msgid "Closed" +msgstr "关闭" + +#: tickets/models/comment.py:19 msgid "User display name" msgstr "用户显示名称" -#: tickets/models/ticket.py:33 -msgid "Title" -msgstr "标题" - -#: tickets/models/ticket.py:34 tickets/models/ticket.py:138 +#: tickets/models/comment.py:20 msgid "Body" msgstr "内容" -#: tickets/models/ticket.py:36 -msgid "Assignee" +#: tickets/models/ticket.py:28 +msgid "ugettext_lazy" +msgstr "" + +#: tickets/models/ticket.py:35 +msgid "Title" +msgstr "标题" + +#: tickets/models/ticket.py:52 +msgid "Applicant" +msgstr "申请人" + +#: tickets/models/ticket.py:55 +msgid "Applicant display" +msgstr "申请人" + +#: tickets/models/ticket.py:60 +msgid "Processor" msgstr "处理人" -#: tickets/models/ticket.py:37 -msgid "Assignee display name" -msgstr "处理人名称" +#: tickets/models/ticket.py:63 +msgid "Processor display" +msgstr "处理人" -#: tickets/models/ticket.py:38 +#: tickets/models/ticket.py:67 msgid "Assignees" -msgstr "待处理人" +msgstr "受理人" -#: tickets/models/ticket.py:39 -msgid "Assignees display name" -msgstr "待处理人名称" +#: tickets/models/ticket.py:70 +msgid "Assignees display" +msgstr "受理人" -#: tickets/models/ticket.py:71 -msgid "{} {} this ticket" -msgstr "{} {} 这个工单" +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:22 +msgid "Category display" +msgstr "种类" -#: tickets/models/ticket.py:85 -msgid "this ticket" -msgstr "这个工单" +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:29 +msgid "Type display" +msgstr "类型" -#: tickets/serializers/request_asset_perm.py:37 -msgid "Confirmed system user" -msgstr "确认的系统用户" +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:33 +msgid "Application group" +msgstr "应用组" -#: tickets/serializers/request_asset_perm.py:86 -msgid "Invalid `org_id`" -msgstr "无效的 `org_id`" +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:37 +#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:26 +msgid "System user group" +msgstr "系统用户组" -#: tickets/serializers/request_asset_perm.py:95 -msgid "Field `assignees` must be organization admin or superuser" -msgstr "字段 assignees 必须是组织管理员或者超级管理员" +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:51 +msgid "Approve applications" +msgstr "批准的应用" -#: tickets/serializers/request_asset_perm.py:156 -#, python-brace-format +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:56 +msgid "Approve applications display" +msgstr "批准的应用" + +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:60 +#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:57 +msgid "Approve system users" +msgstr "批准的系统用户" + +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:65 +msgid "Approve system user display" +msgstr "批准的系统用户" + +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:89 +msgid "No `Application` are found under Organization `{}`" +msgstr "在组织 `{}` 下没有发现 `应用`" + +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:107 +#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:106 +msgid "No `SystemUser` are found under Organization `{}`" +msgstr "在组织 `{}` 下没有发现 `系统用户`" + +#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:18 +msgid "IP group" +msgstr "IP组" + +#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:22 +msgid "Hostname group" +msgstr "主机名组" + +#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:34 +#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:52 +#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:61 +#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:69 +msgid "Approve assets display" +msgstr "批准的资产" + +#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:48 +msgid "Approve assets" +msgstr "批准的资产" + +#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:90 +msgid "No `Asset` are found under Organization `{}`" +msgstr "在组织 `{}` 下没有发现 `资产`" + +#: tickets/serializers/ticket/meta/ticket_type/login_confirm.py:20 +msgid "Login datetime" +msgstr "登录日期" + +#: tickets/serializers/ticket/ticket.py:92 msgid "" -"\n" -" Type: {type}
\n" -" User: {username}
\n" -" Ip group: {ips}
\n" -" Hostname: {hostname}
\n" -" System user: {system_user}
\n" -" Date start: {date_start}
\n" -" Date expired: {date_expired}
\n" -" " -msgstr "" -"\n" -" 类型: {type}
\n" -" 用户: {username}
\n" -" IP 组: {ips}
\n" -" 主机名: {hostname}
\n" -" 系统用户: {system_user}
\n" -" 开始时间: {date_start}
\n" -" 过期时间: {date_expired}
\n" -" " +"The `type` in the submission data (`{}`) is different from the type in the " +"request url (`{}`)" +msgstr "提交数据中的类型 (`{}`) 与请求URL地址中的类型 (`{}`) 不一致" + +#: tickets/serializers/ticket/ticket.py:102 +msgid "The organization `{}` does not exist" +msgstr "组织 `{}` 不存在" + +#: tickets/serializers/ticket/ticket.py:113 +msgid "None of the assignees belong to Organization `{}` admins" +msgstr "所有受理人都不属于组织 `{}` 下的管理员" #: tickets/utils.py:21 -msgid "New ticket" -msgstr "新工单" +msgid "New Ticket: {} ({})" +msgstr "新建工单: {} ({})" -#: tickets/utils.py:25 +#: tickets/utils.py:26 #, python-brace-format msgid "" -"\n" -"
\n" +"
\n" "

Your has a new ticket

\n" "
\n" -" {body}\n" +" Ticket: \n" "
\n" -" click here to review \n" +" {body}\n" +"
\n" +" click here to review \n" "
\n" "
\n" -" " +" " msgstr "" -"\n" -"
\n" +"
\n" "

你有一个新工单

\n" "
\n" -" {body}\n" +" 工单: \n" "
\n" -" 点击我查看 \n" +" {body}\n" +"
\n" +" 点击查看 \n" "
\n" "
\n" -" " +" " -#: tickets/utils.py:44 -msgid "Ticket has been reply" -msgstr "工单已被回复" +#: tickets/utils.py:51 +msgid "Ticket has processed: {} ({})" +msgstr "工单已处理: {} ({})" -#: tickets/utils.py:45 +#: tickets/utils.py:53 #, python-brace-format msgid "" "\n" "
\n" -"

Your ticket has been replay

\n" +"

Your ticket has been processed

\n" "
\n" -" Title: {ticket.title}\n" +" Ticket: \n" "
\n" -" Assignee: {ticket.assignee_display}\n" -"
\n" -" Status: {ticket.status_display}\n" +" {body}\n" "
\n" "
\n" "
\n" -" " +" " msgstr "" "\n" "
\n" -"

您的工单已被回复

\n" +"

你的工单已被处理

\n" "
\n" -" 标题: {ticket.title}\n" +" 工单: \n" "
\n" -" 处理人: {ticket.assignee_display}\n" -"
\n" -" 状态: {ticket.status_display}\n" +" {body}\n" "
\n" "
\n" "
\n" -" " +" " #: users/api/user.py:199 msgid "Could not reset self otp, use profile reset instead" @@ -3179,7 +3074,7 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:89 users/models/user.py:497 +#: users/forms/profile.py:89 users/models/user.py:520 #: users/templates/users/user_detail.html:57 #: users/templates/users/user_profile.html:59 msgid "Email" @@ -3220,7 +3115,7 @@ msgstr "不能和原来的密钥相同" msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/user.py:31 users/models/user.py:540 +#: users/forms/user.py:31 users/models/user.py:563 #: users/templates/users/user_detail.html:89 #: users/templates/users/user_list.html:18 #: users/templates/users/user_profile.html:102 @@ -3236,7 +3131,7 @@ msgid "Join user groups" msgstr "添加到用户组" #: users/forms/user.py:103 users/views/profile/password.py:59 -#: users/views/profile/reset.py:124 +#: users/views/profile/reset.py:127 msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" @@ -3262,31 +3157,31 @@ msgstr "系统管理员" msgid "System auditor" msgstr "系统审计员" -#: users/models/user.py:423 users/templates/users/user_profile.html:90 +#: users/models/user.py:446 users/templates/users/user_profile.html:90 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:485 +#: users/models/user.py:508 msgid "Local" msgstr "数据库" -#: users/models/user.py:508 +#: users/models/user.py:531 msgid "Avatar" msgstr "头像" -#: users/models/user.py:511 users/templates/users/user_detail.html:68 +#: users/models/user.py:534 users/templates/users/user_detail.html:68 msgid "Wechat" msgstr "微信" -#: users/models/user.py:544 +#: users/models/user.py:567 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:651 +#: users/models/user.py:674 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:654 +#: users/models/user.py:677 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -3368,7 +3263,7 @@ msgstr "安全令牌验证" #: users/templates/users/_base_otp.html:14 users/templates/users/_user.html:13 #: users/templates/users/user_profile_update.html:55 -#: xpack/plugins/cloud/models.py:125 xpack/plugins/cloud/serializers.py:113 +#: xpack/plugins/cloud/models.py:125 xpack/plugins/cloud/serializers.py:125 msgid "Account" msgstr "账户" @@ -3436,10 +3331,18 @@ msgstr "授权的资产" msgid "RemoteApp granted" msgstr "授权的远程应用" +#: users/templates/users/_user_detail_nav_header.html:47 +msgid "RemoteApp permission" +msgstr "远程应用授权" + #: users/templates/users/_user_detail_nav_header.html:54 msgid "DatabaseApp granted" msgstr "授权的数据库应用" +#: users/templates/users/_user_detail_nav_header.html:61 +msgid "DatabaseApp permission" +msgstr "数据库应用授权" + #: users/templates/users/_user_update_pk_modal.html:4 msgid "Update User SSH Public Key" msgstr "更新SSH密钥" @@ -3674,6 +3577,10 @@ msgstr "解除用户登录限制后,此用户即可正常登录" msgid "Reset user MFA success" msgstr "重置用户多因子认证成功" +#: users/templates/users/user_granted_remote_app.html:35 +msgid "App type" +msgstr "应用类型" + #: users/templates/users/user_group_detail.html:17 #: users/templates/users/user_group_granted_asset.html:18 msgid "User group detail" @@ -3855,8 +3762,7 @@ msgstr "新的公钥已设置成功,请下载对应的私钥" msgid "Update user" msgstr "更新用户" -#: users/templates/users/user_update.html:22 users/views/profile/reset.py:50 -#: users/views/profile/reset.py:117 +#: users/templates/users/user_update.html:22 users/views/profile/reset.py:120 msgid "User auth from {}, go there change password" msgstr "用户认证源来自 {}, 请去相应系统修改密码" @@ -3964,7 +3870,7 @@ msgstr "" "
\n" " " -#: users/utils.py:116 users/views/profile/reset.py:77 +#: users/utils.py:116 users/views/profile/reset.py:80 msgid "Reset password success" msgstr "重置密码成功" @@ -4167,24 +4073,30 @@ msgstr "用户名或密码无效" msgid "Public key update" msgstr "密钥更新" -#: users/views/profile/reset.py:46 +#: users/views/profile/reset.py:45 msgid "Email address invalid, please input again" msgstr "邮箱地址错误,重新输入" -#: users/views/profile/reset.py:63 +#: users/views/profile/reset.py:51 +msgid "" +"The user is from A, please go to the corresponding system to change the " +"password" +msgstr "用户来自 {} 请去相应系统修改密码" + +#: users/views/profile/reset.py:66 msgid "Send reset password message" msgstr "发送重置密码邮件" -#: users/views/profile/reset.py:64 +#: users/views/profile/reset.py:67 msgid "Send reset password mail success, login your mail box and follow it " msgstr "" "发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)" -#: users/views/profile/reset.py:78 +#: users/views/profile/reset.py:81 msgid "Reset password success, return to login page" msgstr "重置密码成功,返回到登录页面" -#: users/views/profile/reset.py:102 users/views/profile/reset.py:112 +#: users/views/profile/reset.py:105 users/views/profile/reset.py:115 msgid "Token invalid or expired" msgstr "Token错误或失效" @@ -4315,7 +4227,7 @@ msgstr "实例名称" msgid "Instance name and Partial IP" msgstr "实例名称和部分IP" -#: xpack/plugins/cloud/models.py:128 xpack/plugins/cloud/serializers.py:89 +#: xpack/plugins/cloud/models.py:128 xpack/plugins/cloud/serializers.py:101 msgid "Regions" msgstr "地域" @@ -4327,7 +4239,7 @@ msgstr "实例" msgid "Hostname strategy" msgstr "主机名策略" -#: xpack/plugins/cloud/models.py:147 xpack/plugins/cloud/serializers.py:117 +#: xpack/plugins/cloud/models.py:147 xpack/plugins/cloud/serializers.py:129 msgid "Always update" msgstr "总是更新" @@ -4339,6 +4251,10 @@ msgstr "最后同步日期" msgid "Sync instance task" msgstr "同步实例任务" +#: xpack/plugins/cloud/models.py:215 +msgid "Succeed" +msgstr "成功" + #: xpack/plugins/cloud/models.py:230 xpack/plugins/cloud/models.py:285 msgid "Date sync" msgstr "同步日期" @@ -4371,10 +4287,6 @@ msgstr "同步实例任务历史" msgid "Instance" msgstr "实例" -#: xpack/plugins/cloud/models.py:276 -msgid "Region" -msgstr "地域" - #: xpack/plugins/cloud/providers/aliyun.py:22 msgid "Alibaba Cloud" msgstr "阿里云" @@ -4387,7 +4299,7 @@ msgstr "AWS (国际)" msgid "AWS (China)" msgstr "AWS (中国)" -#: xpack/plugins/cloud/providers/azure_.py:18 +#: xpack/plugins/cloud/providers/azure_.py:16 msgid "Azure (China)" msgstr "Azure (中国)" @@ -4451,23 +4363,23 @@ msgstr "拉美-圣地亚哥" msgid "Tencent Cloud" msgstr "腾讯云" -#: xpack/plugins/cloud/serializers.py:26 +#: xpack/plugins/cloud/serializers.py:22 msgid "Tenant ID" msgstr "租户ID" -#: xpack/plugins/cloud/serializers.py:30 +#: xpack/plugins/cloud/serializers.py:26 msgid "Subscription ID" msgstr "订阅ID" -#: xpack/plugins/cloud/serializers.py:87 +#: xpack/plugins/cloud/serializers.py:99 msgid "History count" msgstr "执行次数" -#: xpack/plugins/cloud/serializers.py:88 +#: xpack/plugins/cloud/serializers.py:100 msgid "Instance count" msgstr "实例个数" -#: xpack/plugins/cloud/serializers.py:116 +#: xpack/plugins/cloud/serializers.py:128 #: xpack/plugins/gathered_user/serializers.py:20 msgid "Periodic display" msgstr "定时执行" @@ -4559,2105 +4471,3 @@ msgstr "旗舰版" #: xpack/plugins/license/models.py:77 msgid "Community edition" msgstr "社区版" - -#, python-format -#~ msgid "The max size of CSV is %d bytes" -#~ msgstr "CSV 文件最大为 %d 字节" - -#, fuzzy -#~| msgid "Confirmed system user" -#~ msgid "Confirmed systemusers" -#~ msgstr "确认的系统用户" - -#~ msgid "MFA level" -#~ msgstr "多因子认证级别" - -#~ msgid "Beijing unicom" -#~ msgstr "北京联通" - -#~ msgid "Beijing telecom" -#~ msgstr "北京电信" - -#~ msgid "BGP full netcom" -#~ msgstr "BGP全网通" - -#~ msgid "ungrouped" -#~ msgstr "未分组" - -#~ msgid "Organization User" -#~ msgstr "组织用户" - -#~ msgid "System User" -#~ msgstr "系统用户" - -#~ msgid "Auditor" -#~ msgstr "审计员" - -#~ msgid "Org admin" -#~ msgstr "组织管理员" - -#~ msgid "Role name" -#~ msgstr "角色名" - -#~ msgid "Covered always" -#~ msgstr "总是被覆盖" - -#~ msgid "Account name" -#~ msgstr "账户名称" - -#~ msgid "Superuser" -#~ msgstr "超级管理员" - -#~ msgid "Auditors cannot be join in the user group" -#~ msgstr "审计员不能被加入到用户组" - -#~ msgid "Login username" -#~ msgstr "登录账号" - -#~ msgid "Login password" -#~ msgstr "登录密码" - -#~ msgid "Database IP" -#~ msgstr "数据库IP" - -#~ msgid "Database name" -#~ msgstr "数据库名" - -#~ msgid "Database username" -#~ msgstr "数据库账号" - -#~ msgid "Database password" -#~ msgstr "数据库密码" - -#~ msgid "Target address" -#~ msgstr "目标地址" - -#~ msgid "Detail" -#~ msgstr "详情" - -#~ msgid "Create DatabaseApp" -#~ msgstr "创建数据库应用" - -#~ msgid "" -#~ "Before using this feature, make sure that the application loader has been " -#~ "uploaded to the application server and successfully published as a " -#~ "RemoteApp application" -#~ msgstr "" -#~ "使用此功能前,请确保已将应用加载器上传到应用服务器并成功发布为一个 " -#~ "RemoteApp 应用" - -#~ msgid "Download application loader" -#~ msgstr "下载应用加载器" - -#~ msgid "Create RemoteApp" -#~ msgstr "创建远程应用" - -#~ msgid "DatabaseApp list" -#~ msgstr "数据库应用列表" - -#~ msgid "DatabaseApp detail" -#~ msgstr "数据库应用详情" - -#~ msgid "My DatabaseApp" -#~ msgstr "我的数据库应用" - -#~ msgid "Update RemoteApp" -#~ msgstr "更新远程应用" - -#~ msgid "RemoteApp detail" -#~ msgstr "远程应用详情" - -#~ msgid "My RemoteApp" -#~ msgstr "我的远程应用" - -#~ msgid "Label" -#~ msgstr "标签" - -#~ msgid "" -#~ "root or other NOPASSWD sudo privilege user existed in asset,If asset is " -#~ "windows or other set any one, more see admin user left menu" -#~ msgstr "" -#~ "root或其他拥有NOPASSWD: ALL权限的用户, 如果是windows或其它硬件可以随意设置" -#~ "一个, 更多信息查看左侧 `管理用户` 菜单" - -#~ msgid "Windows 2016 RDP protocol is different, If is window 2016, set it" -#~ msgstr "Windows 2016的RDP协议与之前不同,如果是请设置" - -#~ msgid "" -#~ "If your have some network not connect with each other, you can set domain" -#~ msgstr "" -#~ "如果有多个的互相隔离的网络,设置资产属于的网域,使用网域网关跳转登录" - -#~ msgid "Select assets" -#~ msgstr "选择资产" - -#~ msgid "Content should not be contain: {}" -#~ msgstr "内容不能包含: {}" - -#~ msgid "SSH gateway support proxy SSH,RDP,VNC" -#~ msgstr "SSH网关,支持代理SSH,RDP和VNC" - -#~ msgid "Yes" -#~ msgstr "是" - -#~ msgid "No" -#~ msgstr "否" - -#~ msgid "Base platform" -#~ msgstr "基础平台" - -#~ msgid "Password or private key passphrase" -#~ msgstr "密码或密钥密码" - -#~ msgid "Invalid private key, Only support RSA/DSA format key" -#~ msgstr "不合法的密钥,仅支持RSA/DSA格式的密钥" - -#~ msgid "Password and private key file must be input one" -#~ msgstr "密码和私钥, 必须输入一个" - -#~ msgid "Auto push system user to asset" -#~ msgstr "自动推送系统用户到资产" - -#~ msgid "" -#~ "1-100, High level will be using login asset as default, if user was " -#~ "granted more than 2 system user" -#~ msgstr "" -#~ "1-100, 1最低优先级,100最高优先级。授权多个用户时,高优先级的系统用户将会" -#~ "作为默认登录用户" - -#~ msgid "" -#~ "If you choose manual login mode, you do not need to fill in the username " -#~ "and password." -#~ msgstr "如果选择手动登录模式,用户名和密码可以不填写" - -#~ msgid "Use comma split multi command, ex: /bin/whoami,/bin/ifconfig" -#~ msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig" - -#~ msgid "SFTP root dir, tmp, home or custom" -#~ msgstr "SFTP的起始路径,tmp目录, 用户home目录或者自定义" - -#~ msgid "" -#~ "Username is dynamic, When connect asset, using current user's username" -#~ msgstr "用户名是动态的,登录资产时使用当前用户的用户名登录" - -#~ msgid "Update asset group" -#~ msgstr "更新用户组" - -#~ msgid "Hint: only change the field you want to update." -#~ msgstr "仅修改你需要更新的字段" - -#~ msgid "Select Asset" -#~ msgstr "选择资产" - -#~ msgid "Select System Users" -#~ msgstr "选择系统用户" - -#~ msgid "Enable-MFA" -#~ msgstr "启用多因子认证" - -#~ msgid "Update asset user auth" -#~ msgstr "更新资产用户认证信息" - -#~ msgid "Asset user auth" -#~ msgstr "资产用户信息" - -#~ msgid "Get auth info error" -#~ msgstr "获取认证信息错误" - -#~ msgid "Test datetime: " -#~ msgstr "测试日期: " - -#~ msgid "Only latest version" -#~ msgstr "仅最新版本" - -#~ msgid "View" -#~ msgstr "查看" - -#~ msgid "Test" -#~ msgstr "测试" - -#~ msgid "Push" -#~ msgstr "推送" - -#~ msgid "Test gateway test connection" -#~ msgstr "测试连接网关" - -#~ msgid "If use nat, set the ssh real port" -#~ msgstr "如果使用了nat端口映射,请设置为ssh真实监听的端口" - -#~ msgid "Node detail" -#~ msgstr "节点详情" - -#~ msgid "Full name" -#~ msgstr "全称" - -#~ msgid "Add node" -#~ msgstr "新建节点" - -#~ msgid "Rename node" -#~ msgstr "重命名节点" - -#~ msgid "Delete node" -#~ msgstr "删除节点" - -#~ msgid "Create node failed" -#~ msgstr "创建节点失败" - -#~ msgid "Rename success" -#~ msgstr "重命名成功" - -#~ msgid "Basic" -#~ msgstr "基本" - -#~ msgid "Auto generate key" -#~ msgstr "自动生成密钥" - -#~ msgid "Other" -#~ msgstr "其它" - -#~ msgid "Asset detail" -#~ msgstr "资产详情" - -#~ msgid "Assets list" -#~ msgstr "资产列表" - -#~ msgid "Asset list of " -#~ msgstr "资产列表" - -#~ msgid "Quick update" -#~ msgstr "快速更新" - -#~ msgid "Test connective" -#~ msgstr "测试可连接性" - -#~ msgid "Replace node assets admin user with this" -#~ msgstr "替换资产的管理员" - -#~ msgid "Select nodes" -#~ msgstr "选择节点" - -#~ msgid "" -#~ "Admin users are asset (charged server) on the root, or have NOPASSWD: ALL " -#~ "sudo permissions users, " -#~ msgstr "" -#~ "管理用户是资产(被控服务器)上的 root,或拥有 NOPASSWD: ALL sudo 权限的用" -#~ "户," - -#~ msgid "" -#~ "JumpServer users of the system using the user to `push system user`, " -#~ "`get assets hardware information`, etc. " -#~ msgstr "JumpServer 使用该用户来 `推送系统用户`、`获取资产硬件信息` 等。" - -#~ msgid "Create admin user" -#~ msgstr "创建管理用户" - -#~ msgid "Asset user list" -#~ msgstr "资产用户列表" - -#~ msgid "Asset users of" -#~ msgstr "资产用户" - -#~ msgid "CPU" -#~ msgstr "CPU" - -#~ msgid "Disk" -#~ msgstr "硬盘" - -#~ msgid "Refresh hardware" -#~ msgstr "更新硬件信息" - -#~ msgid "" -#~ "The left side is the asset tree, right click to create, delete, and " -#~ "change the tree node, authorization asset is also organized as a node, " -#~ "and the right side is the asset under that node" -#~ msgstr "" -#~ "左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织" -#~ "的,右侧是属于该节点下的资产" - -#~ msgid "Create asset" -#~ msgstr "创建资产" - -#~ msgid "Hardware" -#~ msgstr "硬件" - -#~ msgid "Remove from this node" -#~ msgstr "从节点移除" - -#~ msgid "Add assets to node" -#~ msgstr "添加资产到节点" - -#~ msgid "Move assets to node" -#~ msgstr "移动资产到节点" - -#~ msgid "Refresh node hardware info" -#~ msgstr "更新节点资产硬件信息" - -#~ msgid "Test node connective" -#~ msgstr "测试节点资产可连接性" - -#~ msgid "Display only current node assets" -#~ msgstr "仅显示当前节点资产" - -#~ msgid "Displays all child node assets" -#~ msgstr "显示所有子节点资产" - -#~ msgid "This will delete the selected assets !!!" -#~ msgstr "删除选择资产" - -#~ msgid "Asset Deleting failed." -#~ msgstr "删除失败" - -#~ msgid "Asset Delete" -#~ msgstr "删除" - -#~ msgid "Asset Deleted." -#~ msgstr "已被删除" - -#~ msgid "Please select node" -#~ msgstr "请选择节点" - -#~ msgid "Configuration" -#~ msgstr "配置" - -#~ msgid "Rules" -#~ msgstr "规则" - -#~ msgid "Binding to system user" -#~ msgstr "绑定到系统用户" - -#~ msgid "" -#~ "System user bound some command filter, each command filter has some rules," -#~ msgstr "系统用户可以绑定一些命令过滤器,一个过滤器可以定义一些规则" - -#~ msgid "When user login asset with this system user, then run a command," -#~ msgstr "当用户使用这个系统用户登录资产,然后执行一个命令" - -#~ msgid "The command will be filter by rules, higher priority rule run first," -#~ msgstr "这个命令需要被绑定过滤器的所有规则匹配,高优先级先被匹配," - -#~ msgid "" -#~ "When a rule matched, if rule action is allow, then allow command execute," -#~ msgstr "当一个规则匹配到了,如果规则的动作是允许,这个命令会被放行," - -#~ msgid "else if action is deny, then command with be deny," -#~ msgstr "如果规则的动作是禁止,命令将会被禁止执行," - -#~ msgid "else match next rule, if none matched, allowed" -#~ msgstr "否则就匹配下一个规则,如果最后没有匹配到规则,则允许执行" - -#~ msgid "Create command filter" -#~ msgstr "创建命令过滤器" - -#~ msgid "Command filter rule list" -#~ msgstr "命令过滤器规则列表" - -#~ msgid "Create rule" -#~ msgstr "创建规则" - -#~ msgid "Strategy" -#~ msgstr "策略" - -#~ msgid "Gateway list" -#~ msgstr "网关列表" - -#~ msgid "Create gateway" -#~ msgstr "创建网关" - -#~ msgid "Test connection" -#~ msgstr "测试连接" - -#~ msgid "Can be connected" -#~ msgstr "可连接" - -#~ msgid "" -#~ "The domain function is added to address the fact that some environments " -#~ "(such as the hybrid cloud) cannot be connected directly by jumping on the " -#~ "gateway server." -#~ msgstr "" -#~ "网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通" -#~ "过网关服务器进行跳转登录。" - -#~ msgid "JMS => Domain gateway => Target assets" -#~ msgstr "JMS => 网域网关 => 目标资产" - -#~ msgid "Create domain" -#~ msgstr "创建网域" - -#~ msgid "Create label" -#~ msgstr "创建标签" - -#~ msgid "Create platform" -#~ msgstr "创建系统平台" - -#~ msgid "Test assets connective" -#~ msgstr "测试资产可连接性" - -#~ msgid "Push system user now" -#~ msgstr "立刻推送系统" - -#~ msgid "Have existed: " -#~ msgstr "已经存在: " - -#~ msgid "Uid" -#~ msgstr "Uid" - -#~ msgid "Binding command filters" -#~ msgstr "绑定命令过滤器" - -#~ msgid "" -#~ "System user is JumpServer jump login assets used by the users, can be " -#~ "understood as the user login assets, such as web, sa, the dba (` ssh " -#~ "web@some-host `), rather than using a user the username login server jump " -#~ "(` ssh xiaoming@some-host `); " -#~ msgstr "" -#~ "系统用户是 JumpServer 跳转登录资产时使用的用户,可以理解为登录资产用户," -#~ "如 web,sa,dba(`ssh web@some-host`),而不是使用某个用户的用户名跳转登录" -#~ "服务器(`ssh xiaoming@some-host`);" - -#~ msgid "" -#~ "In simple terms, users log into JumpServer using their own username, and " -#~ "JumpServer uses system users to log into assets. " -#~ msgstr "" -#~ "简单来说是用户使用自己的用户名登录 JumpServer,JumpServer 使用系统用户登录" -#~ "资产。" - -#~ msgid "" -#~ "When system users are created, if you choose auto push JumpServer to use " -#~ "Ansible push system users into the asset, if the asset (Switch) does not " -#~ "support ansible, please manually fill in the account password." -#~ msgstr "" -#~ "系统用户创建时,如果选择了自动推送,JumpServer 会使用 Ansible 自动推送系统" -#~ "用户到资产中,如果资产(交换机)不支持 Ansible,请手动填写账号密码。" - -#~ msgid "Remove success" -#~ msgstr "移除成功" - -#~ msgid "Admin user list" -#~ msgstr "管理用户列表" - -#~ msgid "Update admin user" -#~ msgstr "更新管理用户" - -#~ msgid "Admin user detail" -#~ msgstr "管理用户详情" - -#~ msgid "Admin user assets" -#~ msgstr "管理用户关联资产" - -#~ msgid "Update asset" -#~ msgstr "更新资产" - -#~ msgid "Bulk update asset success" -#~ msgstr "批量更新资产成功" - -#~ msgid "Bulk update asset" -#~ msgstr "批量更新资产" - -#~ msgid "Command filter list" -#~ msgstr "命令过滤器列表" - -#~ msgid "Update command filter" -#~ msgstr "更新命令过滤器" - -#~ msgid "Command filter detail" -#~ msgstr "命令过滤器详情" - -#~ msgid "Create command filter rule" -#~ msgstr "创建命令过滤器规则" - -#~ msgid "Update command filter rule" -#~ msgstr "更新命令过滤器规则" - -#~ msgid "Domain detail" -#~ msgstr "网域详情" - -#~ msgid "Domain gateway list" -#~ msgstr "域网关列表" - -#~ msgid "Update gateway" -#~ msgstr "创建网关" - -#~ msgid "Label list" -#~ msgstr "标签列表" - -#~ msgid "Tips: Avoid using label names reserved internally: {}" -#~ msgstr "提示: 请避免使用内部预留标签名: {}" - -#~ msgid "Update label" -#~ msgstr "更新标签" - -#~ msgid "Update platform" -#~ msgstr "更新系统平台" - -#~ msgid "Platform detail" -#~ msgstr "平台详情" - -#~ msgid "Update system user" -#~ msgstr "更新系统用户" - -#~ msgid "System user detail" -#~ msgstr "系统用户详情" - -#~ msgid "assets" -#~ msgstr "资产管理" - -#~ msgid "System user assets" -#~ msgstr "系统用户关联资产" - -#~ msgid "Select user" -#~ msgstr "选择用户" - -#~ msgid "UA" -#~ msgstr "Agent" - -#~ msgid "Handlers" -#~ msgstr "操作者" - -#~ msgid "Command execution log" -#~ msgstr "命令执行" - -#~ msgid "Version detail" -#~ msgstr "版本详情" - -#~ msgid "Version run execution" -#~ msgstr "执行历史" - -#~ msgid "Last run" -#~ msgstr "最后运行" - -#~ msgid "Is success " -#~ msgstr "成功" - -#~ msgid "Last run failed hosts" -#~ msgstr "最后运行失败主机" - -#~ msgid "No hosts" -#~ msgstr "没有主机" - -#~ msgid "Last run success hosts" -#~ msgstr "最后运行成功主机" - -#~ msgid "Executions of " -#~ msgstr "执行历史 " - -#~ msgid "F/S/T" -#~ msgstr "失败/成功/总" - -#~ msgid "Ratio" -#~ msgstr "比例" - -#~ msgid "Execution detail" -#~ msgstr "执行历史详情" - -#~ msgid "Execution detail of" -#~ msgstr "执行历史详情" - -#~ msgid "Task name" -#~ msgstr "任务名称" - -#~ msgid "No assets" -#~ msgstr "没有资产" - -#~ msgid "Success assets" -#~ msgstr "成功资产" - -#~ msgid "Asset configuration does not include the SSH protocol" -#~ msgstr "资产配置不包含 SSH 协议" - -#~ msgid "Selected assets" -#~ msgstr "已选择资产" - -#~ msgid "In total" -#~ msgstr "总共" - -#~ msgid "" -#~ "Select the left asset, select the running system user, execute command in " -#~ "batch" -#~ msgstr "选择左侧资产, 选择运行的系统用户,批量执行命令" - -#~ msgid "Unselected assets" -#~ msgstr "没有选中资产" - -#~ msgid "No input command" -#~ msgstr "没有输入命令" - -#~ msgid "No system user was selected" -#~ msgstr "没有选择系统用户" - -#~ msgid "Pending" -#~ msgstr "等待" - -#~ msgid "Task detail" -#~ msgstr "任务详情" - -#~ msgid "Task versions" -#~ msgstr "任务各版本" - -#~ msgid "Execution" -#~ msgstr "执行历史" - -#~ msgid "Last execution output" -#~ msgstr "最后执行输出" - -#~ msgid "Versions of " -#~ msgstr "版本" - -#~ msgid "Total versions" -#~ msgstr "版本数量" - -#~ msgid "Contents" -#~ msgstr "内容" - -#~ msgid "Run" -#~ msgstr "执行" - -#~ msgid "Task start: " -#~ msgstr "任务开始: " - -#~ msgid "Ops" -#~ msgstr "作业中心" - -#~ msgid "Task execution list" -#~ msgstr "任务执行列表" - -#~ msgid "Command execution list" -#~ msgstr "命令执行列表" - -#~ msgid "Users and user groups" -#~ msgstr "用户或用户组" - -#~ msgid "Assets and node" -#~ msgstr "资产或节点" - -#~ msgid "Add asset to this permission" -#~ msgstr "添加资产" - -#~ msgid "Add node to this permission" -#~ msgstr "添加节点" - -#~ msgid "Select system users" -#~ msgstr "选择系统用户" - -#~ msgid "Validity period" -#~ msgstr "有效期" - -#~ msgid "Create permission" -#~ msgstr "创建授权规则" - -#~ msgid "Refresh permission cache" -#~ msgstr "刷新授权缓存" - -#~ msgid "Refresh success" -#~ msgstr "刷新成功" - -#~ msgid "User list of " -#~ msgstr "用户列表" - -#~ msgid "Add user to asset permission" -#~ msgstr "添加用户" - -#~ msgid "Add user group to asset permission" -#~ msgstr "添加用户组" - -#~ msgid "Select user groups" -#~ msgstr "选择用户组" - -#~ msgid "DatabaseApp list of " -#~ msgstr "数据库应用列表" - -#~ msgid "Add DatabaseApp to this permission" -#~ msgstr "添加数据库应用" - -#~ msgid "Select DatabaseApp" -#~ msgstr "选择数据库应用" - -#~ msgid "Add user to permission" -#~ msgstr "添加用户" - -#~ msgid "Add user group to permission" -#~ msgstr "添加用户组" - -#~ msgid "RemoteApp list of " -#~ msgstr "远程应用列表" - -#~ msgid "Add RemoteApp to this permission" -#~ msgstr "添加远程应用" - -#~ msgid "Select RemoteApp" -#~ msgstr "选择远程应用" - -#~ msgid "Add user to this permission" -#~ msgstr "添加用户" - -#~ msgid "Add user group to this permission" -#~ msgstr "添加用户组" - -#~ msgid "Asset permission list" -#~ msgstr "资产授权列表" - -#~ msgid "Update asset permission" -#~ msgstr "更新资产授权" - -#~ msgid "Asset permission detail" -#~ msgstr "资产授权详情" - -#~ msgid "Asset permission user list" -#~ msgstr "资产授权用户列表" - -#~ msgid "Asset permission asset list" -#~ msgstr "资产授权资产列表" - -#~ msgid "DatabaseApp permission list" -#~ msgstr "数据库应用授权列表" - -#~ msgid "Create DatabaseApp permission" -#~ msgstr "创建数据库应用授权规则" - -#~ msgid "Update DatabaseApp permission" -#~ msgstr "更新数据库应用授权规则" - -#~ msgid "DatabaseApp permission detail" -#~ msgstr "数据库应用授权详情" - -#~ msgid "DatabaseApp permission user list" -#~ msgstr "数据库应用授权用户列表" - -#~ msgid "DatabaseApp permission DatabaseApp list" -#~ msgstr "数据库应用授权数据库应用列表" - -#~ msgid "RemoteApp permission list" -#~ msgstr "远程应用授权列表" - -#~ msgid "Create RemoteApp permission" -#~ msgstr "创建远程应用授权规则" - -#~ msgid "Update RemoteApp permission" -#~ msgstr "更新远程应用授权规则" - -#~ msgid "RemoteApp permission detail" -#~ msgstr "远程应用授权详情" - -#~ msgid "RemoteApp permission user list" -#~ msgstr "远程应用授权用户列表" - -#~ msgid "RemoteApp permission RemoteApp list" -#~ msgstr "远程应用授权远程应用列表" - -#~ msgid "Current SITE URL" -#~ msgstr "当前站点URL" - -#~ msgid "User Guide URL" -#~ msgstr "用户向导URL" - -#~ msgid "User first login update profile done redirect to it" -#~ msgstr "用户第一次登录,修改profile后重定向到地址" - -#~ msgid "Email Subject Prefix" -#~ msgstr "Email主题前缀" - -#~ msgid "Tips: Some word will be intercept by mail provider" -#~ msgstr "提示: 一些关键字可能会被邮件提供商拦截,如 跳板机、JumpServer" - -#~ msgid "SMTP host" -#~ msgstr "SMTP主机" - -#~ msgid "SMTP port" -#~ msgstr "SMTP端口" - -#~ msgid "SMTP user" -#~ msgstr "SMTP账号" - -#~ msgid "SMTP password" -#~ msgstr "SMTP密码" - -#~ msgid "Tips: Some provider use token except password" -#~ msgstr "提示:一些邮件提供商需要输入的是Token" - -#~ msgid "Tips: Send mail account, default SMTP account as the send account" -#~ msgstr "提示:发送邮件账号,默认使用SMTP账号作为发送账号" - -#~ msgid "Test recipient" -#~ msgstr "测试收件人" - -#~ msgid "Tips: Used only as a test mail recipient" -#~ msgstr "提示:仅用来作为测试邮件收件人" - -#~ msgid "Use SSL" -#~ msgstr "使用SSL" - -#~ msgid "If SMTP port is 465, may be select" -#~ msgstr "如果SMTP端口是465,通常需要启用SSL" - -#~ msgid "Use TLS" -#~ msgstr "使用TLS" - -#~ msgid "If SMTP port is 587, may be select" -#~ msgstr "如果SMTP端口是587,通常需要启用TLS" - -#~ msgid "Create user email subject" -#~ msgstr "创建用户邮件的主题" - -#~ msgid "" -#~ "Tips: When creating a user, send the subject of the email (eg:Create " -#~ "account successfully)" -#~ msgstr "提示: 创建用户时,发送设置密码邮件的主题 (例如: 创建用户成功)" - -#~ msgid "Create user honorific" -#~ msgstr "创建用户邮件的敬语" - -#~ msgid "" -#~ "Tips: When creating a user, send the honorific of the email (eg:Hello)" -#~ msgstr "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 您好)" - -#~ msgid "Create user email content" -#~ msgstr "创建用户邮件的内容" - -#~ msgid "Tips:When creating a user, send the content of the email" -#~ msgstr "提示: 创建用户时,发送设置密码邮件的内容" - -#~ msgid "Signature" -#~ msgstr "署名" - -#~ msgid "Tips: Email signature (eg:jumpserver)" -#~ msgstr "提示: 邮件的署名 (例如: jumpserver)" - -#~ msgid "LDAP server" -#~ msgstr "LDAP地址" - -#~ msgid "Bind DN" -#~ msgstr "绑定DN" - -#~ msgid "User OU" -#~ msgstr "用户OU" - -#~ msgid "Use | split User OUs" -#~ msgstr "使用|分隔各OU" - -#~ msgid "User search filter" -#~ msgstr "用户过滤器" - -#, python-format -#~ msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" -#~ msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)" - -#~ msgid "User attr map" -#~ msgstr "LDAP属性映射" - -#~ msgid "" -#~ "User attr map present how to map LDAP user attr to jumpserver, username," -#~ "name,email is jumpserver attr" -#~ msgstr "" -#~ "用户属性映射代表怎样将LDAP中用户属性映射到jumpserver用户上,username, " -#~ "name,email 是jumpserver的属性" - -#~ msgid "Enable LDAP auth" -#~ msgstr "启用LDAP认证" - -#~ msgid "" -#~ "After opening, all user login must use MFA(valid for all users, including " -#~ "administrators)" -#~ msgstr "" -#~ "开启后,所有用户登录必须使用多因子认证(对所有用户有效,包括管理员)" - -#~ msgid "Batch execute commands" -#~ msgstr "批量命令" - -#~ msgid "Allow user batch execute commands" -#~ msgstr "允许用户批量执行命令" - -#~ msgid "Service account registration" -#~ msgstr "终端注册" - -#~ msgid "" -#~ "Allow using bootstrap token register service account, when terminal " -#~ "setup, can disable it" -#~ msgstr "允许使用bootstrap token注册终端, 当终端注册成功后可以禁止" - -#~ msgid "Limit the number of login failures" -#~ msgstr "限制登录失败次数" - -#~ msgid "No logon interval" -#~ msgstr "禁止登录时间间隔" - -#~ msgid "" -#~ "Tip: (unit/minute) if the user has failed to log in for a limited number " -#~ "of times, no login is allowed during this time interval." -#~ msgstr "" -#~ "提示:(单位:分)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录" - -#~ msgid "Connection max idle time" -#~ msgstr "连接最大空闲时间" - -#~ msgid "If idle time more than it, disconnect connection Unit: minute" -#~ msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)" - -#~ msgid "Password expiration time" -#~ msgstr "密码过期时间" - -#~ msgid "" -#~ "Tip: (unit: day) If the user does not update the password during the " -#~ "time, the user password will expire failure;The password expiration " -#~ "reminder mail will be automatic sent to the user by system within 5 days " -#~ "(daily) before the password expires" -#~ msgstr "" -#~ "提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码" -#~ "过期提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户" - -#~ msgid "Password minimum length" -#~ msgstr "密码最小长度 " - -#~ msgid "Must contain capital letters" -#~ msgstr "必须包含大写字母" - -#~ msgid "" -#~ "After opening, the user password changes and resets must contain " -#~ "uppercase letters" -#~ msgstr "开启后,用户密码修改、重置必须包含大写字母" - -#~ msgid "Must contain lowercase letters" -#~ msgstr "必须包含小写字母" - -#~ msgid "" -#~ "After opening, the user password changes and resets must contain " -#~ "lowercase letters" -#~ msgstr "开启后,用户密码修改、重置必须包含小写字母" - -#~ msgid "Must contain numeric characters" -#~ msgstr "必须包含数字字符" - -#~ msgid "" -#~ "After opening, the user password changes and resets must contain numeric " -#~ "characters" -#~ msgstr "开启后,用户密码修改、重置必须包含数字字符" - -#~ msgid "Must contain special characters" -#~ msgstr "必须包含特殊字符" - -#~ msgid "" -#~ "After opening, the user password changes and resets must contain special " -#~ "characters" -#~ msgstr "开启后,用户密码修改、重置必须包含特殊字符" - -#~ msgid "Password auth" -#~ msgstr "密码认证" - -#~ msgid "Public key auth" -#~ msgstr "密钥认证" - -#~ msgid "Heartbeat interval" -#~ msgstr "心跳间隔" - -#~ msgid "Units: seconds" -#~ msgstr "单位: 秒" - -#~ msgid "List sort by" -#~ msgstr "资产列表排序" - -#~ msgid "List page size" -#~ msgstr "资产分页每页数量" - -#~ msgid "" -#~ "Units: days, Session, record, command will be delete if more than " -#~ "duration, only in database" -#~ msgstr "" -#~ "单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss" -#~ "等不受影响)" - -#~ msgid "Telnet login regex" -#~ msgstr "Telnet 成功正则表达式" - -#~ msgid "ex: Last\\s*login|success|成功" -#~ msgstr "" -#~ "登录telnet服务器成功后的提示正则表达式,如: Last\\s*login|success|成功 " - -#~ msgid "LDAP user list" -#~ msgstr "LDAP 用户列表" - -#~ msgid "Please submit the LDAP configuration before import" -#~ msgstr "请先提交LDAP配置再进行导入" - -#~ msgid "Refresh cache" -#~ msgstr "刷新缓存" - -#~ msgid "Existing" -#~ msgstr "已存在" - -#~ msgid "" -#~ "User is not currently selected, please check the user you want to import" -#~ msgstr "当前无勾选用户,请勾选你想要导入的用户" - -#~ msgid "Test LDAP user login" -#~ msgstr "测试LDAP 用户登录" - -#~ msgid "Save the configuration before testing the login" -#~ msgstr "请先提交LDAP配置再进行测试登录" - -#~ msgid "Please input username" -#~ msgstr "请输入用户名" - -#~ msgid "Basic setting" -#~ msgstr "基本设置" - -#~ msgid "Email setting" -#~ msgstr "邮件设置" - -#~ msgid "Email content setting" -#~ msgstr "邮件内容设置" - -#~ msgid "LDAP setting" -#~ msgstr "LDAP设置" - -#~ msgid "Terminal setting" -#~ msgstr "终端设置" - -#~ msgid "Security setting" -#~ msgstr "安全设置" - -#~ msgid "Create User setting" -#~ msgstr "创建用户设置" - -#~ msgid "Test login" -#~ msgstr "测试登录" - -#~ msgid "Bulk import" -#~ msgstr "一键导入" - -#~ msgid "Password check rule" -#~ msgstr "密码校验规则" - -#~ msgid "Command and Replay storage configuration migrated to" -#~ msgstr "命令和录像存储配置已迁移到" - -#~ msgid "Sessions -> Terminal -> Storage configuration" -#~ msgstr "会话管理 -> 终端管理 -> 存储配置" - -#~ msgid "Here" -#~ msgstr "这里" - -#~ msgid "Update setting successfully" -#~ msgstr "更新设置成功" - -#~ msgid "Container name" -#~ msgstr "容器名称" - -#~ msgid "Account key" -#~ msgstr "账户密钥" - -#~ msgid "Endpoint suffix" -#~ msgstr "端点后缀" - -#~ msgid "Bucket" -#~ msgstr "桶名称" - -#~ msgid "Endpoint" -#~ msgstr "端点" - -#~ msgid "" -#~ "\n" -#~ " Tips: If there are multiple hosts, separate them with a comma " -#~ "(,) \n" -#~ "
\n" -#~ " eg: http://www.jumpserver.a.com,http://www.jumpserver.b.com\n" -#~ " " -#~ msgstr "" -#~ "\n" -#~ " 提示: 如果有多台主机,请使用逗号 ( , ) 进行分割\n" -#~ "
\n" -#~ " eg: http://www.jumpserver.a.com,http://www.jumpserver.b.com\n" -#~ " " - -#~ msgid "Index" -#~ msgstr "索引" - -#~ msgid "Doc type" -#~ msgstr "文档类型" - -#~ msgid "" -#~ "Command can store in server db or ES, default to server, more see docs" -#~ msgstr "" -#~ "命令支持存储到服务器端数据库、ES中,默认存储的服务器端数据库,更多查看文档" - -#~ msgid "" -#~ "Replay file can store in server disk, AWS S3, Aliyun OSS, default to " -#~ "server, more see docs" -#~ msgstr "" -#~ "录像文件支持存储到服务器端硬盘、AWS S3、 阿里云 OSS 中,默认存储到服务器端" -#~ "硬盘, 更多查看文档" - -#~ msgid "Export command" -#~ msgstr "导出命令" - -#~ msgid "Goto" -#~ msgstr "转到" - -#~ msgid "Create command storage" -#~ msgstr "创建命令存储" - -#~ msgid "Create replay storage" -#~ msgstr "创建录像存储" - -#~ msgid "Session detail" -#~ msgstr "会话详情" - -#~ msgid "Command list" -#~ msgstr "命令记录列表" - -#~ msgid "There is no command about this session" -#~ msgstr "该会话没有命令记录" - -#~ msgid "Replay session" -#~ msgstr "回放会话" - -#~ msgid "Monitor session" -#~ msgstr "监控" - -#~ msgid "Terminate session" -#~ msgstr "终止会话" - -#~ msgid "Terminate success" -#~ msgstr "终断成功" - -#~ msgid "Duration" -#~ msgstr "时长" - -#~ msgid "Terminate selected" -#~ msgstr "终断所选" - -#~ msgid "Confirm finished" -#~ msgstr "确认已完成" - -#~ msgid "Terminate task send, waiting ..." -#~ msgstr "终断任务已发送,请等待" - -#~ msgid "Terminate" -#~ msgstr "终断" - -#~ msgid "Monitoring" -#~ msgstr "监控" - -#~ msgid "Finish session success" -#~ msgstr "标记会话完成成功" - -#~ msgid "Visit doc for replay play offline: " -#~ msgstr "访问文档查看如何离线播放: " - -#~ msgid "Terminal detail" -#~ msgstr "终端详情" - -#~ msgid "SSH port" -#~ msgstr "SSH端口" - -#~ msgid "Http port" -#~ msgstr "HTTP端口" - -#~ msgid "Storage configuration" -#~ msgstr "存储配置" - -#~ msgid "Addr" -#~ msgstr "地址" - -#~ msgid "Alive" -#~ msgstr "在线" - -#~ msgid "Accept" -#~ msgstr "接受" - -#~ msgid "Accept terminal registration" -#~ msgstr "接受终端注册" - -#~ msgid "Info" -#~ msgstr "信息" - -#~ msgid "Session online list" -#~ msgstr "在线会话" - -#~ msgid "Replay storage list" -#~ msgstr "录像存储列表" - -#~ msgid "Command storage list" -#~ msgstr "命令存储列表" - -#~ msgid "Update replay storage" -#~ msgstr "更新录像存储" - -#~ msgid "Update command storage" -#~ msgstr "更新命令存储" - -#~ msgid "Terminal list" -#~ msgstr "终端列表" - -#~ msgid "Update terminal" -#~ msgstr "更新终端" - -#~ msgid "Redirect to web terminal" -#~ msgstr "重定向到web terminal" - -#~ msgid "Connect ssh terminal" -#~ msgstr "连接ssh终端" - -#~ msgid "" -#~ "You should use your ssh client tools connect terminal: {}

{}" -#~ msgstr "你可以使用ssh客户端工具连接终端" - -#~ msgid "ago" -#~ msgstr "前" - -#~ msgid "My tickets" -#~ msgstr "我的工单" - -#~ msgid "Assigned me" -#~ msgstr "待处理" - -#~ msgid "Create ticket" -#~ msgstr "提交工单" - -#~ msgid "Ticket detail" -#~ msgstr "工单详情" - -#~ msgid "I agree with the terms and conditions." -#~ msgstr "我同意条款和条件" - -#~ msgid "Please choose the terms and conditions." -#~ msgstr "请选择同意条款和条件" - -#~ msgid "Previous" -#~ msgstr "上一步" - -#~ msgid "User group list" -#~ msgstr "用户组列表" - -#~ msgid "Update user group" -#~ msgstr "更新用户组" - -#~ msgid "User group granted asset" -#~ msgstr "用户组授权资产" - -#~ msgid "First login" -#~ msgstr "首次登录" - -#~ msgid "Profile setting" -#~ msgstr "个人信息设置" - -#~ msgid "Bulk update user success" -#~ msgstr "批量更新用户成功" - -#~ msgid "Bulk update user" -#~ msgstr "批量更新用户" - -#~ msgid "User granted assets" -#~ msgstr "用户授权资产" - -#~ msgid "User granted RemoteApp" -#~ msgstr "用户授权远程应用" - -#~ msgid "User granted DatabaseApp" -#~ msgstr "用户授权数据库应用" - -#~ msgid "Password length" -#~ msgstr "密码长度" - -#~ msgid "" -#~ "Tips: The username of the user on the asset to be modified. if the user " -#~ "exists, change the password; If the user does not exist, create the user." -#~ msgstr "" -#~ "提示:用户名为将要修改的资产上的用户的用户名。如果用户存在,则修改密码;如" -#~ "果用户不存在,则创建用户。" - -#~ msgid "Plan execution list" -#~ msgstr "执行列表" - -#~ msgid "Add asset to this plan" -#~ msgstr "添加资产" - -#~ msgid "Add node to this plan" -#~ msgstr "添加节点" - -#~ msgid "" -#~ "When the user password on the asset is changed, the action is performed " -#~ "using the admin user associated with the asset" -#~ msgstr "更改资产上的用户密码时,将会使用与该资产关联的管理用户进行操作" - -#~ msgid "Length" -#~ msgstr "长度" - -#~ msgid "Run plan manually" -#~ msgstr "手动执行计划" - -#~ msgid "Execute failed" -#~ msgstr "执行失败" - -#~ msgid "Execution list of plan" -#~ msgstr "执行历史列表" - -#~ msgid "Log" -#~ msgstr "日志" - -#~ msgid "Retry" -#~ msgstr "重试" - -#~ msgid "Run failed" -#~ msgstr "执行失败" - -#~ msgid "Create plan" -#~ msgstr "创建计划" - -#~ msgid "Plan list" -#~ msgstr "计划列表" - -#~ msgid "Update plan" -#~ msgstr "更新计划" - -#~ msgid "Plan execution task list" -#~ msgstr "执行任务列表" - -#~ msgid "Select account" -#~ msgstr "选择账户" - -#~ msgid "Select regions" -#~ msgstr "选择地域" - -#~ msgid "Select instances" -#~ msgstr "选择实例" - -#~ msgid "Select node" -#~ msgstr "选择节点" - -#~ msgid "Select admins" -#~ msgstr "选择管理员" - -#~ msgid "Account detail" -#~ msgstr "账户详情" - -#~ msgid "Create account" -#~ msgstr "创建账户" - -#~ msgid "Node & AdminUser" -#~ msgstr "节点 & 管理用户" - -#~ msgid "Load failed" -#~ msgstr "加载失败" - -#~ msgid "Sync task detail" -#~ msgstr "同步任务详情" - -#~ msgid "Sync task history" -#~ msgstr "同步历史列表" - -#~ msgid "Sync instance list" -#~ msgstr "同步实例列表" - -#~ msgid "Run task manually" -#~ msgstr "手动执行任务" - -#~ msgid "Sync success" -#~ msgstr "同步成功" - -#~ msgid "Unsync count" -#~ msgstr "未同步" - -#~ msgid "Synced count" -#~ msgstr "已同步" - -#~ msgid "Released count" -#~ msgstr "已释放" - -#~ msgid "Delete released assets" -#~ msgstr "删除已释放的资产" - -#~ msgid "Create sync instance task" -#~ msgstr "创建同步实例任务" - -#~ msgid "Run count" -#~ msgstr "执行次数" - -#~ msgid "Update account" -#~ msgstr "更新账户" - -#~ msgid "Sync instance task list" -#~ msgstr "同步实例任务列表" - -#~ msgid "Create sync Instance task" -#~ msgstr "创建同步实例任务" - -#~ msgid "Update sync Instance task" -#~ msgstr "更新同步实例任务" - -#~ msgid "Asset user" -#~ msgstr "资产用户" - -#~ msgid "Create task" -#~ msgstr "创建任务" - -#~ msgid "Periodic" -#~ msgstr "定时执行" - -#~ msgid "Gathered user list" -#~ msgstr "收集用户列表" - -#~ msgid "Update task" -#~ msgstr "更新任务" - -#~ msgid "" -#~ "Tips: This will be displayed on the enterprise user login page. (eg: " -#~ "Welcome to the JumpServer open source fortress)" -#~ msgstr "" -#~ "提示:将会显示在企业版用户登录页面(eg: 欢迎使用JumpServer开源堡垒机)" - -#~ msgid "" -#~ "Tips: This will be displayed on the enterprise user login page. (suggest " -#~ "image size: 492px*472px)" -#~ msgstr "提示:将会显示在企业版用户登录页面(建议图片大小为: 492*472px)" - -#~ msgid "Tips: website icon. (suggest image size: 16px*16px)" -#~ msgstr "提示:网站图标(建议图片大小为: 16px*16px)" - -#~ msgid "" -#~ "Tips: This will appear at the top left of the administration page. " -#~ "(suggest image size: 185px*55px)" -#~ msgstr "提示:将会显示在管理页面左上方(建议图片大小为: 185px*55px)" - -#~ msgid "" -#~ "Tips: This will be displayed on the enterprise user logout page. (suggest " -#~ "image size: 82px*82px)" -#~ msgstr "提示:将会显示在企业版用户退出页面(建议图片大小为:82px*82px)" - -#~ msgid "Interface setting" -#~ msgstr "界面设置" - -#~ msgid "Restore Default" -#~ msgstr "恢复默认" - -#~ msgid "This will restore default Settings of the interface !!!" -#~ msgstr "您确定要恢复默认初始化吗?" - -#~ msgid "Restore default failed." -#~ msgstr "恢复默认失败!" - -#~ msgid "Import license" -#~ msgstr "导入许可证" - -#~ msgid "License file" -#~ msgstr "许可证文件" - -#~ msgid "Please Import License" -#~ msgstr "请导入许可证" - -#~ msgid "License has expired" -#~ msgstr "许可证已经过期" - -#~ msgid "The license will at " -#~ msgstr "许可证将在 " - -#~ msgid " expired." -#~ msgstr " 过期。" - -#~ msgid "License detail" -#~ msgstr "许可证详情" - -#~ msgid "No license" -#~ msgstr "暂无许可证" - -#~ msgid "Corporation" -#~ msgstr "公司" - -#~ msgid "Edition" -#~ msgstr "版本" - -#~ msgid "Technology consulting" -#~ msgstr "技术咨询" - -#~ msgid "Consult" -#~ msgstr "咨询" - -#~ msgid "Select auditor" -#~ msgstr "选择审计员" - -#~ msgid "Admin" -#~ msgstr "管理员" - -#~ msgid "Org detail" -#~ msgstr "组织详情" - -#~ msgid "Add admin" -#~ msgstr "添加管理员" - -#~ msgid "Create organization " -#~ msgstr "创建组织" - -#~ msgid "Org list" -#~ msgstr "组织列表" - -#~ msgid "Create org" -#~ msgstr "创建组织" - -#~ msgid "Update org" -#~ msgstr "更新组织" - -#~ msgid "Vault" -#~ msgstr "密码匣子" - -#~ msgid "Import vault" -#~ msgstr "导入密码" - -#~ msgid "vault" -#~ msgstr "密码匣子" - -#~ msgid "vault list" -#~ msgstr "密码匣子" - -#~ msgid "vault create" -#~ msgstr "创建" - -#~ msgid "Org users" -#~ msgstr "组织用户" - -#~ msgid "Add auditor" -#~ msgstr "添加审计员" - -#~ msgid "Add user to organization" -#~ msgstr "添加用户" - -#~ msgid "Org user list" -#~ msgstr "组织用户列表" - -#~ msgid "Total hosts" -#~ msgstr "主机总数" - -#~ msgid "* For security, do not change {}'s password" -#~ msgstr "* 为了安全,不能修改 {} 的密码" - -#~ msgid "Assets is empty, please add the asset" -#~ msgstr "资产为空,请添加资产" - -#~ msgid "Region & Instance" -#~ msgstr "地域 & 实例" - -#~ msgid "Crontab" -#~ msgstr "Crontab" - -#~ msgid "5 * * * *" -#~ msgstr "5 * * * *" - -#~ msgid "Aliyun" -#~ msgstr "阿里云" - -#~ msgid "Qcloud" -#~ msgstr "腾讯云" - -#~ msgid "History detail of" -#~ msgstr "执行历史详情" - -#~ msgid "Assets count: {}" -#~ msgstr "资产数量" - -#~ msgid "Assets of " -#~ msgstr "资产" - -#~ msgid "MFA authentication failed" -#~ msgstr "多因子认证失败" - -#~ msgid "MFA Secondary certification" -#~ msgstr "MFA 二次认证" - -#~ msgid "Unbind" -#~ msgstr "解绑" - -#~ msgid "Bind" -#~ msgstr "绑定" - -#~ msgid "Have child node, cancel" -#~ msgstr "存在子节点,不能删除" - -#~ msgid "Add to node" -#~ msgstr "添加到节点" - -#~ msgid "Test ldap success" -#~ msgstr "连接LDAP成功" - -#~ msgid " Top 5 Active user" -#~ msgstr "活跃用户Top5" - -#~ msgid "The last time logged on to the host" -#~ msgstr "最近一次登录主机" - -#~ msgid "No assets matched related system user protocol, stop task" -#~ msgstr "没有匹配到与系统用户协议相关的资产,结束任务" - -#~ msgid "Run history" -#~ msgstr "执行历史" - -#~ msgid "Task run history" -#~ msgstr "执行历史" - -#~ msgid "Unlimited" -#~ msgstr "无限制" - -#~ msgid "Concurrent connections" -#~ msgstr "并发连接" - -#~ msgid "Update assets" -#~ msgstr "更新资产" - -#~ msgid "Browser" -#~ msgstr "浏览器" - -#~ msgid "Virtualization tools" -#~ msgstr "虚拟化工具" - -#~ msgid "Import admin user" -#~ msgstr "导入管理用户" - -#~ msgid "Import assets" -#~ msgstr "导入资产" - -#~ msgid "Import system user" -#~ msgstr "导入系统用户" - -#~ msgid "This will delete the selected System Users !!!" -#~ msgstr "删除选择系统用户" - -#~ msgid "System Users Deleted." -#~ msgstr "已被删除" - -#~ msgid "System Users Delete" -#~ msgstr "删除系统用户" - -#~ msgid "System Users Deleting failed." -#~ msgstr "系统用户删除失败" - -#~ msgid "Versions" -#~ msgstr "版本" - -#~ msgid "Import user groups" -#~ msgstr "导入用户组" - -#~ msgid "Import users" -#~ msgstr "导入用户" - -#~ msgid "" -#~ "JumpServer is an open source desktop system developed using Python and " -#~ "Django that helps Internet businesses with efficient users, assets, " -#~ "permissions, and audit management" -#~ msgstr "" -#~ "JumpServer是一款使用Python, Django开发的开源跳板机系统, 助力互联网企业高" -#~ "效 用户、资产、权限、审计 管理" - -#~ msgid "" -#~ "We are from all over the world, we have great admiration and worship for " -#~ "the spirit of open source, we have endless pursuit for perfection, " -#~ "neatness and elegance" -#~ msgstr "" -#~ "我们自五湖四海,我们对开源精神无比敬仰和崇拜,我们对完美、整洁、优雅 无止" -#~ "境的追求" - -#~ msgid "" -#~ "We focus on automatic operation and maintenance, and strive to build an " -#~ "easy-to-use, stable, safe and automatic board hopping machine, which is " -#~ "our unremitting pursuit and power" -#~ msgstr "" -#~ "专注自动化运维,努力打造 易用、稳定、安全、自动化 的跳板机, 这是我们的不懈" -#~ "的追求和动力" - -#~ msgid "Always young, always with tears in my eyes. Stay foolish Stay hungry" -#~ msgstr "永远年轻,永远热泪盈眶 stay foolish stay hungry" - -#~ msgid "Password again" -#~ msgstr "再次输入密码" - -#~ msgid "This will delete the selected groups !!!" -#~ msgstr "删除选择组" - -#~ msgid "UserGroups Deleted." -#~ msgstr "用户组删除" - -#~ msgid "UserGroups Delete" -#~ msgstr "用户组删除" - -#~ msgid "UserGroup Deleting failed." -#~ msgstr "用户组删除失败" - -#~ msgid "User Deleted." -#~ msgstr "已被删除" - -#~ msgid "Password not same" -#~ msgstr "密码不一致" - -#, fuzzy -#~| msgid "Password update" -#~ msgid "Platform update" -#~ msgstr "密码更新" - -#~ msgid "Search no entry matched in ou {}" -#~ msgstr "在ou:{}中没有匹配条目" - -#~ msgid "Date last active" -#~ msgstr "最后活跃日期" - -#~ msgid "" -#~ "Error: Account invalid (Please make sure the information such as Access " -#~ "key or Secret key is correct)" -#~ msgstr "错误:账户无效 (请确保 Access key 或 Secret key 等信息正确)" - -#~ msgid "Create succeed" -#~ msgstr "创建成功" - -#~ msgid "Tips: If there are multiple hosts, separate them with a comma (,)" -#~ msgstr "提示: 如果有多台主机,请使用逗号 ( , ) 进行分割" - -#~ msgid "OSS: http://{REGION_NAME}.aliyuncs.com" -#~ msgstr "OSS: http://{REGION_NAME}.aliyuncs.com" - -#~ msgid "Example: http://oss-cn-hangzhou.aliyuncs.com" -#~ msgstr "如: http://oss-cn-hangzhou.aliyuncs.com" - -#~ msgid "S3: http://s3.{REGION_NAME}.amazonaws.com" -#~ msgstr "S3: http://s3.{REGION_NAME}.amazonaws.com" - -#~ msgid "S3(China): http://s3.{REGION_NAME}.amazonaws.com.cn" -#~ msgstr "S3(中国): http://s3.{REGION_NAME}.amazonaws.com.cn" - -#~ msgid "Example: http://s3.cn-north-1.amazonaws.com.cn" -#~ msgstr "如: http://s3.cn-north-1.amazonaws.com.cn" - -#~ msgid "Beijing: cn-north-1" -#~ msgstr "北京: cn-north-1" - -#~ msgid "Ningxia: cn-northwest-1" -#~ msgstr "宁夏: cn-northwest-1" - -#~ msgid "More" -#~ msgstr "更多" - -#~ msgid "Submitting" -#~ msgstr "提交中" - -#~ msgid "Endpoint need contain protocol, ex: http" -#~ msgstr "端点需要包含协议,如 http" - -#~ msgid "Delete failed" -#~ msgstr "删除失败" - -#~ msgid "The connection fails" -#~ msgstr "连接失败" - -#~ msgid "Assigned ticket" -#~ msgstr "处理人" - -#~ msgid "My ticket" -#~ msgstr "我的工单" - -#~ msgid "User login confirm: {}" -#~ msgstr "用户登录复核: {}" - -#~ msgid "" -#~ "User: {}\n" -#~ "IP: {}\n" -#~ "City: {}\n" -#~ "Date: {}\n" -#~ msgstr "" -#~ "用户: {}\n" -#~ "IP: {}\n" -#~ "城市: {}\n" -#~ "日期: {}\n" - -#~ msgid "this order" -#~ msgstr "这个工单" - -#~ msgid "Approve selected" -#~ msgstr "同意所选" - -#~ msgid "" -#~ "\n" -#~ "
\n" -#~ "

Your has a new ticket

\n" -#~ "
\n" -#~ " Title: {ticket.title}\n" -#~ "
\n" -#~ " User: {user}\n" -#~ "
\n" -#~ " Assignees: {ticket.assignees_display}\n" -#~ "
\n" -#~ " City: {ticket.city}\n" -#~ "
\n" -#~ " IP: {ticket.ip}\n" -#~ "
\n" -#~ " click here to review \n" -#~ "
\n" -#~ "
\n" -#~ " " -#~ msgstr "" -#~ "\n" -#~ "
\n" -#~ "

您有一个新工单

\n" -#~ "
\n" -#~ " 标题: {ticket.title}\n" -#~ "
\n" -#~ " 用户: {user}\n" -#~ "
\n" -#~ " 待处理人: {ticket.assignees_display}\n" -#~ "
\n" -#~ " 城市: {ticket.city}\n" -#~ "
\n" -#~ " IP: {ticket.ip}\n" -#~ "
\n" -#~ " 点我查看 \n" -#~ "
\n" -#~ "
\n" -#~ " " - -#~ msgid "Login confirm ticket list" -#~ msgstr "登录复核工单列表" - -#~ msgid "Login confirm ticket detail" -#~ msgstr "登录复核工单详情" - -#, fuzzy -#~| msgid "Login" -#~ msgid "Login IP" -#~ msgstr "登录" - -#~ msgid "succeed: {} failed: {} total: {}" -#~ msgstr "成功:{} 失败:{} 总数:{}" - -#~ msgid "The user source is not LDAP" -#~ msgstr "用户来源不是LDAP" - -#~ msgid "selected" -#~ msgstr "所选" - -#~ msgid "Log in frequently and try again later" -#~ msgstr "登录频繁, 稍后重试" - -#~ msgid "Please carry seed value and conduct MFA secondary certification" -#~ msgstr "请携带seed值, 进行MFA二次认证" - -#~ msgid "Please verify the user name and password first" -#~ msgstr "请先进行用户名和密码验证" - -#~ msgid "MFA certification failed" -#~ msgstr "MFA认证失败" - -#~ msgid "Accepted" -#~ msgstr "已接受" - -#~ msgid "New order" -#~ msgstr "新工单" - -#~ msgid "Orders" -#~ msgstr "工单管理" - -#~ msgid "" -#~ "The username or password you entered is incorrect, please enter it again." -#~ msgstr "您输入的用户名或密码不正确,请重新输入。" - -#~ msgid "" -#~ "You can also try {times_try} times (The account will be temporarily " -#~ "locked for {block_time} minutes)" -#~ msgstr "您还可以尝试 {times_try} 次(账号将被临时锁定 {block_time} 分钟)" - -#~ msgid "No order found or order expired" -#~ msgstr "没有找到工单,或者已过期" - -#~ msgid "Order was rejected by {}" -#~ msgstr "工单被拒绝 {}" - -#~ msgid "login_confirm_setting" -#~ msgstr "登录复核设置" - -#~ msgid "The user password has expired" -#~ msgstr "用户密码已过期" - -#~ msgid "Recipient" -#~ msgstr "收件人" - -#~ msgid "Gather user plan" -#~ msgstr "收集用户计划" - -#~ msgid "Task create" -#~ msgstr "创建" - -#~ msgid "%(hostname)s was %(action)s successfully" -#~ msgstr "%(hostname)s %(action)s成功" - -#~ msgid "%(name)s was %(action)s successfully" -#~ msgstr "%(name)s %(action)s成功" - -#~ msgid "Loading..." -#~ msgstr "加载中..." - -#~ msgid "Interface" -#~ msgstr "界面" - -#~ msgid "Orgs" -#~ msgstr "组织管理" - -#~ msgid "already exists" -#~ msgstr "已经存在" - -#~ msgid "Refresh all node assets amount" -#~ msgstr "刷新所有节点资产数量" - -#~ msgid "Rename failed, do not change the root node name" -#~ msgstr "重命名失败,不能更改root节点的名称" - -#~ msgid "Failed to update SSH public key." -#~ msgstr "更新密钥失败" - -#~ msgid "Unreachable assets" -#~ msgstr "不可达资产" - -#~ msgid "Reachable assets" -#~ msgstr "可连接资产" - -#~ msgid "Restore default successfully!" -#~ msgstr "恢复默认成功!" - -#~ msgid "The initial password has been cleared." -#~ msgstr "当前初始密码已经清除." - -#~ msgid "Clear initial password successfully." -#~ msgstr "清除初始密码成功." - -#~ msgid "User Initial Password" -#~ msgstr "用户初始密码" - -#~ msgid "Clear initial password" -#~ msgstr "清除初始密码" - -#~ msgid "This will be clear the initial password !!!" -#~ msgstr "这将会清除用户初始密码!!!" - -#~ msgid "Clear Initial Password" -#~ msgstr "清除初始密码" - -#~ msgid "Clear initial password failed." -#~ msgstr "清除初始密码失败." - -#~ msgid "INITIAL_PASSWORD" -#~ msgstr "初始化密码" - -#~ msgid "CUSTOM_PASSWORD" -#~ msgstr "自定义密码" - -#~ msgid "EMAIL_SET_PASSWORD" -#~ msgstr "邮件设置密码" - -#~ msgid "" -#~ "This will use the default initial password by the system. Please go to " -#~ "the system Settings application to set the initial password" -#~ msgstr "这将会使用系统设置的初始密码. 请先到系统设置应用里去设置初始密码." - -#~ msgid "The password cannot be the same as the initial password" -#~ msgstr "密码设置不能和系统设置的初始密码一致" - -#~ msgid "Update user groups" -#~ msgstr "更新用户组" - -#~ msgid "Template" -#~ msgstr "模板" - -#~ msgid "Asset csv file" -#~ msgstr "资产csv文件" - -#~ msgid "If set id, will use this id update asset existed" -#~ msgstr "如果设置了id,则会使用该行信息更新该id的资产" - -#~ msgid "Users csv file" -#~ msgstr "用户csv文件" - -#~ msgid "If set id, will use this id update user existed" -#~ msgstr "如果设置了id,则会使用该行信息更新该id的用户" - -#~ msgid "Beijing Duizhan Tech, Inc." -#~ msgstr "北京堆栈科技有限公司" - -#~ msgid "" -#~ "Import {} users successfully;import {} users failed, Because’TypeError' " -#~ "object has no attribute 'keys'" -#~ msgstr "导入 {} 个用户成功; 导入 {} 这些用户失败,因为对象没有属性'keys'" - -#~ msgid "Invalid private key" -#~ msgstr "SSH密钥不合法" - -#~ msgid "Login JumpServer" -#~ msgstr "登录 JumpServer" - -#~ msgid "Valid" -#~ msgstr "账户状态" - -#~ msgid "Asset has been disabled, skip: {}" -#~ msgstr "资产被禁用,跳过:{}" - -#~ msgid "Asset may not be support ansible, skip: {}" -#~ msgstr "资产或许不支持ansible, 跳过: {}" - -#~ msgid "No assets, task stop" -#~ msgstr "没有匹配到资产,结束任务" - -#~ msgid "Update assets hardware info period" -#~ msgstr "定期更新资产硬件信息" - -#~ msgid "User id" -#~ msgstr "用户" - -#~ msgid "Start execute" -#~ msgstr "开始执行" - -#~ msgid "Start" -#~ msgstr "开始" - -#~ msgid "Bit" -#~ msgstr " 位" - -#~ msgid "" -#~ "Set terminal storage setting, `default` is the using as default,You can " -#~ "set other storage and some terminal using" -#~ msgstr "设置终端命令存储,default是默认用的存储方式" - -#~ msgid "" -#~ "Set replay storage setting, `default` is the using as default,You can set " -#~ "other storage and some terminal using" -#~ msgstr "设置终端录像存储,default是默认用的存储方式" - -#~ msgid "Sync instance task detail" -#~ msgstr "同步实例任务详情" - -#~ msgid "Sync task instance" -#~ msgstr "同步实例列表" - -#~ msgid "Sync instance task instance" -#~ msgstr "同步实例任务实例" - -#~ msgid "Get sync task error" -#~ msgstr "获取同步任务" - -#~ msgid "" -#~ "Are you sure to remove authentication information for the system user ?" -#~ msgstr "你确定清除该系统用户的认证信息吗 ?" - -#~ msgid "success" -#~ msgstr "成功" - -#~ msgid "Task has been send, Go to ops task list seen result" -#~ msgstr "任务已下发,查看ops任务列表" - -#~ msgid "Task has been send, seen left assets status" -#~ msgstr "任务已下发,查看左侧资产状态" - -#~ msgid "OK" -#~ msgstr "完成" - -#~ msgid "Update MFA settings" -#~ msgstr "更改MFA设置" - -#~ msgid "MFA code invalid" -#~ msgstr "MFA码认证失败" - -#~ msgid "Chinese" -#~ msgstr "中文" - -#~ msgid "* required Must set exact system platform, Windows, Linux ..." -#~ msgstr "* required 必须准确设置操作系统平台,如Windows, Linux ..." - -#~ msgid "Clear" -#~ msgstr "清除" diff --git a/apps/ops/ansible/callback.py b/apps/ops/ansible/callback.py index adba3e81b..8edbbbcb7 100644 --- a/apps/ops/ansible/callback.py +++ b/apps/ops/ansible/callback.py @@ -60,6 +60,10 @@ class CallbackMixin: self.results_raw[t][host][task_name] = task_result self.clean_result(t, host, task_name, task_result) + def close(self): + if hasattr(self._display, 'close'): + self._display.close() + class AdHocResultCallback(CallbackMixin, CallbackModule, CMDCallBackModule): """ diff --git a/apps/ops/ansible/display.py b/apps/ops/ansible/display.py new file mode 100644 index 000000000..b272f6a6d --- /dev/null +++ b/apps/ops/ansible/display.py @@ -0,0 +1,66 @@ +import errno +import sys +import os + +from ansible.utils.display import Display +from ansible.utils.color import stringc +from ansible.utils.singleton import Singleton + +from .utils import get_ansible_task_log_path + + +class UnSingleton(Singleton): + def __init__(cls, name, bases, dct): + type.__init__(cls, name, bases, dct) + + def __call__(cls, *args, **kwargs): + return type.__call__(cls, *args, **kwargs) + + +class AdHocDisplay(Display, metaclass=UnSingleton): + def __init__(self, execution_id, verbosity=0): + super().__init__(verbosity=verbosity) + if execution_id: + log_path = get_ansible_task_log_path(execution_id) + else: + log_path = os.devnull + self.log_file = open(log_path, mode='a') + + def close(self): + self.log_file.close() + + def set_cowsay_info(self): + # 中断 cowsay 的测试,会频繁开启子进程 + return + + def _write_to_screen(self, msg, stderr): + if not stderr: + screen = sys.stdout + else: + screen = sys.stderr + + screen.write(msg) + + try: + screen.flush() + except IOError as e: + # Ignore EPIPE in case fileobj has been prematurely closed, eg. + # when piping to "head -n1" + if e.errno != errno.EPIPE: + raise + + def _write_to_log_file(self, msg): + # 这里先不 flush,log 文件不需要那么及时。 + self.log_file.write(msg) + + def display(self, msg, color=None, stderr=False, screen_only=False, log_only=False): + if color: + msg = stringc(msg, color) + + if not msg.endswith(u'\n'): + msg2 = msg + u'\n' + else: + msg2 = msg + + self._write_to_screen(msg2, stderr) + self._write_to_log_file(msg2) diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py index e741d8f8b..fdbed74cb 100644 --- a/apps/ops/ansible/runner.py +++ b/apps/ops/ansible/runner.py @@ -1,6 +1,7 @@ # ~*~ coding: utf-8 ~*~ import os + import shutil from collections import namedtuple @@ -18,6 +19,7 @@ from .callback import ( ) from common.utils import get_logger from .exceptions import AnsibleError +from .display import AdHocDisplay __all__ = ["AdHocRunner", "PlayBookRunner", "CommandRunner"] @@ -130,8 +132,8 @@ class AdHocRunner: loader=self.loader, inventory=self.inventory ) - def get_result_callback(self, file_obj=None): - return self.__class__.results_callback_class() + def get_result_callback(self, execution_id=None): + return self.__class__.results_callback_class(display=AdHocDisplay(execution_id)) @staticmethod def check_module_args(module_name, module_args=''): @@ -189,7 +191,7 @@ class AdHocRunner: 'ssh_args': '-C -o ControlMaster=no' } - def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no'): + def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no', execution_id=None): """ :param tasks: [{'action': {'module': 'shell', 'args': 'ls'}, ...}, ] :param pattern: all, *, or others @@ -198,7 +200,7 @@ class AdHocRunner: :return: """ self.check_pattern(pattern) - self.results_callback = self.get_result_callback() + self.results_callback = self.get_result_callback(execution_id) cleaned_tasks = self.clean_tasks(tasks) self.set_control_master_if_need(cleaned_tasks) context.CLIARGS = ImmutableDict(self.options) @@ -233,6 +235,8 @@ class AdHocRunner: tqm.cleanup() shutil.rmtree(C.DEFAULT_LOCAL_TMP, True) + self.results_callback.close() + class CommandRunner(AdHocRunner): results_callback_class = CommandResultCallback diff --git a/apps/ops/ansible/utils.py b/apps/ops/ansible/utils.py new file mode 100644 index 000000000..478badc56 --- /dev/null +++ b/apps/ops/ansible/utils.py @@ -0,0 +1,6 @@ +from django.conf import settings + + +def get_ansible_task_log_path(task_id): + from ops.utils import get_task_log_path + return get_task_log_path(settings.ANSIBLE_LOG_DIR, task_id, level=3) diff --git a/apps/ops/api/adhoc.py b/apps/ops/api/adhoc.py index 586415c20..4845d49dc 100644 --- a/apps/ops/api/adhoc.py +++ b/apps/ops/api/adhoc.py @@ -7,7 +7,7 @@ from rest_framework.views import Response from common.drf.api import JMSBulkModelViewSet from common.permissions import IsOrgAdmin -from common.serializers import CeleryTaskSerializer +from common.drf.serializers import CeleryTaskSerializer from ..models import Task, AdHoc, AdHocExecution from ..serializers import ( TaskSerializer, @@ -25,8 +25,8 @@ __all__ = [ class TaskViewSet(JMSBulkModelViewSet): queryset = Task.objects.all() - filter_fields = ("name",) - search_fields = filter_fields + filterset_fields = ("name",) + search_fields = filterset_fields serializer_class = TaskSerializer permission_classes = (IsOrgAdmin,) diff --git a/apps/ops/api/celery.py b/apps/ops/api/celery.py index 814835465..3968b39b7 100644 --- a/apps/ops/api/celery.py +++ b/apps/ops/api/celery.py @@ -15,10 +15,14 @@ from common.api import LogTailApi from ..models import CeleryTask from ..serializers import CeleryResultSerializer, CeleryPeriodTaskSerializer from ..celery.utils import get_celery_task_log_path +from ..ansible.utils import get_ansible_task_log_path from common.mixins.api import CommonApiMixin -__all__ = ['CeleryTaskLogApi', 'CeleryResultApi', 'CeleryPeriodTaskViewSet'] +__all__ = [ + 'CeleryTaskLogApi', 'CeleryResultApi', 'CeleryPeriodTaskViewSet', + 'AnsibleTaskLogApi', +] class CeleryTaskLogApi(LogTailApi): @@ -57,6 +61,21 @@ class CeleryTaskLogApi(LogTailApi): return _('Waiting task start') +class AnsibleTaskLogApi(LogTailApi): + permission_classes = (IsValidUser,) + + def get_log_path(self): + new_path = get_ansible_task_log_path(self.kwargs.get('pk')) + if new_path and os.path.isfile(new_path): + return new_path + + def get_no_file_message(self, request): + if self.mark == 'undefined': + return '.' + else: + return _('Waiting task start') + + class CeleryResultApi(generics.RetrieveAPIView): permission_classes = (IsValidUser,) serializer_class = CeleryResultSerializer diff --git a/apps/ops/celery/utils.py b/apps/ops/celery/utils.py index ff5aeb1d4..41f99212f 100644 --- a/apps/ops/celery/utils.py +++ b/apps/ops/celery/utils.py @@ -102,11 +102,8 @@ def get_celery_periodic_task(task_name): def get_celery_task_log_path(task_id): - task_id = str(task_id) - rel_path = os.path.join(task_id[0], task_id[1], task_id + '.log') - path = os.path.join(settings.CELERY_LOG_DIR, rel_path) - os.makedirs(os.path.dirname(path), exist_ok=True) - return path + from ops.utils import get_task_log_path + return get_task_log_path(settings.CELERY_LOG_DIR, task_id) def get_celery_status(): diff --git a/apps/ops/migrations/0019_adhocexecution_celery_task_id.py b/apps/ops/migrations/0019_adhocexecution_celery_task_id.py new file mode 100644 index 000000000..4789267e6 --- /dev/null +++ b/apps/ops/migrations/0019_adhocexecution_celery_task_id.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-12-30 12:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0018_auto_20200509_1434'), + ] + + operations = [ + migrations.AddField( + model_name='adhocexecution', + name='celery_task_id', + field=models.UUIDField(default=None, null=True), + ), + ] diff --git a/apps/ops/models/adhoc.py b/apps/ops/models/adhoc.py index aa2884105..63050e436 100644 --- a/apps/ops/models/adhoc.py +++ b/apps/ops/models/adhoc.py @@ -179,13 +179,13 @@ class AdHoc(OrgModelMixin): def run(self): try: - hid = current_task.request.id - if AdHocExecution.objects.filter(id=hid).exists(): - hid = uuid.uuid4() + celery_task_id = current_task.request.id except AttributeError: - hid = uuid.uuid4() + celery_task_id = None + execution = AdHocExecution( - id=hid, adhoc=self, task=self.task, + celery_task_id=celery_task_id, + adhoc=self, task=self.task, task_display=str(self.task)[:128], date_start=timezone.now(), hosts_amount=self.hosts.count(), @@ -237,6 +237,7 @@ class AdHocExecution(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) task = models.ForeignKey(Task, related_name='execution', on_delete=models.SET_NULL, null=True) task_display = models.CharField(max_length=128, blank=True, default='', verbose_name=_("Task display")) + celery_task_id = models.UUIDField(default=None, null=True) hosts_amount = models.IntegerField(default=0, verbose_name=_("Host amount")) adhoc = models.ForeignKey(AdHoc, related_name='execution', on_delete=models.SET_NULL, null=True) date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Start time')) @@ -270,6 +271,7 @@ class AdHocExecution(OrgModelMixin): self.adhoc.tasks, self.adhoc.pattern, self.task.name, + execution_id=self.id ) return result.results_raw, result.results_summary except AnsibleError as e: diff --git a/apps/ops/urls/api_urls.py b/apps/ops/urls/api_urls.py index 15010923b..a5838073f 100644 --- a/apps/ops/urls/api_urls.py +++ b/apps/ops/urls/api_urls.py @@ -22,6 +22,8 @@ urlpatterns = [ path('tasks//run/', api.TaskRun.as_view(), name='task-run'), path('celery/task//log/', api.CeleryTaskLogApi.as_view(), name='celery-task-log'), path('celery/task//result/', api.CeleryResultApi.as_view(), name='celery-result'), + + path('ansible/task//log/', api.AnsibleTaskLogApi.as_view(), name='ansible-task-log'), ] urlpatterns += router.urls diff --git a/apps/ops/urls/ws_urls.py b/apps/ops/urls/ws_urls.py index 5b6bd74d2..e1d44dd7c 100644 --- a/apps/ops/urls/ws_urls.py +++ b/apps/ops/urls/ws_urls.py @@ -5,5 +5,5 @@ from .. import ws app_name = 'ops' urlpatterns = [ - path('ws/ops/tasks/log/', ws.CeleryLogWebsocket, name='task-log-ws'), + path('ws/ops/tasks/log/', ws.TaskLogWebsocket, name='task-log-ws'), ] diff --git a/apps/ops/utils.py b/apps/ops/utils.py index 04d35a2fa..d8c186dd6 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -1,4 +1,6 @@ # ~*~ coding: utf-8 ~*~ +import os + from django.utils.translation import ugettext_lazy as _ from common.utils import get_logger, get_object_or_none @@ -75,3 +77,10 @@ def send_server_performance_mail(path, usage, usages): send_mail_async(subject, message, recipient_list, html_message=message) +def get_task_log_path(base_path, task_id, level=2): + task_id = str(task_id) + rel_path = os.path.join(*task_id[:level], task_id + '.log') + path = os.path.join(base_path, rel_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + return path + diff --git a/apps/ops/ws.py b/apps/ops/ws.py index 343a6a8ee..f21c79570 100644 --- a/apps/ops/ws.py +++ b/apps/ops/ws.py @@ -6,25 +6,41 @@ import json from common.utils import get_logger from .celery.utils import get_celery_task_log_path +from .ansible.utils import get_ansible_task_log_path from channels.generic.websocket import JsonWebsocketConsumer logger = get_logger(__name__) -class CeleryLogWebsocket(JsonWebsocketConsumer): +class TaskLogWebsocket(JsonWebsocketConsumer): disconnected = False + log_types = { + 'celery': get_celery_task_log_path, + 'ansible': get_ansible_task_log_path + } + def connect(self): - self.accept() + user = self.scope["user"] + if user.is_authenticated and user.is_org_admin: + self.accept() + else: + self.close() + + def get_log_path(self, task_id): + func = self.log_types.get(self.log_type) + if func: + return func(task_id) def receive(self, text_data=None, bytes_data=None, **kwargs): data = json.loads(text_data) - task_id = data.get("task") + task_id = data.get('task') + self.log_type = data.get('type', 'celery') if task_id: self.handle_task(task_id) def wait_util_log_path_exist(self, task_id): - log_path = get_celery_task_log_path(task_id) + log_path = self.get_log_path(task_id) while not self.disconnected: if not os.path.exists(log_path): self.send_json({'message': '.', 'task': task_id}) @@ -70,5 +86,3 @@ class CeleryLogWebsocket(JsonWebsocketConsumer): def disconnect(self, close_code): self.disconnected = True self.close() - - diff --git a/apps/orgs/api.py b/apps/orgs/api.py index 077b7eba7..1a6545f0b 100644 --- a/apps/orgs/api.py +++ b/apps/orgs/api.py @@ -27,7 +27,7 @@ logger = get_logger(__file__) class OrgViewSet(BulkModelViewSet): - filter_fields = ('name',) + filterset_fields = ('name',) search_fields = ('name', 'comment') queryset = Organization.objects.all() serializer_class = OrgSerializer @@ -75,11 +75,6 @@ class OrgMemberRelationBulkViewSet(JMSBulkRelationModelViewSet): filterset_class = OrgMemberRelationFilterSet search_fields = ('user__name', 'user__username', 'org__name') - def perform_bulk_create(self, serializer): - data = serializer.validated_data - relations = [OrganizationMember(**i) for i in data] - OrganizationMember.objects.bulk_create(relations, ignore_conflicts=True) - def perform_bulk_destroy(self, queryset): objs = list(queryset.all().prefetch_related('user', 'org')) queryset.delete() diff --git a/apps/orgs/cache.py b/apps/orgs/cache.py new file mode 100644 index 000000000..c77dd988f --- /dev/null +++ b/apps/orgs/cache.py @@ -0,0 +1,34 @@ +from django.db.transaction import on_commit + +from common.cache import * +from .utils import current_org, tmp_to_org +from .tasks import refresh_org_cache_task +from orgs.models import Organization + + +class OrgRelatedCache(Cache): + + def __init__(self): + super().__init__() + self.current_org = Organization.get_instance(current_org.id) + + def get_current_org(self): + """ + 暴露给子类控制组织的回调 + 1. 在交互式环境下能控制组织 + 2. 在 celery 任务下能控制组织 + """ + return self.current_org + + def refresh(self, *fields): + with tmp_to_org(self.get_current_org()): + return super().refresh(*fields) + + def refresh_async(self, *fields): + """ + 在事务提交之后再发送信号,防止因事务的隔离性导致未获得最新的数据 + """ + def func(): + logger.info(f'CACHE: Send refresh task {self}.{fields}') + refresh_org_cache_task.delay(self, *fields) + on_commit(func) diff --git a/apps/orgs/caches.py b/apps/orgs/caches.py new file mode 100644 index 000000000..462b13594 --- /dev/null +++ b/apps/orgs/caches.py @@ -0,0 +1,46 @@ +from .cache import OrgRelatedCache, IntegerField +from users.models import UserGroup, User +from assets.models import Node, AdminUser, SystemUser, Domain, Gateway +from applications.models import Application +from perms.models import AssetPermission, ApplicationPermission +from .models import OrganizationMember + + +class OrgResourceStatisticsCache(OrgRelatedCache): + users_amount = IntegerField() + groups_amount = IntegerField(queryset=UserGroup.objects) + + assets_amount = IntegerField() + nodes_amount = IntegerField(queryset=Node.objects) + admin_users_amount = IntegerField(queryset=AdminUser.objects) + system_users_amount = IntegerField(queryset=SystemUser.objects) + domains_amount = IntegerField(queryset=Domain.objects) + gateways_amount = IntegerField(queryset=Gateway.objects) + + applications_amount = IntegerField(queryset=Application.objects) + + asset_perms_amount = IntegerField(queryset=AssetPermission.objects) + app_perms_amount = IntegerField(queryset=ApplicationPermission.objects) + + def __init__(self, org): + super().__init__() + self.org = org + + def get_key_suffix(self): + return f'' + + def get_current_org(self): + return self.org + + def compute_users_amount(self): + if self.org.is_real(): + users_amount = OrganizationMember.objects.values( + 'user_id' + ).filter(org_id=self.org.id).distinct().count() + else: + users_amount = User.objects.all().distinct().count() + return users_amount + + def compute_assets_amount(self): + node = Node.org_root() + return node.assets_amount diff --git a/apps/orgs/models.py b/apps/orgs/models.py index 715067b1c..1fbae50ce 100644 --- a/apps/orgs/models.py +++ b/apps/orgs/models.py @@ -7,7 +7,7 @@ from django.db.models import signals from django.db.models import Q from django.utils.translation import ugettext_lazy as _ -from common.utils import is_uuid +from common.utils import is_uuid, lazyproperty from common.const import choices from common.db.models import ChoiceSet @@ -215,6 +215,33 @@ class Organization(models.Model): from .utils import set_current_org set_current_org(self) + @lazyproperty + def resource_statistics_cache(self): + from .caches import OrgResourceStatisticsCache + return OrgResourceStatisticsCache(self) + + def get_total_resources_amount(self): + from django.apps import apps + from orgs.mixins.models import OrgModelMixin + summary = {'users.Members': self.members.all().count()} + for app_name, app_config in apps.app_configs.items(): + models_cls = app_config.get_models() + for model in models_cls: + if not issubclass(model, OrgModelMixin): + continue + key = '{}.{}'.format(app_name, model.__name__) + summary[key] = self.get_resource_amount(model) + return summary + + def get_resource_amount(self, resource_model): + from .utils import tmp_to_org + from .mixins.models import OrgModelMixin + + if not issubclass(resource_model, OrgModelMixin): + return 0 + with tmp_to_org(self): + return resource_model.objects.all().count() + def _convert_to_uuid_set(users): rst = set() diff --git a/apps/orgs/serializers.py b/apps/orgs/serializers.py index c4402d433..d10963531 100644 --- a/apps/orgs/serializers.py +++ b/apps/orgs/serializers.py @@ -4,24 +4,43 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from users.models.user import User -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from common.drf.serializers import BulkModelSerializer from common.db.models import concated_display as display from .models import Organization, OrganizationMember, ROLE +class ResourceStatisticsSerializer(serializers.Serializer): + users_amount = serializers.IntegerField(required=False) + groups_amount = serializers.IntegerField(required=False) + + assets_amount = serializers.IntegerField(required=False) + nodes_amount = serializers.IntegerField(required=False) + admin_users_amount = serializers.IntegerField(required=False) + system_users_amount = serializers.IntegerField(required=False) + domains_amount = serializers.IntegerField(required=False) + gateways_amount = serializers.IntegerField(required=False) + + applications_amount = serializers.IntegerField(required=False) + asset_perms_amount = serializers.IntegerField(required=False) + app_perms_amount = serializers.IntegerField(required=False) + + class OrgSerializer(ModelSerializer): users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False) admins = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False) auditors = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False) + resource_statistics = ResourceStatisticsSerializer(source='resource_statistics_cache') + class Meta: model = Organization list_serializer_class = AdaptedBulkListSerializer fields_mini = ['id', 'name'] fields_small = fields_mini + [ - 'created_by', 'date_created', 'comment' + 'created_by', 'date_created', 'comment', 'resource_statistics' ] + fields_m2m = ['users', 'admins', 'auditors'] fields = fields_small + fields_m2m read_only_fields = ['created_by', 'date_created'] @@ -60,6 +79,8 @@ class OrgMemberSerializer(BulkModelSerializer): class Meta: model = OrganizationMember fields = ('id', 'org', 'user', 'role', 'org_display', 'user_display', 'role_display') + use_model_bulk_create = True + model_bulk_create_kwargs = {'ignore_conflicts': True} def get_unique_together_validators(self): if self.parent: diff --git a/apps/orgs/signals_handler.py b/apps/orgs/signals_handler.py index adfd9d373..5f918ee21 100644 --- a/apps/orgs/signals_handler.py +++ b/apps/orgs/signals_handler.py @@ -4,7 +4,7 @@ from collections import defaultdict from functools import partial from django.db.models.signals import m2m_changed -from django.db.models.signals import post_save +from django.db.models.signals import post_save, pre_delete from django.dispatch import receiver from orgs.utils import tmp_to_org @@ -12,7 +12,10 @@ from .models import Organization, OrganizationMember from .hands import set_current_org, Node, get_current_org from perms.models import (AssetPermission, ApplicationPermission) from users.models import UserGroup, User -from common.const.signals import PRE_REMOVE, POST_REMOVE +from applications.models import Application +from assets.models import Asset, AdminUser, SystemUser, Domain, Gateway +from common.const.signals import PRE_REMOVE, POST_REMOVE, POST_PREFIX +from .caches import OrgResourceStatisticsCache @receiver(post_save, sender=Organization) @@ -106,3 +109,72 @@ def on_org_user_changed(action, instance, reverse, pk_set, **kwargs): leaved_users = set(pk_set) - set(org.members.filter(id__in=user_pk_set).values_list('id', flat=True)) _clear_users_from_org(org, leaved_users) + + +# 缓存相关 +# ----------------------------------------------------- + +def refresh_user_amount_on_user_create_or_delete(user_id): + orgs = Organization.objects.filter(m2m_org_members__user_id=user_id).distinct() + for org in orgs: + org_cache = OrgResourceStatisticsCache(org) + org_cache.refresh_async('users_amount') + + +@receiver(post_save, sender=User) +def on_user_create(sender, instance, created, **kwargs): + if created: + refresh_user_amount_on_user_create_or_delete(instance.id) + + +@receiver(pre_delete, sender=User) +def on_user_delete(sender, instance, **kwargs): + refresh_user_amount_on_user_create_or_delete(instance.id) + + +@receiver(m2m_changed, sender=OrganizationMember) +def on_org_user_changed(sender, action, instance, reverse, pk_set, **kwargs): + if not action.startswith(POST_PREFIX): + return + + if reverse: + orgs = Organization.objects.filter(id__in=pk_set) + else: + orgs = [instance] + + for org in orgs: + org_cache = OrgResourceStatisticsCache(org) + org_cache.refresh_async('users_amount') + + +class OrgResourceStatisticsRefreshUtil: + model_cache_field_mapper = { + ApplicationPermission: 'app_perms_amount', + AssetPermission: 'asset_perms_amount', + Application: 'applications_amount', + Gateway: 'gateways_amount', + Domain: 'domains_amount', + SystemUser: 'system_users_amount', + AdminUser: 'admin_users_amount', + Node: 'nodes_amount', + Asset: 'assets_amount', + UserGroup: 'groups_amount', + } + + @classmethod + def refresh_if_need(cls, instance): + cache_field_name = cls.model_cache_field_mapper.get(type(instance)) + if cache_field_name: + org_cache = OrgResourceStatisticsCache(instance.org) + org_cache.refresh_async(cache_field_name) + + +@receiver(post_save) +def on_post_save_refresh_org_resource_statistics_cache(sender, instance, created, **kwargs): + if created: + OrgResourceStatisticsRefreshUtil.refresh_if_need(instance) + + +@receiver(pre_delete) +def on_pre_delete_refresh_org_resource_statistics_cache(sender, instance, **kwargs): + OrgResourceStatisticsRefreshUtil.refresh_if_need(instance) diff --git a/apps/orgs/tasks.py b/apps/orgs/tasks.py new file mode 100644 index 000000000..a33456913 --- /dev/null +++ b/apps/orgs/tasks.py @@ -0,0 +1,11 @@ +from celery import shared_task + +from common.utils import get_logger + +logger = get_logger(__file__) + + +@shared_task +def refresh_org_cache_task(cache, *fields): + logger.info(f'CACHE: refresh {cache.key}.{fields}') + cache.refresh(*fields) diff --git a/apps/perms/api/__init__.py b/apps/perms/api/__init__.py index 50957c154..d9a7afe0f 100644 --- a/apps/perms/api/__init__.py +++ b/apps/perms/api/__init__.py @@ -3,15 +3,4 @@ from .asset import * from .application import * - -# TODO: 删除 -from .remote_app_permission import * -from .remote_app_permission_relation import * -from .user_remote_app_permission import * -from .database_app_permission import * -from .database_app_permission_relation import * -from .user_database_app_permission import * from .system_user_permission import * -from .k8s_app_permission import * -from .k8s_app_permission_relation import * -from .user_k8s_app_permission import * diff --git a/apps/perms/api/application/application_permission.py b/apps/perms/api/application/application_permission.py index bbad79d26..e7b449b5a 100644 --- a/apps/perms/api/application/application_permission.py +++ b/apps/perms/api/application/application_permission.py @@ -12,8 +12,8 @@ class ApplicationPermissionViewSet(BasePermissionViewSet): """ model = ApplicationPermission serializer_class = serializers.ApplicationPermissionSerializer - filter_fields = ['name', 'category', 'type'] - search_fields = filter_fields + filterset_fields = ['name', 'category', 'type'] + search_fields = filterset_fields custom_filter_fields = BasePermissionViewSet.custom_filter_fields + [ 'application_id', 'application' ] diff --git a/apps/perms/api/application/application_permission_relation.py b/apps/perms/api/application/application_permission_relation.py index 70d7e66e1..770463f7e 100644 --- a/apps/perms/api/application/application_permission_relation.py +++ b/apps/perms/api/application/application_permission_relation.py @@ -37,7 +37,7 @@ class ApplicationPermissionUserRelationViewSet(RelationMixin): serializer_class = serializers.ApplicationPermissionUserRelationSerializer m2m_field = models.ApplicationPermission.users.field permission_classes = (IsOrgAdmin,) - filter_fields = [ + filterset_fields = [ 'id', "user", "applicationpermission", ] search_fields = ("user__name", "user__username", "applicationpermission__name") @@ -52,7 +52,7 @@ class ApplicationPermissionUserGroupRelationViewSet(RelationMixin): serializer_class = serializers.ApplicationPermissionUserGroupRelationSerializer m2m_field = models.ApplicationPermission.user_groups.field permission_classes = (IsOrgAdmin,) - filter_fields = [ + filterset_fields = [ 'id', "usergroup", "applicationpermission" ] search_fields = ["usergroup__name", "applicationpermission__name"] @@ -67,7 +67,7 @@ class ApplicationPermissionApplicationRelationViewSet(RelationMixin): serializer_class = serializers.ApplicationPermissionApplicationRelationSerializer m2m_field = models.ApplicationPermission.applications.field permission_classes = (IsOrgAdmin,) - filter_fields = [ + filterset_fields = [ 'id', 'application', 'applicationpermission', ] search_fields = ["id", "application__name", "applicationpermission__name"] @@ -82,7 +82,7 @@ class ApplicationPermissionSystemUserRelationViewSet(RelationMixin): serializer_class = serializers.ApplicationPermissionSystemUserRelationSerializer m2m_field = models.ApplicationPermission.system_users.field permission_classes = (IsOrgAdmin,) - filter_fields = [ + filterset_fields = [ 'id', 'systemuser', 'applicationpermission', ] search_fields = [ @@ -103,8 +103,8 @@ class ApplicationPermissionAllApplicationListApi(generics.ListAPIView): permission_classes = (IsOrgAdmin,) serializer_class = serializers.ApplicationPermissionAllApplicationSerializer only_fields = serializers.ApplicationPermissionAllApplicationSerializer.Meta.only_fields - filter_fields = ('name',) - search_fields = filter_fields + filterset_fields = ('name',) + search_fields = filterset_fields def get_queryset(self): pk = self.kwargs.get('pk') @@ -118,8 +118,8 @@ class ApplicationPermissionAllUserListApi(generics.ListAPIView): permission_classes = (IsOrgAdmin,) serializer_class = serializers.ApplicationPermissionAllUserSerializer only_fields = serializers.ApplicationPermissionAllUserSerializer.Meta.only_fields - filter_fields = ('username', 'name') - search_fields = filter_fields + filterset_fields = ('username', 'name') + search_fields = filterset_fields def get_queryset(self): pk = self.kwargs.get('pk') diff --git a/apps/perms/api/application/user_group_permission.py b/apps/perms/api/application/user_group_permission.py index 4c112c183..32d116b2f 100644 --- a/apps/perms/api/application/user_group_permission.py +++ b/apps/perms/api/application/user_group_permission.py @@ -5,8 +5,8 @@ from django.db.models import Q from rest_framework.generics import ListAPIView from common.permissions import IsOrgAdminOrAppUser +from common.mixins.api import CommonApiMixin from applications.models import Application -from applications.api.mixin import ApplicationAttrsSerializerViewMixin from perms import serializers __all__ = [ @@ -14,14 +14,14 @@ __all__ = [ ] -class UserGroupGrantedApplicationsApi(ApplicationAttrsSerializerViewMixin, ListAPIView): +class UserGroupGrantedApplicationsApi(CommonApiMixin, ListAPIView): """ - 获取用户组直接授权的资产 + 获取用户组直接授权的应用 """ permission_classes = (IsOrgAdminOrAppUser,) serializer_class = serializers.ApplicationGrantedSerializer only_fields = serializers.ApplicationGrantedSerializer.Meta.only_fields - filter_fields = ['id', 'name', 'category', 'type', 'comment'] + filterset_fields = ['id', 'name', 'category', 'type', 'comment'] search_fields = ['name', 'comment'] def get_queryset(self): diff --git a/apps/perms/api/application/user_permission/user_permission_applications.py b/apps/perms/api/application/user_permission/user_permission_applications.py index 848e22304..2b8b71847 100644 --- a/apps/perms/api/application/user_permission/user_permission_applications.py +++ b/apps/perms/api/application/user_permission/user_permission_applications.py @@ -3,8 +3,9 @@ from rest_framework.generics import ListAPIView from rest_framework.response import Response +from common.mixins.api import CommonApiMixin from applications.api.mixin import ( - SerializeApplicationToTreeNodeMixin, ApplicationAttrsSerializerViewMixin + SerializeApplicationToTreeNodeMixin ) from perms import serializers from perms.api.asset.user_permission.mixin import ForAdminMixin, ForUserMixin @@ -21,10 +22,10 @@ __all__ = [ ] -class AllGrantedApplicationsMixin(ApplicationAttrsSerializerViewMixin, ListAPIView): +class AllGrantedApplicationsMixin(CommonApiMixin, ListAPIView): only_fields = serializers.ApplicationGrantedSerializer.Meta.only_fields serializer_class = serializers.ApplicationGrantedSerializer - filter_fields = ['id', 'name', 'category', 'type', 'comment'] + filterset_fields = ['id', 'name', 'category', 'type', 'comment'] search_fields = ['name', 'comment'] user: None diff --git a/apps/perms/api/asset/asset_permission.py b/apps/perms/api/asset/asset_permission.py index 0a61189a1..5062f2099 100644 --- a/apps/perms/api/asset/asset_permission.py +++ b/apps/perms/api/asset/asset_permission.py @@ -21,7 +21,7 @@ class AssetPermissionViewSet(BasePermissionViewSet): """ model = AssetPermission serializer_class = serializers.AssetPermissionSerializer - filter_fields = ['name'] + filterset_fields = ['name'] custom_filter_fields = BasePermissionViewSet.custom_filter_fields + [ 'node_id', 'node', 'asset_id', 'hostname', 'ip' ] diff --git a/apps/perms/api/asset/asset_permission_relation.py b/apps/perms/api/asset/asset_permission_relation.py index b21c12cb0..4deebeb61 100644 --- a/apps/perms/api/asset/asset_permission_relation.py +++ b/apps/perms/api/asset/asset_permission_relation.py @@ -36,7 +36,7 @@ class AssetPermissionUserRelationViewSet(RelationMixin): serializer_class = serializers.AssetPermissionUserRelationSerializer m2m_field = models.AssetPermission.users.field permission_classes = (IsOrgAdmin,) - filter_fields = [ + filterset_fields = [ 'id', "user", "assetpermission", ] search_fields = ("user__name", "user__username", "assetpermission__name") @@ -51,8 +51,8 @@ class AssetPermissionUserRelationViewSet(RelationMixin): class AssetPermissionAllUserListApi(generics.ListAPIView): permission_classes = (IsOrgAdmin,) serializer_class = serializers.AssetPermissionAllUserSerializer - filter_fields = ("username", "name") - search_fields = filter_fields + filterset_fields = ("username", "name") + search_fields = filterset_fields def get_queryset(self): pk = self.kwargs.get("pk") @@ -67,7 +67,7 @@ class AssetPermissionUserGroupRelationViewSet(RelationMixin): serializer_class = serializers.AssetPermissionUserGroupRelationSerializer m2m_field = models.AssetPermission.user_groups.field permission_classes = (IsOrgAdmin,) - filter_fields = [ + filterset_fields = [ 'id', "usergroup", "assetpermission" ] search_fields = ["usergroup__name", "assetpermission__name"] @@ -83,7 +83,7 @@ class AssetPermissionAssetRelationViewSet(RelationMixin): serializer_class = serializers.AssetPermissionAssetRelationSerializer m2m_field = models.AssetPermission.assets.field permission_classes = (IsOrgAdmin,) - filter_fields = [ + filterset_fields = [ 'id', 'asset', 'assetpermission', ] search_fields = ["id", "asset__hostname", "asset__ip", "assetpermission__name"] @@ -98,8 +98,8 @@ class AssetPermissionAssetRelationViewSet(RelationMixin): class AssetPermissionAllAssetListApi(generics.ListAPIView): permission_classes = (IsOrgAdmin,) serializer_class = serializers.AssetPermissionAllAssetSerializer - filter_fields = ("hostname", "ip") - search_fields = filter_fields + filterset_fields = ("hostname", "ip") + search_fields = filterset_fields def get_queryset(self): pk = self.kwargs.get("pk") @@ -119,7 +119,7 @@ class AssetPermissionNodeRelationViewSet(RelationMixin): serializer_class = serializers.AssetPermissionNodeRelationSerializer m2m_field = models.AssetPermission.nodes.field permission_classes = (IsOrgAdmin,) - filter_fields = [ + filterset_fields = [ 'id', 'node', 'assetpermission', ] search_fields = ["node__value", "assetpermission__name"] @@ -135,7 +135,7 @@ class AssetPermissionSystemUserRelationViewSet(RelationMixin): serializer_class = serializers.AssetPermissionSystemUserRelationSerializer m2m_field = models.AssetPermission.system_users.field permission_classes = (IsOrgAdmin,) - filter_fields = [ + filterset_fields = [ 'id', 'systemuser', 'assetpermission', ] search_fields = [ diff --git a/apps/perms/api/asset/user_group_permission.py b/apps/perms/api/asset/user_group_permission.py index 8f48a0342..c1b025cb3 100644 --- a/apps/perms/api/asset/user_group_permission.py +++ b/apps/perms/api/asset/user_group_permission.py @@ -35,7 +35,7 @@ class UserGroupGrantedAssetsApi(ListAPIView): permission_classes = (IsOrgAdminOrAppUser,) serializer_class = serializers.AssetGrantedSerializer only_fields = serializers.AssetGrantedSerializer.Meta.only_fields - filter_fields = ['hostname', 'ip', 'id', 'comment'] + filterset_fields = ['hostname', 'ip', 'id', 'comment'] search_fields = ['hostname', 'ip', 'comment'] def get_queryset(self): @@ -68,7 +68,7 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView): permission_classes = (IsOrgAdminOrAppUser,) serializer_class = serializers.AssetGrantedSerializer only_fields = serializers.AssetGrantedSerializer.Meta.only_fields - filter_fields = ['hostname', 'ip', 'id', 'comment'] + filterset_fields = ['hostname', 'ip', 'id', 'comment'] search_fields = ['hostname', 'ip', 'comment'] def get_queryset(self): diff --git a/apps/perms/api/asset/user_permission/user_permission_assets.py b/apps/perms/api/asset/user_permission/user_permission_assets.py index 771f83bbf..209b59625 100644 --- a/apps/perms/api/asset/user_permission/user_permission_assets.py +++ b/apps/perms/api/asset/user_permission/user_permission_assets.py @@ -27,7 +27,7 @@ class UserDirectGrantedAssetsApi(ListAPIView): """ serializer_class = serializers.AssetGrantedSerializer only_fields = serializers.AssetGrantedSerializer.Meta.only_fields - filter_fields = ['hostname', 'ip', 'id', 'comment'] + filterset_fields = ['hostname', 'ip', 'id', 'comment'] search_fields = ['hostname', 'ip', 'comment'] def get_queryset(self): @@ -43,7 +43,7 @@ class UserDirectGrantedAssetsApi(ListAPIView): class UserFavoriteGrantedAssetsApi(ListAPIView): serializer_class = serializers.AssetGrantedSerializer only_fields = serializers.AssetGrantedSerializer.Meta.only_fields - filter_fields = ['hostname', 'ip', 'id', 'comment'] + filterset_fields = ['hostname', 'ip', 'id', 'comment'] search_fields = ['hostname', 'ip', 'comment'] def get_queryset(self): @@ -101,7 +101,7 @@ class MyUngroupAssetsAsTreeApi(ForUserMixin, AssetsAsTreeMixin, UserDirectGrante class UserAllGrantedAssetsApi(ForAdminMixin, ListAPIView): only_fields = serializers.AssetGrantedSerializer.Meta.only_fields serializer_class = serializers.AssetGrantedSerializer - filter_fields = ['hostname', 'ip', 'id', 'comment'] + filterset_fields = ['hostname', 'ip', 'id', 'comment'] search_fields = ['hostname', 'ip', 'comment'] def get_queryset(self): @@ -123,7 +123,7 @@ class MyAllAssetsAsTreeApi(ForUserMixin, AssetsAsTreeMixin, UserAllGrantedAssets class UserGrantedNodeAssetsApi(UserNodeGrantStatusDispatchMixin, ListAPIView): serializer_class = serializers.AssetGrantedSerializer only_fields = serializers.AssetGrantedSerializer.Meta.only_fields - filter_fields = ['hostname', 'ip', 'id', 'comment'] + filterset_fields = ['hostname', 'ip', 'id', 'comment'] search_fields = ['hostname', 'ip', 'comment'] pagination_class = GrantedAssetLimitOffsetPagination pagination_node: Node diff --git a/apps/perms/api/base.py b/apps/perms/api/base.py index b56c161d7..3d9c8672d 100644 --- a/apps/perms/api/base.py +++ b/apps/perms/api/base.py @@ -1,5 +1,3 @@ -from django.db.models import F -from orgs.mixins.api import OrgRelationMixin from django.db.models import Q from common.permissions import IsOrgAdmin from common.utils import get_object_or_none @@ -8,9 +6,7 @@ from assets.models import SystemUser from users.models import User, UserGroup -__all__ = [ - 'RelationViewSet', 'BasePermissionViewSet' -] +__all__ = ['BasePermissionViewSet'] class BasePermissionViewSet(OrgBulkModelViewSet): @@ -99,10 +95,3 @@ class BasePermissionViewSet(OrgBulkModelViewSet): queryset = self.filter_keyword(queryset) queryset = queryset.distinct() return queryset - - -class RelationViewSet(OrgRelationMixin, OrgBulkModelViewSet): - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate(**{f'{self.from_field}_display': F(f'{self.from_field}__name')}) - return queryset diff --git a/apps/perms/api/database_app_permission.py b/apps/perms/api/database_app_permission.py deleted file mode 100644 index 4b43d347f..000000000 --- a/apps/perms/api/database_app_permission.py +++ /dev/null @@ -1,21 +0,0 @@ -# coding: utf-8 -# - -from orgs.mixins.api import OrgBulkModelViewSet - -from .. import models, serializers -from common.permissions import IsOrgAdmin - - -__all__ = ['DatabaseAppPermissionViewSet'] - - -class DatabaseAppPermissionViewSet(OrgBulkModelViewSet): - model = models.DatabaseAppPermission - serializer_classes = { - 'default': serializers.DatabaseAppPermissionSerializer, - 'display': serializers.DatabaseAppPermissionListSerializer - } - filter_fields = ('name',) - search_fields = filter_fields - permission_classes = (IsOrgAdmin,) diff --git a/apps/perms/api/database_app_permission_relation.py b/apps/perms/api/database_app_permission_relation.py deleted file mode 100644 index 572896e34..000000000 --- a/apps/perms/api/database_app_permission_relation.py +++ /dev/null @@ -1,120 +0,0 @@ -# coding: utf-8 -# -from rest_framework import generics -from django.db.models import F, Value -from django.db.models.functions import Concat -from django.shortcuts import get_object_or_404 - -from common.permissions import IsOrgAdmin -from .base import RelationViewSet -from .. import models, serializers - -__all__ = [ - 'DatabaseAppPermissionUserRelationViewSet', - 'DatabaseAppPermissionUserGroupRelationViewSet', - 'DatabaseAppPermissionAllUserListApi', - 'DatabaseAppPermissionDatabaseAppRelationViewSet', - 'DatabaseAppPermissionAllDatabaseAppListApi', - 'DatabaseAppPermissionSystemUserRelationViewSet', -] - - -class DatabaseAppPermissionUserRelationViewSet(RelationViewSet): - serializer_class = serializers.DatabaseAppPermissionUserRelationSerializer - m2m_field = models.DatabaseAppPermission.users.field - permission_classes = (IsOrgAdmin,) - filter_fields = [ - 'id', 'user', 'databaseapppermission' - ] - search_fields = ('user__name', 'user__username', 'databaseapppermission__name') - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate(user_display=F('user__name')) - return queryset - - -class DatabaseAppPermissionUserGroupRelationViewSet(RelationViewSet): - serializer_class = serializers.DatabaseAppPermissionUserGroupRelationSerializer - m2m_field = models.DatabaseAppPermission.user_groups.field - permission_classes = (IsOrgAdmin,) - filter_fields = [ - 'id', "usergroup", "databaseapppermission" - ] - search_fields = ["usergroup__name", "databaseapppermission__name"] - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset \ - .annotate(usergroup_display=F('usergroup__name')) - return queryset - - -class DatabaseAppPermissionAllUserListApi(generics.ListAPIView): - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.DatabaseAppPermissionAllUserSerializer - filter_fields = ("username", "name") - search_fields = filter_fields - - def get_queryset(self): - pk = self.kwargs.get("pk") - perm = get_object_or_404(models.DatabaseAppPermission, pk=pk) - users = perm.get_all_users().only( - *self.serializer_class.Meta.only_fields - ) - return users - - -class DatabaseAppPermissionDatabaseAppRelationViewSet(RelationViewSet): - serializer_class = serializers.DatabaseAppPermissionDatabaseAppRelationSerializer - m2m_field = models.DatabaseAppPermission.database_apps.field - permission_classes = (IsOrgAdmin,) - filter_fields = [ - 'id', 'databaseapp', 'databaseapppermission', - ] - search_fields = [ - "id", "databaseapp__name", "databaseapppermission__name" - ] - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset \ - .annotate(databaseapp_display=F('databaseapp__name')) - return queryset - - -class DatabaseAppPermissionAllDatabaseAppListApi(generics.ListAPIView): - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.DatabaseAppPermissionAllDatabaseAppSerializer - filter_fields = ("name",) - search_fields = filter_fields - - def get_queryset(self): - pk = self.kwargs.get("pk") - perm = get_object_or_404(models.DatabaseAppPermission, pk=pk) - database_apps = perm.get_all_database_apps().only( - *self.serializer_class.Meta.only_fields - ) - return database_apps - - -class DatabaseAppPermissionSystemUserRelationViewSet(RelationViewSet): - serializer_class = serializers.DatabaseAppPermissionSystemUserRelationSerializer - m2m_field = models.DatabaseAppPermission.system_users.field - permission_classes = (IsOrgAdmin,) - filter_fields = [ - 'id', 'systemuser', 'databaseapppermission' - ] - search_fields = [ - 'databaseapppermission__name', 'systemuser__name', 'systemuser__username' - ] - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate( - systemuser_display=Concat( - F('systemuser__name'), Value('('), F('systemuser__username'), - Value(')') - ) - ) - return queryset diff --git a/apps/perms/api/k8s_app_permission.py b/apps/perms/api/k8s_app_permission.py deleted file mode 100644 index b1111bd7b..000000000 --- a/apps/perms/api/k8s_app_permission.py +++ /dev/null @@ -1,21 +0,0 @@ -# coding: utf-8 -# - -from orgs.mixins.api import OrgBulkModelViewSet - -from .. import models, serializers -from common.permissions import IsOrgAdmin - - -__all__ = ['K8sAppPermissionViewSet'] - - -class K8sAppPermissionViewSet(OrgBulkModelViewSet): - model = models.K8sAppPermission - serializer_classes = { - 'default': serializers.K8sAppPermissionSerializer, - 'display': serializers.K8sAppPermissionListSerializer - } - filter_fields = ('name',) - search_fields = filter_fields - permission_classes = (IsOrgAdmin,) diff --git a/apps/perms/api/k8s_app_permission_relation.py b/apps/perms/api/k8s_app_permission_relation.py deleted file mode 100644 index 443f32204..000000000 --- a/apps/perms/api/k8s_app_permission_relation.py +++ /dev/null @@ -1,111 +0,0 @@ -# coding: utf-8 -# -from rest_framework import generics -from django.db.models import F, Value -from django.db.models.functions import Concat -from django.shortcuts import get_object_or_404 - -from common.permissions import IsOrgAdmin -from .base import RelationViewSet -from .. import models, serializers - - -class K8sAppPermissionUserRelationViewSet(RelationViewSet): - serializer_class = serializers.K8sAppPermissionUserRelationSerializer - m2m_field = models.K8sAppPermission.users.field - permission_classes = (IsOrgAdmin,) - filter_fields = [ - 'id', 'user', 'k8sapppermission' - ] - search_fields = ('user__name', 'user__username', 'k8sapppermission__name') - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate(user_display=F('user__name')) - return queryset - - -class K8sAppPermissionUserGroupRelationViewSet(RelationViewSet): - serializer_class = serializers.K8sAppPermissionUserGroupRelationSerializer - m2m_field = models.K8sAppPermission.user_groups.field - permission_classes = (IsOrgAdmin,) - filter_fields = [ - 'id', "usergroup", "k8sapppermission" - ] - search_fields = ["usergroup__name", "k8sapppermission__name"] - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset \ - .annotate(usergroup_display=F('usergroup__name')) - return queryset - - -class K8sAppPermissionAllUserListApi(generics.ListAPIView): - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.K8sAppPermissionAllUserSerializer - filter_fields = ("username", "name") - search_fields = filter_fields - - def get_queryset(self): - pk = self.kwargs.get("pk") - perm = get_object_or_404(models.K8sAppPermission, pk=pk) - users = perm.get_all_users().only( - *self.serializer_class.Meta.only_fields - ) - return users - - -class K8sAppPermissionK8sAppRelationViewSet(RelationViewSet): - serializer_class = serializers.K8sAppPermissionK8sAppRelationSerializer - m2m_field = models.K8sAppPermission.k8s_apps.field - permission_classes = (IsOrgAdmin,) - filter_fields = [ - 'id', 'k8sapp', 'k8sapppermission', - ] - search_fields = [ - "id", "k8sapp__name", "k8sapppermission__name" - ] - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset \ - .annotate(k8sapp_display=F('k8sapp__name')) - return queryset - - -class K8sAppPermissionAllK8sAppListApi(generics.ListAPIView): - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.K8sAppPermissionAllK8sAppSerializer - filter_fields = ("name",) - search_fields = filter_fields - - def get_queryset(self): - pk = self.kwargs.get("pk") - perm = get_object_or_404(models.K8sAppPermission, pk=pk) - database_apps = perm.get_all_k8s_apps().only( - *self.serializer_class.Meta.only_fields - ) - return database_apps - - -class K8sAppPermissionSystemUserRelationViewSet(RelationViewSet): - serializer_class = serializers.K8sAppPermissionSystemUserRelationSerializer - m2m_field = models.K8sAppPermission.system_users.field - permission_classes = (IsOrgAdmin,) - filter_fields = [ - 'id', 'systemuser', 'k8sapppermission' - ] - search_fields = [ - 'k8sapppermission__name', 'systemuser__name', 'systemuser__username' - ] - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate( - systemuser_display=Concat( - F('systemuser__name'), Value('('), F('systemuser__username'), - Value(')') - ) - ) - return queryset diff --git a/apps/perms/api/mixin.py b/apps/perms/api/mixin.py deleted file mode 100644 index 43c96dc01..000000000 --- a/apps/perms/api/mixin.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from rest_framework.generics import get_object_or_404 -from common.permissions import IsValidUser, IsOrgAdminOrAppUser -from common.utils import get_logger -from orgs.utils import set_to_root_org, set_current_org, get_current_org -from ..hands import User, UserGroup - - -logger = get_logger(__name__) - -__all__ = [ - 'UserPermissionMixin', 'UserGroupPermissionMixin', -] - - -class UserPermissionMixin: - permission_classes = (IsOrgAdminOrAppUser,) - current_org = None - obj = None - - def initial(self, *args, **kwargs): - super().initial(*args, **kwargs) - self.obj = self.get_obj() - - def get_obj(self): - user_id = self.kwargs.get('pk', '') - if user_id: - user = get_object_or_404(User, id=user_id) - else: - self.current_org = get_current_org() - set_to_root_org() - user = self.request.user - return user - - def get_permissions(self): - if self.kwargs.get('pk') is None: - self.permission_classes = (IsValidUser,) - return super().get_permissions() - - def finalize_response(self, request, response, *args, **kwargs): - response = super().finalize_response(request, response, *args, **kwargs) - org = getattr(self, 'current_org', None) - if org: - set_current_org(org) - return response - - -class UserGroupPermissionMixin: - obj = None - - def get_obj(self): - user_group_id = self.kwargs.get('pk', '') - user_group = get_object_or_404(UserGroup, id=user_group_id) - return user_group - - diff --git a/apps/perms/api/remote_app_permission.py b/apps/perms/api/remote_app_permission.py deleted file mode 100644 index cb1998675..000000000 --- a/apps/perms/api/remote_app_permission.py +++ /dev/null @@ -1,98 +0,0 @@ -# coding: utf-8 -# - -from rest_framework.views import Response - -from common.permissions import IsOrgAdmin -from orgs.mixins.api import OrgModelViewSet -from orgs.mixins import generics -from ..models import RemoteAppPermission -from ..serializers import ( - RemoteAppPermissionSerializer, - RemoteAppPermissionUpdateUserSerializer, - RemoteAppPermissionUpdateRemoteAppSerializer, -) - -__all__ = [ - 'RemoteAppPermissionViewSet', - 'RemoteAppPermissionAddUserApi', 'RemoteAppPermissionAddRemoteAppApi', - 'RemoteAppPermissionRemoveUserApi', 'RemoteAppPermissionRemoveRemoteAppApi', -] - - -class RemoteAppPermissionViewSet(OrgModelViewSet): - model = RemoteAppPermission - filter_fields = ('name', ) - search_fields = filter_fields - serializer_class = RemoteAppPermissionSerializer - permission_classes = (IsOrgAdmin,) - - -class RemoteAppPermissionAddUserApi(generics.RetrieveUpdateAPIView): - model = RemoteAppPermission - permission_classes = (IsOrgAdmin,) - serializer_class = RemoteAppPermissionUpdateUserSerializer - - def update(self, request, *args, **kwargs): - perm = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - users = serializer.validated_data.get('users') - if users: - perm.users.add(*tuple(users)) - return Response({"msg": "ok"}) - else: - return Response({"error": serializer.errors}) - - -class RemoteAppPermissionRemoveUserApi(generics.RetrieveUpdateAPIView): - model = RemoteAppPermission - permission_classes = (IsOrgAdmin,) - serializer_class = RemoteAppPermissionUpdateUserSerializer - - def update(self, request, *args, **kwargs): - perm = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - users = serializer.validated_data.get('users') - if users: - perm.users.remove(*tuple(users)) - return Response({"msg": "ok"}) - else: - return Response({"error": serializer.errors}) - - -class RemoteAppPermissionAddRemoteAppApi(generics.RetrieveUpdateAPIView): - model = RemoteAppPermission - permission_classes = (IsOrgAdmin,) - serializer_class = RemoteAppPermissionUpdateRemoteAppSerializer - - def update(self, request, *args, **kwargs): - perm = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - remote_apps = serializer.validated_data.get('remote_apps') - if remote_apps: - perm.remote_apps.add(*tuple(remote_apps)) - return Response({"msg": "ok"}) - else: - return Response({"error": serializer.errors}) - - -class RemoteAppPermissionRemoveRemoteAppApi(generics.RetrieveUpdateAPIView): - model = RemoteAppPermission - permission_classes = (IsOrgAdmin,) - serializer_class = RemoteAppPermissionUpdateRemoteAppSerializer - - def update(self, request, *args, **kwargs): - perm = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - remote_apps = serializer.validated_data.get('remote_apps') - if remote_apps: - perm.remote_apps.remove(*tuple(remote_apps)) - return Response({"msg": "ok"}) - else: - return Response({"error": serializer.errors}) - - diff --git a/apps/perms/api/remote_app_permission_relation.py b/apps/perms/api/remote_app_permission_relation.py deleted file mode 100644 index fe0ab7355..000000000 --- a/apps/perms/api/remote_app_permission_relation.py +++ /dev/null @@ -1,79 +0,0 @@ -# coding: utf-8 -# -from perms.api.base import RelationViewSet -from rest_framework import generics -from django.db.models import F -from django.shortcuts import get_object_or_404 - -from common.permissions import IsOrgAdmin -from .. import models, serializers - -__all__ = [ - 'RemoteAppPermissionUserRelationViewSet', - 'RemoteAppPermissionRemoteAppRelationViewSet', - 'RemoteAppPermissionAllRemoteAppListApi', - 'RemoteAppPermissionAllUserListApi', -] - - -class RemoteAppPermissionAllUserListApi(generics.ListAPIView): - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.PermissionAllUserSerializer - filter_fields = ("username", "name") - search_fields = filter_fields - - def get_queryset(self): - pk = self.kwargs.get("pk") - perm = get_object_or_404(models.RemoteAppPermission, pk=pk) - users = perm.all_users.only( - *self.serializer_class.Meta.only_fields - ) - return users - - -class RemoteAppPermissionUserRelationViewSet(RelationViewSet): - serializer_class = serializers.RemoteAppPermissionUserRelationSerializer - m2m_field = models.RemoteAppPermission.users.field - permission_classes = (IsOrgAdmin,) - filter_fields = [ - 'id', 'user', 'remoteapppermission' - ] - search_fields = ('user__name', 'user__username', 'remoteapppermission__name') - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate(user_display=F('user__name')) - return queryset - - -class RemoteAppPermissionRemoteAppRelationViewSet(RelationViewSet): - serializer_class = serializers.RemoteAppPermissionRemoteAppRelationSerializer - m2m_field = models.RemoteAppPermission.remote_apps.field - permission_classes = (IsOrgAdmin,) - filter_fields = [ - 'id', 'remoteapp', 'remoteapppermission', - ] - search_fields = [ - "id", "remoteapp__name", "remoteapppermission__name" - ] - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset \ - .annotate(remoteapp_display=F('remoteapp__name')) - return queryset - - -class RemoteAppPermissionAllRemoteAppListApi(generics.ListAPIView): - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.RemoteAppPermissionAllRemoteAppSerializer - filter_fields = ("name",) - search_fields = filter_fields - - def get_queryset(self): - pk = self.kwargs.get("pk") - perm = get_object_or_404(models.RemoteAppPermission, pk=pk) - remote_apps = perm.all_remote_apps.only( - *self.serializer_class.Meta.only_fields - ) - return remote_apps diff --git a/apps/perms/api/user_database_app_permission.py b/apps/perms/api/user_database_app_permission.py deleted file mode 100644 index f102c3623..000000000 --- a/apps/perms/api/user_database_app_permission.py +++ /dev/null @@ -1,128 +0,0 @@ -# coding: utf-8 -# - -import uuid -from django.shortcuts import get_object_or_404 -from rest_framework.views import APIView, Response -from common.permissions import IsOrgAdminOrAppUser, IsValidUser -from common.tree import TreeNodeSerializer -from orgs.mixins import generics -from users.models import User, UserGroup -from applications.serializers import DatabaseAppSerializer -from applications.models import DatabaseApp -from assets.models import SystemUser -from .. import utils, serializers -from .mixin import UserPermissionMixin - -__all__ = [ - 'UserGrantedDatabaseAppsApi', - 'UserGrantedDatabaseAppsAsTreeApi', - 'UserGroupGrantedDatabaseAppsApi', - 'ValidateUserDatabaseAppPermissionApi', - 'UserGrantedDatabaseAppSystemUsersApi', -] - - -class UserGrantedDatabaseAppsApi(generics.ListAPIView): - permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = DatabaseAppSerializer - filter_fields = ['id', 'name', 'type', 'comment'] - search_fields = ['name', 'comment'] - - def get_object(self): - user_id = self.kwargs.get('pk', '') - if user_id: - user = get_object_or_404(User, id=user_id) - else: - user = self.request.user - return user - - def get_queryset(self): - util = utils.DatabaseAppPermissionUtil(self.get_object()) - queryset = util.get_database_apps() - return queryset - - def get_permissions(self): - if self.kwargs.get('pk') is None: - self.permission_classes = (IsValidUser,) - return super().get_permissions() - - -class UserGrantedDatabaseAppsAsTreeApi(UserGrantedDatabaseAppsApi): - serializer_class = TreeNodeSerializer - permission_classes = (IsOrgAdminOrAppUser,) - - def get_serializer(self, database_apps, *args, **kwargs): - if database_apps is None: - database_apps = [] - only_database_app = self.request.query_params.get('only', '0') == '1' - tree_root = None - data = [] - if not only_database_app: - amount = len(database_apps) - tree_root = utils.construct_database_apps_tree_root(amount) - data.append(tree_root) - for database_app in database_apps: - node = utils.parse_database_app_to_tree_node(tree_root, database_app) - data.append(node) - data.sort() - return super().get_serializer(data, many=True) - - -class UserGrantedDatabaseAppSystemUsersApi(UserPermissionMixin, generics.ListAPIView): - permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = serializers.DatabaseAppSystemUserSerializer - only_fields = serializers.DatabaseAppSystemUserSerializer.Meta.only_fields - - def get_queryset(self): - util = utils.DatabaseAppPermissionUtil(self.obj) - database_app_id = self.kwargs.get('database_app_id') - database_app = get_object_or_404(DatabaseApp, id=database_app_id) - system_users = util.get_database_app_system_users(database_app) - return system_users - - -# Validate - -class ValidateUserDatabaseAppPermissionApi(APIView): - permission_classes = (IsOrgAdminOrAppUser,) - - def get(self, request, *args, **kwargs): - user_id = request.query_params.get('user_id', '') - database_app_id = request.query_params.get('database_app_id', '') - system_user_id = request.query_params.get('system_user_id', '') - - try: - user_id = uuid.UUID(user_id) - database_app_id = uuid.UUID(database_app_id) - system_user_id = uuid.UUID(system_user_id) - except ValueError: - return Response({'msg': False}, status=403) - - user = get_object_or_404(User, id=user_id) - database_app = get_object_or_404(DatabaseApp, id=database_app_id) - system_user = get_object_or_404(SystemUser, id=system_user_id) - - util = utils.DatabaseAppPermissionUtil(user) - system_users = util.get_database_app_system_users(database_app) - if system_user in system_users: - return Response({'msg': True}, status=200) - - return Response({'msg': False}, status=403) - - -# UserGroup - -class UserGroupGrantedDatabaseAppsApi(generics.ListAPIView): - permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = DatabaseAppSerializer - - def get_queryset(self): - queryset = [] - user_group_id = self.kwargs.get('pk') - if not user_group_id: - return queryset - user_group = get_object_or_404(UserGroup, id=user_group_id) - util = utils.DatabaseAppPermissionUtil(user_group) - queryset = util.get_database_apps() - return queryset diff --git a/apps/perms/api/user_k8s_app_permission.py b/apps/perms/api/user_k8s_app_permission.py deleted file mode 100644 index 0aa5ac6f5..000000000 --- a/apps/perms/api/user_k8s_app_permission.py +++ /dev/null @@ -1,120 +0,0 @@ -# coding: utf-8 -# - -import uuid -from django.shortcuts import get_object_or_404 -from rest_framework.views import APIView, Response -from common.permissions import IsOrgAdminOrAppUser, IsValidUser -from common.tree import TreeNodeSerializer -from orgs.mixins import generics -from users.models import User, UserGroup -from applications.serializers import K8sAppSerializer -from applications.models import K8sApp -from assets.models import SystemUser -from .. import utils, serializers -from .mixin import UserPermissionMixin - - -class UserGrantedK8sAppsApi(generics.ListAPIView): - permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = K8sAppSerializer - filter_fields = ['id', 'name', 'type', 'comment'] - search_fields = ['name', 'comment'] - - def get_object(self): - user_id = self.kwargs.get('pk', '') - if user_id: - user = get_object_or_404(User, id=user_id) - else: - user = self.request.user - return user - - def get_queryset(self): - util = utils.K8sAppPermissionUtil(self.get_object()) - queryset = util.get_k8s_apps() - return queryset - - def get_permissions(self): - if self.kwargs.get('pk') is None: - self.permission_classes = (IsValidUser,) - return super().get_permissions() - - -class UserGrantedK8sAppsAsTreeApi(UserGrantedK8sAppsApi): - serializer_class = TreeNodeSerializer - permission_classes = (IsOrgAdminOrAppUser,) - - def get_serializer(self, k8s_apps, *args, **kwargs): - if k8s_apps is None: - k8s_apps = [] - only_k8s_app = self.request.query_params.get('only', '0') == '1' - tree_root = None - data = [] - if not only_k8s_app: - amount = len(k8s_apps) - tree_root = utils.construct_k8s_apps_tree_root(amount) - data.append(tree_root) - for k8s_app in k8s_apps: - node = utils.parse_k8s_app_to_tree_node(tree_root, k8s_app) - data.append(node) - data.sort() - return super().get_serializer(data, many=True) - - -class UserGrantedK8sAppSystemUsersApi(UserPermissionMixin, generics.ListAPIView): - permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = serializers.K8sAppSystemUserSerializer - only_fields = serializers.K8sAppSystemUserSerializer.Meta.only_fields - - def get_queryset(self): - util = utils.K8sAppPermissionUtil(self.obj) - k8s_app_id = self.kwargs.get('k8s_app_id') - k8s_app = get_object_or_404(K8sApp, id=k8s_app_id) - system_users = util.get_k8s_app_system_users(k8s_app) - return system_users - - -# Validate - -class ValidateUserK8sAppPermissionApi(APIView): - permission_classes = (IsOrgAdminOrAppUser,) - - def get(self, request, *args, **kwargs): - user_id = request.query_params.get('user_id', '') - k8s_app_id = request.query_params.get('k8s_app_id', '') - system_user_id = request.query_params.get('system_user_id', '') - - try: - user_id = uuid.UUID(user_id) - k8s_app_id = uuid.UUID(k8s_app_id) - system_user_id = uuid.UUID(system_user_id) - except ValueError: - return Response({'msg': False}, status=403) - - user = get_object_or_404(User, id=user_id) - k8s_app = get_object_or_404(K8sApp, id=k8s_app_id) - system_user = get_object_or_404(SystemUser, id=system_user_id) - - util = utils.K8sAppPermissionUtil(user) - system_users = util.get_k8s_app_system_users(k8s_app) - if system_user in system_users: - return Response({'msg': True}, status=200) - - return Response({'msg': False}, status=403) - - -# UserGroup - -class UserGroupGrantedK8sAppsApi(generics.ListAPIView): - permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = K8sAppSerializer - - def get_queryset(self): - queryset = [] - user_group_id = self.kwargs.get('pk') - if not user_group_id: - return queryset - user_group = get_object_or_404(UserGroup, id=user_group_id) - util = utils.K8sAppPermissionUtil(user_group) - queryset = util.get_k8s_apps() - return queryset diff --git a/apps/perms/api/user_remote_app_permission.py b/apps/perms/api/user_remote_app_permission.py deleted file mode 100644 index 8f418683e..000000000 --- a/apps/perms/api/user_remote_app_permission.py +++ /dev/null @@ -1,126 +0,0 @@ -# -*- coding: utf-8 -*- - -import uuid -from django.shortcuts import get_object_or_404 -from rest_framework.views import APIView, Response - -from common.permissions import IsValidUser, IsOrgAdminOrAppUser -from common.tree import TreeNodeSerializer -from orgs.mixins import generics -from ..utils import ( - RemoteAppPermissionUtil, construct_remote_apps_tree_root, - parse_remote_app_to_tree_node, -) -from ..hands import User, RemoteApp, RemoteAppSerializer, UserGroup, SystemUser -from .mixin import UserPermissionMixin -from .. import serializers - - -__all__ = [ - 'UserGrantedRemoteAppsApi', 'ValidateUserRemoteAppPermissionApi', - 'UserGrantedRemoteAppsAsTreeApi', 'UserGroupGrantedRemoteAppsApi', - 'UserGrantedRemoteAppSystemUsersApi', -] - - -class UserGrantedRemoteAppsApi(generics.ListAPIView): - permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = RemoteAppSerializer - filter_fields = ['name', 'id', 'type', 'comment'] - search_fields = ['name', 'comment'] - - def get_object(self): - user_id = self.kwargs.get('pk', '') - if user_id: - user = get_object_or_404(User, id=user_id) - else: - user = self.request.user - return user - - def get_queryset(self): - util = RemoteAppPermissionUtil(self.get_object()) - queryset = util.get_remote_apps() - return queryset - - def get_permissions(self): - if self.kwargs.get('pk') is None: - self.permission_classes = (IsValidUser,) - return super().get_permissions() - - -class UserGrantedRemoteAppsAsTreeApi(UserGrantedRemoteAppsApi): - serializer_class = TreeNodeSerializer - permission_classes = (IsOrgAdminOrAppUser,) - - def get_serializer(self, remote_apps=None, *args, **kwargs): - if remote_apps is None: - remote_apps = [] - only_remote_app = self.request.query_params.get('only', '0') == '1' - tree_root = None - data = [] - if not only_remote_app: - amount = len(remote_apps) - tree_root = construct_remote_apps_tree_root(amount) - data.append(tree_root) - for remote_app in remote_apps: - node = parse_remote_app_to_tree_node(tree_root, remote_app) - data.append(node) - data.sort() - return super().get_serializer(data, many=True) - - -class UserGrantedRemoteAppSystemUsersApi(UserPermissionMixin, generics.ListAPIView): - permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = serializers.RemoteAppSystemUserSerializer - only_fields = serializers.RemoteAppSystemUserSerializer.Meta.only_fields - - def get_queryset(self): - util = RemoteAppPermissionUtil(self.obj) - remote_app_id = self.kwargs.get('remote_app_id') - remote_app = get_object_or_404(RemoteApp, id=remote_app_id) - system_users = util.get_remote_app_system_users(remote_app) - return system_users - - -class ValidateUserRemoteAppPermissionApi(APIView): - permission_classes = (IsOrgAdminOrAppUser,) - - def get(self, request, *args, **kwargs): - user_id = request.query_params.get('user_id', '') - remote_app_id = request.query_params.get('remote_app_id', '') - system_id = request.query_params.get('system_user_id', '') - - try: - user_id = uuid.UUID(user_id) - remote_app_id = uuid.UUID(remote_app_id) - system_id = uuid.UUID(system_id) - except ValueError: - return Response({'msg': False}, status=403) - - user = get_object_or_404(User, id=user_id) - remote_app = get_object_or_404(RemoteApp, id=remote_app_id) - system_user = get_object_or_404(SystemUser, id=system_id) - - util = RemoteAppPermissionUtil(user) - system_users = util.get_remote_app_system_users(remote_app) - if system_user in system_users: - return Response({'msg': True}, status=200) - - return Response({'msg': False}, status=403) - - -# RemoteApp permission - -class UserGroupGrantedRemoteAppsApi(generics.ListAPIView): - permission_classes = (IsOrgAdminOrAppUser, ) - serializer_class = RemoteAppSerializer - - def get_queryset(self): - queryset = [] - user_group_id = self.kwargs.get('pk') - if not user_group_id: - return queryset - user_group = get_object_or_404(UserGroup, id=user_group_id) - util = RemoteAppPermissionUtil(user_group) - queryset = util.get_remote_apps() - return queryset diff --git a/apps/perms/forms/__init__.py b/apps/perms/forms/__init__.py deleted file mode 100644 index c6581b858..000000000 --- a/apps/perms/forms/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# coding: utf-8 -# - -from .asset_permission import * -from .remote_app_permission import * -from .database_app_permission import * diff --git a/apps/perms/forms/asset_permission.py b/apps/perms/forms/asset_permission.py deleted file mode 100644 index aa75b8be5..000000000 --- a/apps/perms/forms/asset_permission.py +++ /dev/null @@ -1,119 +0,0 @@ -# ~*~ coding: utf-8 ~*~ - -from __future__ import absolute_import, unicode_literals -from django import forms -from django.utils.translation import ugettext_lazy as _ - -from orgs.mixins.forms import OrgModelForm -from assets.models import Asset, Node, SystemUser -from ..models import AssetPermission, Action - -__all__ = [ - 'AssetPermissionForm', -] - - -class ActionField(forms.MultipleChoiceField): - def __init__(self, *args, **kwargs): - kwargs['choices'] = Action.CHOICES - kwargs['initial'] = Action.ALL - kwargs['label'] = _("Action") - kwargs['widget'] = forms.CheckboxSelectMultiple() - kwargs['help_text'] = _( - 'Tips: The RDP protocol does not support separate controls ' - 'for uploading or downloading files' - ) - super().__init__(*args, **kwargs) - - def to_python(self, value): - value = super().to_python(value) - return Action.choices_to_value(value) - - def prepare_value(self, value): - if value is None: - return value - value = Action.value_to_choices(value) - return value - - -class AssetPermissionForm(OrgModelForm): - actions = ActionField() - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - if self.data: - return - # 前端渲染优化, 防止过多资产 - users_field = self.fields.get('users') - assets_field = self.fields['assets'] - nodes_field = self.fields['nodes'] - if self.instance: - assets_field.queryset = self.instance.assets.all() - nodes_field.queryset = self.instance.nodes.all() - users_field.queryset = self.instance.users.all() - else: - assets_field.queryset = Asset.objects.none() - nodes_field.queryset = Node.objects.none() - users_field.queryset = [] - - # 过滤系统用户 - system_users_field = self.fields.get('system_users') - system_users_field.queryset = SystemUser.objects.exclude( - protocol__in=SystemUser.application_category_protocols - ) - - def set_nodes_initial(self, nodes): - field = self.fields['nodes'] - field.choices = [(n.id, n.full_value) for n in nodes] - field.initial = nodes - - def set_assets_initial(self, assets): - field = self.fields['assets'] - field.choices = [(a.id, a.hostname) for a in assets] - field.initial = assets - - class Meta: - model = AssetPermission - exclude = ( - 'id', 'date_created', 'created_by', 'org_id' - ) - widgets = { - 'users': forms.SelectMultiple( - attrs={'class': 'users-select2', 'data-placeholder': _("User")} - ), - 'user_groups': forms.SelectMultiple( - attrs={'class': 'select2', 'data-placeholder': _("User group")} - ), - 'assets': forms.SelectMultiple( - attrs={'class': 'select2', 'data-placeholder': _("Asset")} - ), - 'nodes': forms.SelectMultiple( - attrs={'class': 'nodes-select2', 'data-placeholder': _("Node")} - ), - 'system_users': forms.SelectMultiple( - attrs={'class': 'select2', 'data-placeholder': _('System user')} - ), - } - labels = { - 'nodes': _("Node"), - } - - def clean_user_groups(self): - users = self.cleaned_data.get('users') - user_groups = self.cleaned_data.get('user_groups') - - if not users and not user_groups: - raise forms.ValidationError( - _("User or group at least one required")) - return self.cleaned_data["user_groups"] - - def clean_asset_groups(self): - assets = self.cleaned_data.get('assets') - asset_groups = self.cleaned_data.get('asset_groups') - - if not assets and not asset_groups: - raise forms.ValidationError( - _("Asset or group at least one required")) - - return self.cleaned_data["asset_groups"] diff --git a/apps/perms/forms/database_app_permission.py b/apps/perms/forms/database_app_permission.py deleted file mode 100644 index 9074491c9..000000000 --- a/apps/perms/forms/database_app_permission.py +++ /dev/null @@ -1,49 +0,0 @@ -# coding: utf-8 -# - -from django.utils.translation import ugettext as _ -from django import forms -from orgs.mixins.forms import OrgModelForm -from assets.models import SystemUser - -from ..models import DatabaseAppPermission - - -__all__ = ['DatabaseAppPermissionCreateUpdateForm'] - - -class DatabaseAppPermissionCreateUpdateForm(OrgModelForm): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - users_field = self.fields.get('users') - if self.instance: - users_field.queryset = self.instance.users.all() - else: - users_field.queryset = [] - - # 过滤系统用户 - system_users_field = self.fields.get('system_users') - system_users_field.queryset = SystemUser.objects.filter( - protocol__in=SystemUser.application_category_protocols - ) - - class Meta: - model = DatabaseAppPermission - exclude = ( - 'id', 'date_created', 'created_by', 'org_id' - ) - widgets = { - 'users': forms.SelectMultiple( - attrs={'class': 'users-select2', 'data-placeholder': _('User')} - ), - 'user_groups': forms.SelectMultiple( - attrs={'class': 'select2', 'data-placeholder': _('User group')} - ), - 'database_apps': forms.SelectMultiple( - attrs={'class': 'select2', 'data-placeholder': _('DatabaseApp')} - ), - 'system_users': forms.SelectMultiple( - attrs={'class': 'select2', 'data-placeholder': _('System users')} - ), - } diff --git a/apps/perms/forms/remote_app_permission.py b/apps/perms/forms/remote_app_permission.py deleted file mode 100644 index c4179297f..000000000 --- a/apps/perms/forms/remote_app_permission.py +++ /dev/null @@ -1,51 +0,0 @@ -# coding: utf-8 -# - -from django.utils.translation import ugettext as _ -from django import forms -from orgs.mixins.forms import OrgModelForm -from assets.models import SystemUser - -from ..models import RemoteAppPermission - - -__all__ = [ - 'RemoteAppPermissionCreateUpdateForm', -] - - -class RemoteAppPermissionCreateUpdateForm(OrgModelForm): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - users_field = self.fields.get('users') - if self.instance: - users_field.queryset = self.instance.users.all() - else: - users_field.queryset = [] - - # 过滤系统用户 - system_users_field = self.fields.get('system_users') - system_users_field.queryset = SystemUser.objects.filter( - protocol=SystemUser.PROTOCOL_RDP - ) - - class Meta: - model = RemoteAppPermission - exclude = ( - 'id', 'date_created', 'created_by', 'org_id' - ) - widgets = { - 'users': forms.SelectMultiple( - attrs={'class': 'users-select2', 'data-placeholder': _('User')} - ), - 'user_groups': forms.SelectMultiple( - attrs={'class': 'select2', 'data-placeholder': _('User group')} - ), - 'remote_apps': forms.SelectMultiple( - attrs={'class': 'select2', 'data-placeholder': _('RemoteApp')} - ), - 'system_users': forms.SelectMultiple( - attrs={'class': 'select2', 'data-placeholder': _('System user')} - ) - } diff --git a/apps/perms/hands.py b/apps/perms/hands.py index ab9e3f494..25902fddf 100644 --- a/apps/perms/hands.py +++ b/apps/perms/hands.py @@ -4,13 +4,10 @@ from users.models import User, UserGroup from assets.models import Asset, SystemUser, Node, Label, FavoriteAsset from assets.serializers import NodeSerializer -from applications.serializers import RemoteAppSerializer -from applications.models import RemoteApp __all__ = [ 'User', 'UserGroup', 'Asset', 'SystemUser', 'Node', 'Label', 'FavoriteAsset', - 'NodeSerializer', 'RemoteAppSerializer', - 'RemoteApp' + 'NodeSerializer', ] diff --git a/apps/perms/migrations/0017_auto_20210104_0435.py b/apps/perms/migrations/0017_auto_20210104_0435.py new file mode 100644 index 000000000..d27efe6ee --- /dev/null +++ b/apps/perms/migrations/0017_auto_20210104_0435.py @@ -0,0 +1,62 @@ +# Generated by Django 3.1 on 2021-01-03 20:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('perms', '0016_applicationpermission'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='k8sapppermission', + unique_together=None, + ), + migrations.RemoveField( + model_name='k8sapppermission', + name='k8s_apps', + ), + migrations.RemoveField( + model_name='k8sapppermission', + name='system_users', + ), + migrations.RemoveField( + model_name='k8sapppermission', + name='user_groups', + ), + migrations.RemoveField( + model_name='k8sapppermission', + name='users', + ), + migrations.AlterUniqueTogether( + name='remoteapppermission', + unique_together=None, + ), + migrations.RemoveField( + model_name='remoteapppermission', + name='remote_apps', + ), + migrations.RemoveField( + model_name='remoteapppermission', + name='system_users', + ), + migrations.RemoveField( + model_name='remoteapppermission', + name='user_groups', + ), + migrations.RemoveField( + model_name='remoteapppermission', + name='users', + ), + migrations.DeleteModel( + name='DatabaseAppPermission', + ), + migrations.DeleteModel( + name='K8sAppPermission', + ), + migrations.DeleteModel( + name='RemoteAppPermission', + ), + ] diff --git a/apps/perms/mixins.py b/apps/perms/mixins.py index 98ac8bf76..e69de29bb 100644 --- a/apps/perms/mixins.py +++ b/apps/perms/mixins.py @@ -1,22 +0,0 @@ -# ~*~ coding: utf-8 ~*~ -# - -from orgs.utils import set_to_root_org - -__all__ = [ - 'ChangeOrgIfNeedMixin', -] - - -class ChangeOrgIfNeedMixin(object): - - @staticmethod - def change_org_if_need(request, kwargs): - if request.user.is_authenticated and request.user.is_superuser \ - or request.user.is_app \ - or kwargs.get('pk') is None: - set_to_root_org() - - def get(self, request, *args, **kwargs): - self.change_org_if_need(request, kwargs) - return super().get(request, *args, **kwargs) diff --git a/apps/perms/models/__init__.py b/apps/perms/models/__init__.py index e7b0b0ffb..31a1344eb 100644 --- a/apps/perms/models/__init__.py +++ b/apps/perms/models/__init__.py @@ -3,6 +3,3 @@ from .asset_permission import * from .application_permission import * -from .remote_app_permission import * -from .database_app_permission import * -from .k8s_app_permission import * diff --git a/apps/perms/models/application_permission.py b/apps/perms/models/application_permission.py index cd9cdb001..44cb38323 100644 --- a/apps/perms/models/application_permission.py +++ b/apps/perms/models/application_permission.py @@ -8,7 +8,7 @@ from django.utils.translation import ugettext_lazy as _ from common.utils import lazyproperty from .base import BasePermission from users.models import User -from applications.models import Category +from applications.const import ApplicationCategoryChoices, ApplicationTypeChoices __all__ = [ 'ApplicationPermission', @@ -16,16 +16,38 @@ __all__ = [ class ApplicationPermission(BasePermission): - category = models.CharField(max_length=16, choices=Category.choices, verbose_name=_('Category')) - type = models.CharField(max_length=16, choices=Category.get_all_type_choices(), verbose_name=_('Type')) - applications = models.ManyToManyField('applications.Application', related_name='granted_by_permissions', blank=True, verbose_name=_("Application")) - system_users = models.ManyToManyField('assets.SystemUser', related_name='granted_by_application_permissions', verbose_name=_("System user")) + category = models.CharField( + max_length=16, choices=ApplicationCategoryChoices.choices, verbose_name=_('Category') + ) + type = models.CharField( + max_length=16, choices=ApplicationTypeChoices.choices, verbose_name=_('Type') + ) + applications = models.ManyToManyField( + 'applications.Application', related_name='granted_by_permissions', blank=True, + verbose_name=_("Application") + ) + system_users = models.ManyToManyField( + 'assets.SystemUser', related_name='granted_by_application_permissions', + verbose_name=_("System user") + ) class Meta: unique_together = [('org_id', 'name')] verbose_name = _('Application permission') ordering = ('name',) + @property + def category_remote_app(self): + return self.category == ApplicationCategoryChoices.remote_app.value + + @property + def category_db(self): + return self.category == ApplicationCategoryChoices.db.value + + @property + def category_cloud(self): + return self.category == ApplicationCategoryChoices.cloud.value + @lazyproperty def users_amount(self): return self.users.count() diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index 983805720..00c0d5a97 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -1,4 +1,3 @@ -import uuid import logging from functools import reduce @@ -6,8 +5,6 @@ from django.utils.translation import ugettext_lazy as _ from common.db import models from common.utils import lazyproperty -from orgs.models import Organization -from orgs.utils import get_current_org from assets.models import Asset, SystemUser, Node, FamilyMixin from .base import BasePermission @@ -68,6 +65,11 @@ class Action: choices = [cls.NAME_MAP[i] for i, j in cls.DB_CHOICES if value & i == i] return choices + @classmethod + def value_to_choices_display(cls, value): + choices = cls.value_to_choices(value) + return [str(dict(cls.choices())[i]) for i in choices] + @classmethod def choices_to_value(cls, value): if not isinstance(value, list): diff --git a/apps/perms/models/database_app_permission.py b/apps/perms/models/database_app_permission.py deleted file mode 100644 index 91b989128..000000000 --- a/apps/perms/models/database_app_permission.py +++ /dev/null @@ -1,39 +0,0 @@ -# coding: utf-8 -# - -from django.db import models -from django.utils.translation import ugettext_lazy as _ - -from common.utils import lazyproperty -from .base import BasePermission - -__all__ = [ - 'DatabaseAppPermission', -] - - -class DatabaseAppPermission(BasePermission): - database_apps = models.ManyToManyField( - 'applications.DatabaseApp', related_name='granted_by_permissions', - blank=True, verbose_name=_("DatabaseApp") - ) - system_users = models.ManyToManyField( - 'assets.SystemUser', related_name='granted_by_database_app_permissions', - verbose_name=_("System user") - ) - - class Meta: - unique_together = [('org_id', 'name')] - verbose_name = _('DatabaseApp permission') - ordering = ('name',) - - def get_all_database_apps(self): - return self.database_apps.all() - - @lazyproperty - def database_apps_amount(self): - return self.database_apps.count() - - @lazyproperty - def system_users_amount(self): - return self.system_users.count() diff --git a/apps/perms/models/k8s_app_permission.py b/apps/perms/models/k8s_app_permission.py deleted file mode 100644 index b6c7106bc..000000000 --- a/apps/perms/models/k8s_app_permission.py +++ /dev/null @@ -1,39 +0,0 @@ -# coding: utf-8 -# - -from django.db import models -from django.utils.translation import ugettext_lazy as _ - -from common.utils import lazyproperty -from .base import BasePermission - -__all__ = [ - 'K8sAppPermission', -] - - -class K8sAppPermission(BasePermission): - k8s_apps = models.ManyToManyField( - 'applications.K8sApp', related_name='granted_by_permissions', - blank=True, verbose_name=_("KubernetesApp") - ) - system_users = models.ManyToManyField( - 'assets.SystemUser', related_name='granted_by_k8s_app_permissions', - verbose_name=_("System user") - ) - - class Meta: - unique_together = [('org_id', 'name')] - verbose_name = _('KubernetesApp permission') - ordering = ('name',) - - def get_all_k8s_apps(self): - return self.k8s_apps.all() - - @lazyproperty - def k8s_apps_amount(self): - return self.k8s_apps.count() - - @lazyproperty - def system_users_amount(self): - return self.system_users.count() diff --git a/apps/perms/models/remote_app_permission.py b/apps/perms/models/remote_app_permission.py deleted file mode 100644 index 40114875c..000000000 --- a/apps/perms/models/remote_app_permission.py +++ /dev/null @@ -1,36 +0,0 @@ -# coding: utf-8 -# -from django.db import models -from django.utils.translation import ugettext_lazy as _ - -from common.utils import lazyproperty -from .base import BasePermission - -__all__ = [ - 'RemoteAppPermission', -] - - -class RemoteAppPermission(BasePermission): - remote_apps = models.ManyToManyField('applications.RemoteApp', related_name='granted_by_permissions', blank=True, verbose_name=_("RemoteApp")) - system_users = models.ManyToManyField('assets.SystemUser', related_name='granted_by_remote_app_permissions', verbose_name=_("System user")) - - class Meta: - unique_together = [('org_id', 'name')] - verbose_name = _('RemoteApp permission') - ordering = ('name',) - - def get_all_remote_apps(self): - return set(self.remote_apps.all()) - - @property - def all_remote_apps(self): - return self.remote_apps.all() - - @lazyproperty - def remote_apps_amount(self): - return self.remote_apps.count() - - @lazyproperty - def system_users_amount(self): - return self.system_users.count() diff --git a/apps/perms/serializers/__init__.py b/apps/perms/serializers/__init__.py index 2ecfc8172..9fb257580 100644 --- a/apps/perms/serializers/__init__.py +++ b/apps/perms/serializers/__init__.py @@ -3,12 +3,3 @@ from .asset import * from .application import * from .system_user_permission import * - -# TODO: 删除 -from .remote_app_permission import * -from .remote_app_permission_relation import * -from .database_app_permission import * -from .database_app_permission_relation import * -from .base import * -from .k8s_app_permission import * -from .k8s_app_permission_relation import * diff --git a/apps/perms/serializers/application/permission_relation.py b/apps/perms/serializers/application/permission_relation.py index 94ea8e608..2ec416a66 100644 --- a/apps/perms/serializers/application/permission_relation.py +++ b/apps/perms/serializers/application/permission_relation.py @@ -3,7 +3,7 @@ from rest_framework import serializers from common.mixins import BulkSerializerMixin -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from perms.models import ApplicationPermission __all__ = [ diff --git a/apps/perms/serializers/application/user_permission.py b/apps/perms/serializers/application/user_permission.py index d2a0f0c75..5c722852b 100644 --- a/apps/perms/serializers/application/user_permission.py +++ b/apps/perms/serializers/application/user_permission.py @@ -6,10 +6,10 @@ from django.utils.translation import ugettext_lazy as _ from assets.models import SystemUser from applications.models import Application +from applications.serializers import ApplicationSerializerMixin __all__ = [ - 'ApplicationGrantedSerializer', - 'ApplicationSystemUserSerializer' + 'ApplicationGrantedSerializer', 'ApplicationSystemUserSerializer' ] @@ -26,7 +26,7 @@ class ApplicationSystemUserSerializer(serializers.ModelSerializer): read_only_fields = fields -class ApplicationGrantedSerializer(serializers.ModelSerializer): +class ApplicationGrantedSerializer(ApplicationSerializerMixin, serializers.ModelSerializer): """ 被授权应用的数据结构 """ diff --git a/apps/perms/serializers/asset/permission_relation.py b/apps/perms/serializers/asset/permission_relation.py index 1081f76a0..f98c4bbf0 100644 --- a/apps/perms/serializers/asset/permission_relation.py +++ b/apps/perms/serializers/asset/permission_relation.py @@ -3,7 +3,7 @@ from rest_framework import serializers from common.mixins import BulkSerializerMixin -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from assets.models import Asset, Node from perms.models import AssetPermission from users.models import User diff --git a/apps/perms/serializers/base.py b/apps/perms/serializers/base.py deleted file mode 100644 index 33de4980b..000000000 --- a/apps/perms/serializers/base.py +++ /dev/null @@ -1,13 +0,0 @@ -from rest_framework import serializers - - -class PermissionAllUserSerializer(serializers.Serializer): - user = serializers.UUIDField(read_only=True, source='id') - user_display = serializers.SerializerMethodField() - - class Meta: - only_fields = ['id', 'username', 'name'] - - @staticmethod - def get_user_display(obj): - return str(obj) diff --git a/apps/perms/serializers/database_app_permission.py b/apps/perms/serializers/database_app_permission.py deleted file mode 100644 index 3cd7f3245..000000000 --- a/apps/perms/serializers/database_app_permission.py +++ /dev/null @@ -1,67 +0,0 @@ -# coding: utf-8 -# -from django.db.models import Count -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from common.serializers import AdaptedBulkListSerializer -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from .. import models - -__all__ = [ - 'DatabaseAppPermissionSerializer', 'DatabaseAppPermissionListSerializer' -] - - -class AmountMixin: - @classmethod - def setup_eager_loading(cls, queryset): - """ Perform necessary eager loading of data. """ - queryset = queryset.annotate( - users_amount=Count('users', distinct=True), user_groups_amount=Count('user_groups', distinct=True), - database_apps_amount=Count('database_apps', distinct=True), - system_users_amount=Count('system_users', distinct=True) - ) - return queryset - - -class DatabaseAppPermissionSerializer(AmountMixin, BulkOrgResourceModelSerializer): - class Meta: - model = models.DatabaseAppPermission - list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'name', 'users', 'user_groups', 'database_apps', 'system_users', - 'comment', 'is_active', 'date_start', 'date_expired', 'is_valid', - 'created_by', 'date_created', 'users_amount', 'user_groups_amount', - 'database_apps_amount', 'system_users_amount', - ] - read_only_fields = [ - 'created_by', 'date_created', 'users_amount', 'user_groups_amount', - 'database_apps_amount', 'system_users_amount', - ] - extra_kwargs = { - 'is_valid': {'label': _('Is valid')}, - 'users_amount': {'label': _('Users amount')}, - 'user_groups_amount': {'label': _('User groups amount')}, - 'system_users_amount': {'label': _('System users amount')}, - 'database_apps_amount': {'label': _('Database apps amount')}, - } - - -class DatabaseAppPermissionListSerializer(AmountMixin, BulkOrgResourceModelSerializer): - is_expired = serializers.BooleanField() - - class Meta: - model = models.DatabaseAppPermission - fields = [ - 'id', 'name', 'comment', 'is_active', 'users_amount', 'user_groups_amount', - 'date_start', 'date_expired', 'is_valid', 'database_apps_amount', 'system_users_amount', - 'created_by', 'date_created', 'is_expired' - ] - extra_kwargs = { - 'is_valid': {'label': _('Is valid')}, - 'users_amount': {'label': _('Users amount')}, - 'user_groups_amount': {'label': _('User groups amount')}, - 'system_users_amount': {'label': _('System users amount')}, - 'database_apps_amount': {'label': _('Database apps amount')}, - } diff --git a/apps/perms/serializers/database_app_permission_relation.py b/apps/perms/serializers/database_app_permission_relation.py deleted file mode 100644 index deb761853..000000000 --- a/apps/perms/serializers/database_app_permission_relation.py +++ /dev/null @@ -1,87 +0,0 @@ -# coding: utf-8 -# -from perms.serializers.base import PermissionAllUserSerializer -from rest_framework import serializers - -from common.mixins import BulkSerializerMixin -from common.serializers import AdaptedBulkListSerializer - -from .. import models - -__all__ = [ - 'DatabaseAppPermissionUserRelationSerializer', - 'DatabaseAppPermissionUserGroupRelationSerializer', - 'DatabaseAppPermissionAllUserSerializer', - 'DatabaseAppPermissionDatabaseAppRelationSerializer', - 'DatabaseAppPermissionAllDatabaseAppSerializer', - 'DatabaseAppPermissionSystemUserRelationSerializer', -] - - -class RelationMixin(BulkSerializerMixin, serializers.Serializer): - databaseapppermission_display = serializers.ReadOnlyField() - - def get_field_names(self, declared_fields, info): - fields = super().get_field_names(declared_fields, info) - fields.extend(['databaseapppermission', "databaseapppermission_display"]) - return fields - - class Meta: - list_serializer_class = AdaptedBulkListSerializer - - -class DatabaseAppPermissionUserRelationSerializer(RelationMixin, serializers.ModelSerializer): - user_display = serializers.ReadOnlyField() - - class Meta(RelationMixin.Meta): - model = models.DatabaseAppPermission.users.through - fields = [ - 'id', 'user', 'user_display', - ] - - -class DatabaseAppPermissionUserGroupRelationSerializer(RelationMixin, serializers.ModelSerializer): - usergroup_display = serializers.ReadOnlyField() - - class Meta(RelationMixin.Meta): - model = models.DatabaseAppPermission.user_groups.through - fields = [ - 'id', 'usergroup', "usergroup_display", - ] - - -class DatabaseAppPermissionAllUserSerializer(PermissionAllUserSerializer): - class Meta(PermissionAllUserSerializer.Meta): - pass - - -class DatabaseAppPermissionDatabaseAppRelationSerializer(RelationMixin, serializers.ModelSerializer): - databaseapp_display = serializers.ReadOnlyField() - - class Meta(RelationMixin.Meta): - model = models.DatabaseAppPermission.database_apps.through - fields = [ - 'id', "databaseapp", "databaseapp_display", - ] - - -class DatabaseAppPermissionAllDatabaseAppSerializer(serializers.Serializer): - databaseapp = serializers.UUIDField(read_only=True, source='id') - databaseapp_display = serializers.SerializerMethodField() - - class Meta: - only_fields = ['id', 'name'] - - @staticmethod - def get_databaseapp_display(obj): - return str(obj) - - -class DatabaseAppPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer): - systemuser_display = serializers.ReadOnlyField() - - class Meta(RelationMixin.Meta): - model = models.DatabaseAppPermission.system_users.through - fields = [ - 'id', 'systemuser', 'systemuser_display' - ] diff --git a/apps/perms/serializers/k8s_app_permission.py b/apps/perms/serializers/k8s_app_permission.py deleted file mode 100644 index 96f23ace6..000000000 --- a/apps/perms/serializers/k8s_app_permission.py +++ /dev/null @@ -1,65 +0,0 @@ -# coding: utf-8 -# -from django.db.models import Count -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from .. import models - -__all__ = [ - 'K8sAppPermissionSerializer', 'K8sAppPermissionListSerializer' -] - - -class AmountMixin: - @classmethod - def setup_eager_loading(cls, queryset): - """ Perform necessary eager loading of data. """ - queryset = queryset.annotate( - users_amount=Count('users', distinct=True), user_groups_amount=Count('user_groups', distinct=True), - k8s_apps_amount=Count('k8s_apps', distinct=True), - system_users_amount=Count('system_users', distinct=True) - ) - return queryset - - -class K8sAppPermissionSerializer(AmountMixin, BulkOrgResourceModelSerializer): - class Meta: - model = models.K8sAppPermission - fields = [ - 'id', 'name', 'users', 'user_groups', 'k8s_apps', 'system_users', - 'comment', 'is_active', 'date_start', 'date_expired', 'is_valid', - 'created_by', 'date_created', 'users_amount', 'user_groups_amount', - 'k8s_apps_amount', 'system_users_amount', - ] - read_only_fields = [ - 'created_by', 'date_created', 'users_amount', 'user_groups_amount', - 'k8s_apps_amount', 'system_users_amount', 'id' - ] - extra_kwargs = { - 'is_valid': {'label': _('Is valid')}, - 'users_amount': {'label': _('Users amount')}, - 'user_groups_amount': {'label': _('User groups amount')}, - 'system_users_amount': {'label': _('System users amount')}, - 'database_apps_amount': {'label': _('Database apps amount')}, - } - - -class K8sAppPermissionListSerializer(AmountMixin, BulkOrgResourceModelSerializer): - is_expired = serializers.BooleanField() - - class Meta: - model = models.K8sAppPermission - fields = [ - 'id', 'name', 'comment', 'is_active', 'users_amount', 'user_groups_amount', - 'date_start', 'date_expired', 'is_valid', 'k8s_apps_amount', 'system_users_amount', - 'created_by', 'date_created', 'is_expired' - ] - extra_kwargs = { - 'is_valid': {'label': _('Is valid')}, - 'users_amount': {'label': _('Users amount')}, - 'user_groups_amount': {'label': _('User groups amount')}, - 'system_users_amount': {'label': _('System users amount')}, - 'k8s_apps_amount': {'label': _('K8s apps amount')}, - } diff --git a/apps/perms/serializers/k8s_app_permission_relation.py b/apps/perms/serializers/k8s_app_permission_relation.py deleted file mode 100644 index e5786ca50..000000000 --- a/apps/perms/serializers/k8s_app_permission_relation.py +++ /dev/null @@ -1,73 +0,0 @@ -# coding: utf-8 -# -from perms.serializers.base import PermissionAllUserSerializer -from rest_framework import serializers - -from common.drf.serializers import BulkModelSerializer - -from .. import models - - -class K8sAppPermissionUserRelationSerializer(BulkModelSerializer): - user_display = serializers.ReadOnlyField() - k8sapppermission_display = serializers.ReadOnlyField() - - class Meta: - model = models.K8sAppPermission.users.through - fields = [ - 'id', 'user', 'user_display', 'k8sapppermission', - 'k8sapppermission_display' - ] - - -class K8sAppPermissionUserGroupRelationSerializer(BulkModelSerializer): - usergroup_display = serializers.ReadOnlyField() - k8sapppermission_display = serializers.ReadOnlyField() - - class Meta: - model = models.K8sAppPermission.user_groups.through - fields = [ - 'id', 'usergroup', 'usergroup_display', 'k8sapppermission', - 'k8sapppermission_display' - ] - - -class K8sAppPermissionAllUserSerializer(PermissionAllUserSerializer): - class Meta(PermissionAllUserSerializer.Meta): - pass - - -class K8sAppPermissionK8sAppRelationSerializer(BulkModelSerializer): - k8sapp_display = serializers.ReadOnlyField() - k8sapppermission_display = serializers.ReadOnlyField() - - class Meta: - model = models.K8sAppPermission.k8s_apps.through - fields = [ - 'id', "k8sapp", "k8sapp_display", 'k8sapppermission', - 'k8sapppermission_display' - ] - - -class K8sAppPermissionAllK8sAppSerializer(serializers.Serializer): - k8sapp = serializers.UUIDField(read_only=True, source='id') - k8sapp_display = serializers.SerializerMethodField() - - class Meta: - only_fields = ['id', 'name'] - - @staticmethod - def get_k8sapp_display(obj): - return str(obj) - - -class K8sAppPermissionSystemUserRelationSerializer(BulkModelSerializer): - systemuser_display = serializers.ReadOnlyField() - k8sapppermission_display = serializers.ReadOnlyField() - - class Meta: - model = models.K8sAppPermission.system_users.through - fields = [ - 'id', 'systemuser', 'systemuser_display', 'k8sapppermission', - 'k8sapppermission_display' - ] diff --git a/apps/perms/serializers/remote_app_permission.py b/apps/perms/serializers/remote_app_permission.py deleted file mode 100644 index 75a20436e..000000000 --- a/apps/perms/serializers/remote_app_permission.py +++ /dev/null @@ -1,62 +0,0 @@ -# coding: utf-8 -# -from rest_framework import serializers -from django.db.models import Count -from django.utils.translation import ugettext_lazy as _ - -from common.serializers import AdaptedBulkListSerializer -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from ..models import RemoteAppPermission - - -__all__ = [ - 'RemoteAppPermissionSerializer', - 'RemoteAppPermissionUpdateUserSerializer', - 'RemoteAppPermissionUpdateRemoteAppSerializer', -] - - -class RemoteAppPermissionSerializer(BulkOrgResourceModelSerializer): - class Meta: - model = RemoteAppPermission - list_serializer_class = AdaptedBulkListSerializer - mini_fields = ['id', 'name'] - small_fields = mini_fields + [ - 'comment', 'is_active', 'date_start', 'date_expired', 'is_valid', - 'created_by', 'date_created' - ] - m2m_fields = [ - 'users', 'user_groups', 'remote_apps', 'system_users', - 'users_amount', 'user_groups_amount', 'remote_apps_amount', - 'system_users_amount' - ] - fields = small_fields + m2m_fields - read_only_fields = ['created_by', 'date_created'] - extra_kwargs = { - 'is_valid': {'label': _('Is valid')}, - 'users_amount': {'label': _('Users amount')}, - 'user_groups_amount': {'label': _('User groups amount')}, - 'system_users_amount': {'label': _('System users amount')}, - 'remote_apps_amount': {'label': _('Remote apps amount')}, - } - - @classmethod - def setup_eager_loading(cls, queryset): - """ Perform necessary eager loading of data. """ - queryset = queryset.annotate( - users_amount=Count('users', distinct=True), user_groups_amount=Count('user_groups', distinct=True), - remote_apps_amount=Count('remote_apps', distinct=True), system_users_amount=Count('system_users', distinct=True) - ) - return queryset - - -class RemoteAppPermissionUpdateUserSerializer(serializers.ModelSerializer): - class Meta: - model = RemoteAppPermission - fields = ['id', 'users'] - - -class RemoteAppPermissionUpdateRemoteAppSerializer(serializers.ModelSerializer): - class Meta: - model = RemoteAppPermission - fields = ['id', 'remote_apps'] diff --git a/apps/perms/serializers/remote_app_permission_relation.py b/apps/perms/serializers/remote_app_permission_relation.py deleted file mode 100644 index 05d06a9da..000000000 --- a/apps/perms/serializers/remote_app_permission_relation.py +++ /dev/null @@ -1,49 +0,0 @@ -# coding: utf-8 -# -from rest_framework import serializers - -from common.serializers import AdaptedBulkListSerializer -from ..models import RemoteAppPermission - - -__all__ = [ - 'RemoteAppPermissionRemoteAppRelationSerializer', - 'RemoteAppPermissionAllRemoteAppSerializer', - 'RemoteAppPermissionUserRelationSerializer', -] - - -class RemoteAppPermissionRemoteAppRelationSerializer(serializers.ModelSerializer): - remoteapp_display = serializers.ReadOnlyField() - remoteapppermission_display = serializers.ReadOnlyField() - - class Meta: - model = RemoteAppPermission.remote_apps.through - list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'remoteapp', 'remoteapp_display', 'remoteapppermission', 'remoteapppermission_display' - ] - - -class RemoteAppPermissionAllRemoteAppSerializer(serializers.Serializer): - remoteapp = serializers.UUIDField(read_only=True, source='id') - remoteapp_display = serializers.SerializerMethodField() - - class Meta: - only_fields = ['id', 'name'] - - @staticmethod - def get_remoteapp_display(obj): - return str(obj) - - -class RemoteAppPermissionUserRelationSerializer(serializers.ModelSerializer): - user_display = serializers.ReadOnlyField() - remoteapppermission_display = serializers.ReadOnlyField() - - class Meta: - model = RemoteAppPermission.users.through - list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'user', 'user_display', 'remoteapppermission', 'remoteapppermission_display' - ] diff --git a/apps/perms/signals_handler.py b/apps/perms/signals_handler.py index 95df4c4f2..9e0bfbaeb 100644 --- a/apps/perms/signals_handler.py +++ b/apps/perms/signals_handler.py @@ -7,11 +7,11 @@ from perms.tasks import create_rebuild_user_tree_task, \ create_rebuild_user_tree_task_by_related_nodes_or_assets from users.models import User, UserGroup from assets.models import Asset, SystemUser -from applications.models import Application, Category +from applications.models import Application from common.utils import get_logger from common.exceptions import M2MReverseNotAllowed from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR -from .models import AssetPermission, RemoteAppPermission, ApplicationPermission +from .models import AssetPermission, ApplicationPermission logger = get_logger(__file__) @@ -187,51 +187,6 @@ def on_asset_permission_user_groups_changed(instance, action, pk_set, model, system_user.groups.add(*tuple(groups)) -@receiver(m2m_changed, sender=RemoteAppPermission.system_users.through) -def on_remote_app_permission_system_users_changed(sender, instance=None, - action='', reverse=False, **kwargs): - if action != POST_ADD or reverse: - return - system_users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) - logger.debug("Remote app permission system_users change signal received") - assets = instance.remote_apps.all().values_list('asset__id', flat=True) - users = instance.users.all().values_list('id', flat=True) - groups = instance.user_groups.all().values_list('id', flat=True) - for system_user in system_users: - system_user.assets.add(*tuple(assets)) - if system_user.username_same_with_user: - system_user.groups.add(*tuple(groups)) - system_user.users.add(*tuple(users)) - - -@receiver(m2m_changed, sender=RemoteAppPermission.users.through) -def on_remoteapps_permission_users_changed(sender, instance=None, action='', - reverse=False, **kwargs): - if action != POST_ADD and reverse: - return - logger.debug("Asset permission users change signal received") - users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) - system_users = instance.system_users.all() - - for system_user in system_users: - if system_user.username_same_with_user: - system_user.users.add(*tuple(users)) - - -@receiver(m2m_changed, sender=RemoteAppPermission.user_groups.through) -def on_remoteapps_permission_user_groups_changed(sender, instance=None, action='', - reverse=False, **kwargs): - if action != POST_ADD and reverse: - return - logger.debug("Asset permission user groups change signal received") - groups = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) - system_users = instance.system_users.all() - - for system_user in system_users: - if system_user.username_same_with_user: - system_user.groups.add(*tuple(groups)) - - @receiver(m2m_changed, sender=Asset.nodes.through) def on_node_asset_change(action, instance, reverse, pk_set, **kwargs): if not need_rebuild_mapping_node(action): @@ -249,7 +204,7 @@ def on_node_asset_change(action, instance, reverse, pk_set, **kwargs): @receiver(m2m_changed, sender=ApplicationPermission.system_users.through) def on_application_permission_system_users_changed(sender, instance: ApplicationPermission, action, reverse, pk_set, **kwargs): - if instance.category != Category.remote_app: + if not instance.category_remote_app: return if reverse: @@ -277,7 +232,7 @@ def on_application_permission_system_users_changed(sender, instance: Application @receiver(m2m_changed, sender=ApplicationPermission.users.through) def on_application_permission_users_changed(sender, instance, action, reverse, pk_set, **kwargs): - if instance.category != Category.remote_app: + if not instance.category_remote_app: return if reverse: @@ -297,7 +252,7 @@ def on_application_permission_users_changed(sender, instance, action, reverse, p @receiver(m2m_changed, sender=ApplicationPermission.user_groups.through) def on_application_permission_user_groups_changed(sender, instance, action, reverse, pk_set, **kwargs): - if instance.category != Category.remote_app: + if not instance.category_remote_app: return if reverse: @@ -317,7 +272,7 @@ def on_application_permission_user_groups_changed(sender, instance, action, reve @receiver(m2m_changed, sender=ApplicationPermission.applications.through) def on_application_permission_applications_changed(sender, instance, action, reverse, pk_set, **kwargs): - if instance.category != Category.remote_app: + if not instance.category_remote_app: return if reverse: diff --git a/apps/perms/urls/api_urls.py b/apps/perms/urls/api_urls.py index 97deef6bd..c2dfe2380 100644 --- a/apps/perms/urls/api_urls.py +++ b/apps/perms/urls/api_urls.py @@ -4,11 +4,7 @@ from django.urls import re_path from common import api as capi from .asset_permission import asset_permission_urlpatterns from .application_permission import application_permission_urlpatterns - -from .remote_app_permission import remote_app_permission_urlpatterns -from .database_app_permission import database_app_permission_urlpatterns from .system_user_permission import system_users_permission_urlpatterns -from .k8s_app_permission import k8s_app_permission_urlpatterns app_name = 'perms' @@ -16,10 +12,8 @@ old_version_urlpatterns = [ re_path('(?Puser|user-group|asset-permission|remote-app-permission)/.*', capi.redirect_plural_name_api) ] -urlpatterns = asset_permission_urlpatterns + \ - application_permission_urlpatterns + \ - remote_app_permission_urlpatterns + \ - database_app_permission_urlpatterns + \ - k8s_app_permission_urlpatterns + \ - old_version_urlpatterns + \ - system_users_permission_urlpatterns +urlpatterns = [] +urlpatterns += asset_permission_urlpatterns +urlpatterns += application_permission_urlpatterns +urlpatterns += system_users_permission_urlpatterns +urlpatterns += old_version_urlpatterns diff --git a/apps/perms/urls/database_app_permission.py b/apps/perms/urls/database_app_permission.py deleted file mode 100644 index a793f980e..000000000 --- a/apps/perms/urls/database_app_permission.py +++ /dev/null @@ -1,47 +0,0 @@ -# coding: utf-8 -# - -from django.urls import path, include -from rest_framework_bulk.routes import BulkRouter -from .. import api - - -router = BulkRouter() -router.register('database-app-permissions', api.DatabaseAppPermissionViewSet, 'database-app-permission') -router.register('database-app-permissions-users-relations', api.DatabaseAppPermissionUserRelationViewSet, 'database-app-permissions-users-relation') -router.register('database-app-permissions-user-groups-relations', api.DatabaseAppPermissionUserGroupRelationViewSet, 'database-app-permissions-user-groups-relation') -router.register('database-app-permissions-database-apps-relations', api.DatabaseAppPermissionDatabaseAppRelationViewSet, 'database-app-permissions-database-apps-relation') -router.register('database-app-permissions-system-users-relations', api.DatabaseAppPermissionSystemUserRelationViewSet, 'database-app-permissions-system-users-relation') - -user_permission_urlpatterns = [ - path('/database-apps/', api.UserGrantedDatabaseAppsApi.as_view(), name='user-database-apps'), - path('database-apps/', api.UserGrantedDatabaseAppsApi.as_view(), name='my-database-apps'), - - # DatabaseApps as tree - path('/database-apps/tree/', api.UserGrantedDatabaseAppsAsTreeApi.as_view(), name='user-databases-apps-tree'), - path('database-apps/tree/', api.UserGrantedDatabaseAppsAsTreeApi.as_view(), name='my-databases-apps-tree'), - - path('/database-apps//system-users/', api.UserGrantedDatabaseAppSystemUsersApi.as_view(), name='user-database-app-system-users'), - path('database-apps//system-users/', api.UserGrantedDatabaseAppSystemUsersApi.as_view(), name='user-database-app-system-users'), -] - -user_group_permission_urlpatterns = [ - path('/database-apps/', api.UserGroupGrantedDatabaseAppsApi.as_view(), name='user-group-database-apps'), -] - -permission_urlpatterns = [ - # 授权规则中授权的用户和数据库应用 - path('/users/all/', api.DatabaseAppPermissionAllUserListApi.as_view(), name='database-app-permission-all-users'), - path('/database-apps/all/', api.DatabaseAppPermissionAllDatabaseAppListApi.as_view(), name='database-app-permission-all-database-apps'), - - # 验证用户是否有某个数据库应用的权限 - path('user/validate/', api.ValidateUserDatabaseAppPermissionApi.as_view(), name='validate-user-database-app-permission'), -] - -database_app_permission_urlpatterns = [ - path('users/', include(user_permission_urlpatterns)), - path('user-groups/', include(user_group_permission_urlpatterns)), - path('database-app-permissions/', include(permission_urlpatterns)) -] - -database_app_permission_urlpatterns += router.urls diff --git a/apps/perms/urls/k8s_app_permission.py b/apps/perms/urls/k8s_app_permission.py deleted file mode 100644 index 2c145948b..000000000 --- a/apps/perms/urls/k8s_app_permission.py +++ /dev/null @@ -1,45 +0,0 @@ -# coding: utf-8 -# - -from django.urls import path, include -from rest_framework_bulk.routes import BulkRouter -from .. import api - - -router = BulkRouter() -router.register('k8s-app-permissions', api.K8sAppPermissionViewSet, 'k8s-app-permission') -router.register('k8s-app-permissions-users-relations', api.K8sAppPermissionUserRelationViewSet, 'k8s-app-permissions-users-relation') -router.register('k8s-app-permissions-user-groups-relations', api.K8sAppPermissionUserGroupRelationViewSet, 'k8s-app-permissions-user-groups-relation') -router.register('k8s-app-permissions-k8s-apps-relations', api.K8sAppPermissionK8sAppRelationViewSet, 'k8s-app-permissions-k8s-apps-relation') -router.register('k8s-app-permissions-system-users-relations', api.K8sAppPermissionSystemUserRelationViewSet, 'k8s-app-permissions-system-users-relation') - -user_permission_urlpatterns = [ - path('/k8s-apps/', api.UserGrantedK8sAppsApi.as_view(), name='user-k8s-apps'), - path('k8s-apps/', api.UserGrantedK8sAppsApi.as_view(), name='my-k8s-apps'), - - # k8sApps as tree - path('/k8s-apps/tree/', api.UserGrantedK8sAppsAsTreeApi.as_view(), name='user-k8ss-apps-tree'), - path('k8s-apps/tree/', api.UserGrantedK8sAppsAsTreeApi.as_view(), name='my-k8ss-apps-tree'), - - path('/k8s-apps//system-users/', api.UserGrantedK8sAppSystemUsersApi.as_view(), name='user-k8s-app-system-users'), - path('k8s-apps//system-users/', api.UserGrantedK8sAppSystemUsersApi.as_view(), name='user-k8s-app-system-users'), -] - -user_group_permission_urlpatterns = [ - path('/k8s-apps/', api.UserGroupGrantedK8sAppsApi.as_view(), name='user-group-k8s-apps'), -] - -permission_urlpatterns = [ - path('/users/all/', api.K8sAppPermissionAllUserListApi.as_view(), name='k8s-app-permission-all-users'), - path('/k8s-apps/all/', api.K8sAppPermissionAllK8sAppListApi.as_view(), name='k8s-app-permission-all-k8s-apps'), - - path('user/validate/', api.ValidateUserK8sAppPermissionApi.as_view(), name='validate-user-k8s-app-permission'), -] - -k8s_app_permission_urlpatterns = [ - path('users/', include(user_permission_urlpatterns)), - path('user-groups/', include(user_group_permission_urlpatterns)), - path('k8s-app-permissions/', include(permission_urlpatterns)) -] - -k8s_app_permission_urlpatterns += router.urls diff --git a/apps/perms/urls/remote_app_permission.py b/apps/perms/urls/remote_app_permission.py deleted file mode 100644 index 798ca9639..000000000 --- a/apps/perms/urls/remote_app_permission.py +++ /dev/null @@ -1,43 +0,0 @@ -# coding:utf-8 - -from django.urls import path -from rest_framework_bulk.routes import BulkRouter -from .. import api - - -router = BulkRouter() -router.register('remote-app-permissions', api.RemoteAppPermissionViewSet, 'remote-app-permission') -router.register('remote-app-permissions-users-relations', api.RemoteAppPermissionUserRelationViewSet, 'remote-app-permissions-users-relation') -router.register('remote-app-permissions-remote-apps-relations', api.RemoteAppPermissionRemoteAppRelationViewSet, 'remote-app-permissions-remote-apps-relation') - - -remote_app_permission_urlpatterns = [ - # 查询用户授权的RemoteApp - path('users//remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='user-remote-apps'), - path('users/remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='my-remote-apps'), - - # 获取用户授权的RemoteApp树 - path('users//remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='user-remote-apps-as-tree'), - path('users/remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='my-remote-apps-as-tree'), - - # 查询用户组授权的RemoteApp - path('user-groups//remote-apps/', api.UserGroupGrantedRemoteAppsApi.as_view(), name='user-group-remote-apps'), - - # RemoteApp System users - path('users//remote-apps//system-users/', api.UserGrantedRemoteAppSystemUsersApi.as_view(), name='user-remote-app-system-users'), - path('users/remote-apps//system-users/', api.UserGrantedRemoteAppSystemUsersApi.as_view(), name='my-remote-app-system-users'), - - # 校验用户对RemoteApp的权限 - path('remote-app-permissions/user/validate/', api.ValidateUserRemoteAppPermissionApi.as_view(), name='validate-user-remote-app-permission'), - - # 用户和RemoteApp变更 - path('remote-app-permissions//users/add/', api.RemoteAppPermissionAddUserApi.as_view(), name='remote-app-permission-add-user'), - path('remote-app-permissions//users/remove/', api.RemoteAppPermissionRemoveUserApi.as_view(), name='remote-app-permission-remove-user'), - path('remote-app-permissions//remote-apps/remove/', api.RemoteAppPermissionRemoveRemoteAppApi.as_view(), name='remote-app-permission-remove-remote-app'), - path('remote-app-permissions//remote-apps/add/', api.RemoteAppPermissionAddRemoteAppApi.as_view(), name='remote-app-permission-add-remote-app'), - - path('remote-app-permissions//remote-apps/all/', api.RemoteAppPermissionAllRemoteAppListApi.as_view(), name='remote-app-permission-all-remote-apps'), - path('remote-app-permissions//users/all/', api.RemoteAppPermissionAllUserListApi.as_view(), name='remote-app-permission-all-users'), -] - -remote_app_permission_urlpatterns += router.urls diff --git a/apps/perms/urls/views_urls.py b/apps/perms/urls/views_urls.py deleted file mode 100644 index 89d8c241a..000000000 --- a/apps/perms/urls/views_urls.py +++ /dev/null @@ -1,5 +0,0 @@ -# coding:utf-8 -app_name = 'perms' - -urlpatterns = [ -] diff --git a/apps/perms/utils/__init__.py b/apps/perms/utils/__init__.py index c29f9a2ae..e204cd61b 100644 --- a/apps/perms/utils/__init__.py +++ b/apps/perms/utils/__init__.py @@ -3,8 +3,3 @@ from .asset import * from .application import * - -# TODO: 删除 -from .remote_app_permission import * -from .database_app_permission import * -from .k8s_app_permission import * diff --git a/apps/perms/utils/database_app_permission.py b/apps/perms/utils/database_app_permission.py deleted file mode 100644 index dd51ca458..000000000 --- a/apps/perms/utils/database_app_permission.py +++ /dev/null @@ -1,100 +0,0 @@ -# coding: utf-8 -# - -from django.utils.translation import ugettext as _ -from django.db.models import Q - -from orgs.utils import set_to_root_org -from ..models import DatabaseAppPermission -from common.tree import TreeNode -from applications.models import DatabaseApp -from assets.models import SystemUser - - -__all__ = [ - 'DatabaseAppPermissionUtil', - 'construct_database_apps_tree_root', - 'parse_database_app_to_tree_node' -] - - -def get_user_database_app_permissions(user, include_group=True): - if include_group: - groups = user.groups.all() - arg = Q(users=user) | Q(user_groups__in=groups) - else: - arg = Q(users=user) - return DatabaseAppPermission.objects.all().valid().filter(arg) - - -def get_user_group_database_app_permission(user_group): - return DatabaseAppPermission.objects.all().valid().filter( - user_groups=user_group - ) - - -class DatabaseAppPermissionUtil: - get_permissions_map = { - 'User': get_user_database_app_permissions, - 'UserGroup': get_user_group_database_app_permission - } - - def __init__(self, obj): - self.object = obj - self.change_org_if_need() - - @staticmethod - def change_org_if_need(): - set_to_root_org() - - @property - def permissions(self): - obj_class = self.object.__class__.__name__ - func = self.get_permissions_map[obj_class] - _permissions = func(self.object) - return _permissions - - def get_database_apps(self): - database_apps = DatabaseApp.objects.filter( - granted_by_permissions__in=self.permissions - ).distinct() - return database_apps - - def get_database_app_system_users(self, database_app): - queryset = self.permissions - kwargs = {'database_apps': database_app} - queryset = queryset.filter(**kwargs) - system_users_ids = queryset.values_list('system_users', flat=True) - system_users_ids = system_users_ids.distinct() - system_users = SystemUser.objects.filter(id__in=system_users_ids) - system_users = system_users.order_by('-priority') - return system_users - - -def construct_database_apps_tree_root(amount): - tree_root = { - 'id': 'ID_DATABASE_APP_ROOT', - 'name': '{} ({})'.format(_('DatabaseApp'), amount), - 'title': 'DatabaseApp', - 'pId': '', - 'open': False, - 'isParent': True, - 'iconSkin': '', - 'meta': {'type': 'database_app'} - } - return TreeNode(**tree_root) - - -def parse_database_app_to_tree_node(parent, database_app): - pid = parent.id if parent else '' - tree_node = { - 'id': database_app.id, - 'name': database_app.name, - 'title': database_app.name, - 'pId': pid, - 'open': False, - 'isParent': False, - 'iconSkin': 'file', - 'meta': {'type': 'database_app'} - } - return TreeNode(**tree_node) diff --git a/apps/perms/utils/k8s_app_permission.py b/apps/perms/utils/k8s_app_permission.py deleted file mode 100644 index 74cf79066..000000000 --- a/apps/perms/utils/k8s_app_permission.py +++ /dev/null @@ -1,93 +0,0 @@ -# coding: utf-8 -# - -from django.utils.translation import ugettext as _ -from django.db.models import Q - -from orgs.utils import set_to_root_org -from ..models import K8sAppPermission -from common.tree import TreeNode -from applications.models import K8sApp -from assets.models import SystemUser - - -def get_user_k8s_app_permissions(user, include_group=True): - if include_group: - groups = user.groups.all() - arg = Q(users=user) | Q(user_groups__in=groups) - else: - arg = Q(users=user) - return K8sAppPermission.objects.all().valid().filter(arg) - - -def get_user_group_k8s_app_permission(user_group): - return K8sAppPermission.objects.all().valid().filter( - user_groups=user_group - ) - - -class K8sAppPermissionUtil: - get_permissions_map = { - 'User': get_user_k8s_app_permissions, - 'UserGroup': get_user_group_k8s_app_permission - } - - def __init__(self, obj): - self.object = obj - self.change_org_if_need() - - @staticmethod - def change_org_if_need(): - set_to_root_org() - - @property - def permissions(self): - obj_class = self.object.__class__.__name__ - func = self.get_permissions_map[obj_class] - _permissions = func(self.object) - return _permissions - - def get_k8s_apps(self): - k8s_apps = K8sApp.objects.filter( - granted_by_permissions__in=self.permissions - ).distinct() - return k8s_apps - - def get_k8s_app_system_users(self, k8s_app): - queryset = self.permissions - kwargs = {'k8s_apps': k8s_app} - queryset = queryset.filter(**kwargs) - system_users_ids = queryset.values_list('system_users', flat=True) - system_users_ids = system_users_ids.distinct() - system_users = SystemUser.objects.filter(id__in=system_users_ids) - system_users = system_users.order_by('-priority') - return system_users - - -def construct_k8s_apps_tree_root(amount): - tree_root = { - 'id': 'ID_K8S_APP_ROOT', - 'name': '{} ({})'.format(_('KubernetesApp'), amount), - 'title': 'K8sApp', - 'pId': '', - 'open': False, - 'isParent': True, - 'iconSkin': '', - 'meta': {'type': 'k8s_app'} - } - return TreeNode(**tree_root) - - -def parse_k8s_app_to_tree_node(parent, k8s_app): - pid = parent.id if parent else '' - tree_node = { - 'id': k8s_app.id, - 'name': k8s_app.name, - 'title': k8s_app.name, - 'pId': pid, - 'open': False, - 'isParent': False, - 'iconSkin': 'file', - 'meta': {'type': 'k8s_app'} - } - return TreeNode(**tree_node) diff --git a/apps/perms/utils/remote_app_permission.py b/apps/perms/utils/remote_app_permission.py deleted file mode 100644 index 0fb857611..000000000 --- a/apps/perms/utils/remote_app_permission.py +++ /dev/null @@ -1,99 +0,0 @@ -# coding: utf-8 -# - -from django.utils.translation import ugettext as _ -from django.db.models import Q - -from common.tree import TreeNode -from orgs.utils import set_to_root_org - -from ..models import RemoteAppPermission -from ..hands import RemoteApp, SystemUser - -__all__ = [ - 'RemoteAppPermissionUtil', - 'construct_remote_apps_tree_root', - 'parse_remote_app_to_tree_node', -] - - -def get_user_remote_app_permissions(user, include_group=True): - if include_group: - groups = user.groups.all() - arg = Q(users=user) | Q(user_groups__in=groups) - else: - arg = Q(users=user) - return RemoteAppPermission.objects.all().valid().filter(arg) - - -def get_user_group_remote_app_permissions(user_group): - return RemoteAppPermission.objects.all().valid().filter( - user_groups=user_group - ) - - -class RemoteAppPermissionUtil: - get_permissions_map = { - "User": get_user_remote_app_permissions, - "UserGroup": get_user_group_remote_app_permissions, - } - - def __init__(self, obj): - self.object = obj - self.change_org_if_need() - - @staticmethod - def change_org_if_need(): - set_to_root_org() - - @property - def permissions(self): - obj_class = self.object.__class__.__name__ - func = self.get_permissions_map[obj_class] - _permissions = func(self.object) - return _permissions - - def get_remote_apps(self): - remote_apps = RemoteApp.objects.filter( - granted_by_permissions__in=self.permissions - ).distinct() - return remote_apps - - def get_remote_app_system_users(self, remote_app): - queryset = self.permissions - kwargs = {"remote_apps": remote_app} - queryset = queryset.filter(**kwargs) - system_users_ids = queryset.values_list('system_users', flat=True) - system_users_ids = system_users_ids.distinct() - system_users = SystemUser.objects.filter(id__in=system_users_ids) - system_users = system_users.order_by('-priority') - return system_users - - -def construct_remote_apps_tree_root(amount): - tree_root = { - 'id': 'ID_REMOTE_APP_ROOT', - 'name': '{} ({})'.format(_('RemoteApp'), amount), - 'title': 'RemoteApp', - 'pId': '', - 'open': False, - 'isParent': True, - 'iconSkin': '', - 'meta': {'type': 'remote_app'} - } - return TreeNode(**tree_root) - - -def parse_remote_app_to_tree_node(parent, remote_app): - pid = parent.id if parent else '' - tree_node = { - 'id': remote_app.id, - 'name': remote_app.name, - 'title': remote_app.name, - 'pId': pid, - 'open': False, - 'isParent': False, - 'iconSkin': 'file', - 'meta': {'type': 'remote_app'} - } - return TreeNode(**tree_node) diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index 5d64d9e1a..208c86078 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -62,7 +62,7 @@ class TerminalSettingSerializer(serializers.Serializer): ) TERMINAL_PASSWORD_AUTH = serializers.BooleanField(required=False) TERMINAL_PUBLIC_KEY_AUTH = serializers.BooleanField(required=False) - TERMINAL_HEARTBEAT_INTERVAL = serializers.IntegerField(min_value=5, max_value=99999, required=True) + TERMINAL_HEARTBEAT_INTERVAL = serializers.IntegerField(min_value=5, max_value=99999, required=False) TERMINAL_ASSET_LIST_SORT_BY = serializers.ChoiceField(SORT_BY_CHOICES, required=False) TERMINAL_ASSET_LIST_PAGE_SIZE = serializers.ChoiceField(PAGE_SIZE_CHOICES, required=False) TERMINAL_SESSION_KEEP_DURATION = serializers.IntegerField(min_value=1, max_value=99999, required=True) diff --git a/apps/static/img/login_cas_logo.png b/apps/static/img/login_cas_logo.png new file mode 100644 index 000000000..c6288af5b Binary files /dev/null and b/apps/static/img/login_cas_logo.png differ diff --git a/apps/templates/_base_only_msg_content.html b/apps/templates/_base_only_msg_content.html deleted file mode 100644 index 6d1e781eb..000000000 --- a/apps/templates/_base_only_msg_content.html +++ /dev/null @@ -1,66 +0,0 @@ -{% load static %} -{% load i18n %} - - - - - - - JumpServer - - {% include '_head_css_js.html' %} - - - - - - -
-
-
-

- {% block msg_title %} - {% trans 'Welcome to the JumpServer open source fortress' %} - {% endblock %} -

- {% block msg_content %} -

- {% trans "The world's first fully open source fortress, using the GNU GPL v2.0 open source protocol, is a professional operation and maintenance audit system in compliance with 4A." %} -

-

- {% trans "Developed using Python/Django, following the Web 2.0 specification and equipped with industry-leading Web Terminal solutions, with beautiful interactive interface and good user experience." %} -

-

- {% trans 'Distributed architecture is adopted to support multi-machine room deployment across regions, central node provides API, and each machine room deploys login node, which can be extended horizontally and without concurrent access restrictions.' %} -

-

- {% trans "Changes the world, starting with a little bit." %} -

- {% endblock %} -
-
-
-
- - - {% block content_title %} - {% trans 'Login' %} - {% endblock %} -
- {% block content %} {% endblock %} -
-
-
-
-
-
- {% include '_copyright.html' %} -
-
-
- - diff --git a/apps/terminal/api/command.py b/apps/terminal/api/command.py index 0d5cb1553..cd01df1b3 100644 --- a/apps/terminal/api/command.py +++ b/apps/terminal/api/command.py @@ -28,7 +28,7 @@ __all__ = ['CommandViewSet', 'CommandExportApi', 'InsecureCommandAlertAPI'] class CommandQueryMixin: command_store = get_command_storage() permission_classes = [IsOrgAdminOrAppUser | IsOrgAuditor] - filter_fields = [ + filterset_fields = [ "asset", "system_user", "user", "session", "risk_level", "input" ] diff --git a/apps/terminal/api/session.py b/apps/terminal/api/session.py index 719d7af4f..e8688819e 100644 --- a/apps/terminal/api/session.py +++ b/apps/terminal/api/session.py @@ -42,7 +42,7 @@ class SessionViewSet(OrgBulkModelViewSet): 'display': serializers.SessionDisplaySerializer, } permission_classes = (IsOrgAdminOrAppUser, ) - filter_fields = [ + filterset_fields = [ "user", "asset", "system_user", "remote_addr", "protocol", "terminal", "is_finished", 'login_from', ] diff --git a/apps/terminal/api/storage.py b/apps/terminal/api/storage.py index 0687e213f..be3002060 100644 --- a/apps/terminal/api/storage.py +++ b/apps/terminal/api/storage.py @@ -20,26 +20,26 @@ 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) class CommandStorageViewSet(BaseStorageViewSetMixin, viewsets.ModelViewSet): - filter_fields = ('name', 'type',) - search_fields = filter_fields + filterset_fields = ('name', 'type',) + search_fields = filterset_fields queryset = CommandStorage.objects.all() serializer_class = CommandStorageSerializer permission_classes = (IsSuperUser,) class ReplayStorageViewSet(BaseStorageViewSetMixin, viewsets.ModelViewSet): - filter_fields = ('name', 'type',) - search_fields = filter_fields + filterset_fields = ('name', 'type',) + search_fields = filterset_fields queryset = ReplayStorage.objects.all() serializer_class = ReplayStorageSerializer permission_classes = (IsSuperUser,) @@ -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 a5e976034..14213316c 100644 --- a/apps/terminal/api/terminal.py +++ b/apps/terminal/api/terminal.py @@ -27,7 +27,7 @@ class TerminalViewSet(JMSBulkModelViewSet): queryset = Terminal.objects.filter(is_deleted=False) serializer_class = serializers.TerminalSerializer permission_classes = (IsSuperUser,) - filter_fields = ['name', 'remote_addr', 'type'] + filterset_fields = ['name', 'remote_addr', 'type'] def create(self, request, *args, **kwargs): if isinstance(request.data, list): @@ -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/migrations/0031_auto_20210113_1356.py b/apps/terminal/migrations/0031_auto_20210113_1356.py new file mode 100644 index 000000000..6866b0c2a --- /dev/null +++ b/apps/terminal/migrations/0031_auto_20210113_1356.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1 on 2021-01-13 05:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0030_terminal_type'), + ] + + operations = [ + migrations.AlterField( + model_name='commandstorage', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + migrations.AlterField( + model_name='replaystorage', + name='comment', + field=models.TextField(blank=True, default='', verbose_name='Comment'), + ), + ] diff --git a/apps/terminal/models/storage.py b/apps/terminal/models/storage.py index 66fbe393d..b74feae40 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(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(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/session.py b/apps/terminal/serializers/session.py index 9b1a128e7..d66c3cfa3 100644 --- a/apps/terminal/serializers/session.py +++ b/apps/terminal/serializers/session.py @@ -2,7 +2,7 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from ..models import Session __all__ = [ diff --git a/apps/terminal/serializers/storage.py b/apps/terminal/serializers/storage.py index ea988e597..dcac31c16 100644 --- a/apps/terminal/serializers/storage.py +++ b/apps/terminal/serializers/storage.py @@ -2,74 +2,199 @@ # import copy from rest_framework import serializers - -from common.fields.serializer 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 name'), 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/__init__.py b/apps/tickets/api/__init__.py index 4eb820587..6b519ef80 100644 --- a/apps/tickets/api/__init__.py +++ b/apps/tickets/api/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- # from .ticket import * -from .request_asset_perm import * +from .assignee import * +from .comment import * diff --git a/apps/tickets/api/assignee.py b/apps/tickets/api/assignee.py new file mode 100644 index 000000000..5b223ffcd --- /dev/null +++ b/apps/tickets/api/assignee.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import viewsets + +from common.permissions import IsValidUser +from common.exceptions import JMSException +from users.models import User +from orgs.utils import get_org_by_id +from .. import serializers + + +class AssigneeViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = (IsValidUser,) + serializer_class = serializers.AssigneeSerializer + filterset_fields = ('id', 'name', 'username', 'email', 'source') + search_fields = filterset_fields + + def get_org(self): + org_id = self.request.query_params.get('org_id') + org = get_org_by_id(org_id) + if not org: + error = ('The organization `{}` does not exist'.format(org_id)) + raise JMSException(error) + return org + + def get_queryset(self): + org = self.get_org() + queryset = User.get_super_and_org_admins(org=org) + return queryset diff --git a/apps/tickets/api/comment.py b/apps/tickets/api/comment.py new file mode 100644 index 000000000..dc7740d1a --- /dev/null +++ b/apps/tickets/api/comment.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# + +from rest_framework import viewsets, mixins +from common.exceptions import JMSException +from common.utils import lazyproperty +from tickets import serializers +from tickets.models import Ticket +from tickets.permissions.comment import IsAssignee, IsApplicant, IsSwagger + + +__all__ = ['CommentViewSet'] + + +class CommentViewSet(mixins.CreateModelMixin, viewsets.ReadOnlyModelViewSet): + serializer_class = serializers.CommentSerializer + permission_classes = (IsSwagger | IsAssignee | IsApplicant,) + + @lazyproperty + def ticket(self): + if getattr(self, 'swagger_fake_view', False): + return None + ticket_id = self.request.query_params.get('ticket_id') + ticket = Ticket.all().filter(pk=ticket_id).first() + if not ticket: + raise JMSException('Not found Ticket object about `id={}`'.format(ticket_id)) + return ticket + + def get_serializer_context(self): + context = super().get_serializer_context() + context['ticket'] = self.ticket + return context + + def get_queryset(self): + queryset = self.ticket.comments.all() + return queryset diff --git a/apps/tickets/api/request_asset_perm.py b/apps/tickets/api/request_asset_perm.py deleted file mode 100644 index 8bac63481..000000000 --- a/apps/tickets/api/request_asset_perm.py +++ /dev/null @@ -1,157 +0,0 @@ -import textwrap - -from django.db.models import Q -from django.utils.translation import ugettext_lazy as _ -from rest_framework.mixins import ListModelMixin -from rest_framework.decorators import action -from rest_framework.response import Response - -from orgs.models import Organization, ROLE as ORG_ROLE -from users.models.user import User -from common.const.http import POST -from common.drf.api import JMSModelViewSet, JmsGenericViewSet -from common.permissions import IsValidUser, IsObjectOwner -from common.utils.timezone import dt_parser -from common.drf.serializers import EmptySerializer -from perms.models.asset_permission import AssetPermission, Asset -from perms.models import Action -from assets.models.user import SystemUser -from ..exceptions import ( - ConfirmedAssetsChanged, ConfirmedSystemUserChanged, - TicketClosed, TicketActionAlready, NotHaveConfirmedAssets, - NotHaveConfirmedSystemUser -) -from .. import serializers -from ..models import Ticket -from ..permissions import IsAssignee - - -class RequestAssetPermTicketViewSet(JMSModelViewSet): - queryset = Ticket.origin_objects.filter(type=Ticket.TYPE.REQUEST_ASSET_PERM) - serializer_classes = { - 'default': serializers.RequestAssetPermTicketSerializer, - 'approve': EmptySerializer, - 'reject': EmptySerializer, - 'close': EmptySerializer, - 'assignees': serializers.AssigneeSerializer, - } - permission_classes = (IsValidUser,) - filter_fields = ['status', 'title', 'action', 'user_display', 'org_id'] - search_fields = ['user_display', 'title'] - - def _check_can_set_action(self, instance, action): - if instance.status == instance.STATUS.CLOSED: - raise TicketClosed - if instance.action == action: - action_display = instance.ACTION.get(action) - raise TicketActionAlready(detail=_('Ticket has %s') % action_display) - - def _get_extra_comment(self, instance): - meta = instance.meta - ips = ', '.join(meta.get('ips', [])) - confirmed_assets_id = meta.get('confirmed_assets', []) - confirmed_system_users_id = meta.get('confirmed_system_users', []) - confirmed_assets = Asset.objects.filter(id__in=confirmed_assets_id) - confirmed_system_users = SystemUser.objects.filter(id__in=confirmed_system_users_id) - confirmed_assets_display = ', '.join([str(i) for i in confirmed_assets]) - confirmed_system_users_display = ', '.join([str(i) for i in confirmed_system_users]) - - return textwrap.dedent(''' - {}: {} - {}: {} - {}: {} - {}: {} - {}: {} - '''.format( - _('IP group'), ips, - _('Hostname'), meta.get('hostname', ''), - _('System user'), meta.get('system_user', ''), - _('Confirmed assets'), confirmed_assets_display, - _('Confirmed system users'), confirmed_system_users_display - )) - - @action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser]) - def reject(self, request, *args, **kwargs): - instance = self.get_object() - action = instance.ACTION.REJECT - self._check_can_set_action(instance, action) - instance.perform_action(action, request.user, self._get_extra_comment(instance)) - return Response() - - @action(detail=True, methods=[POST], permission_classes=[IsAssignee, IsValidUser]) - def approve(self, request, *args, **kwargs): - instance = self.get_object() - action = instance.ACTION.APPROVE - self._check_can_set_action(instance, action) - - meta = instance.meta - confirmed_assets = meta.get('confirmed_assets', []) - assets = list(Asset.objects.filter(id__in=confirmed_assets)) - if not assets: - raise NotHaveConfirmedAssets(detail=_('Confirm assets first')) - - if len(assets) != len(confirmed_assets): - raise ConfirmedAssetsChanged(detail=_('Confirmed assets changed')) - - confirmed_system_users = meta.get('confirmed_system_users', []) - if not confirmed_system_users: - raise NotHaveConfirmedSystemUser(detail=_('Confirm system-users first')) - - system_users = SystemUser.objects.filter(id__in=confirmed_system_users) - if system_users is None: - raise ConfirmedSystemUserChanged(detail=_('Confirmed system-users changed')) - - instance.perform_action(instance.ACTION.APPROVE, - request.user, - self._get_extra_comment(instance)) - self._create_asset_permission(instance, assets, system_users) - return Response({'detail': _('Succeed')}) - - @action(detail=True, methods=[POST], permission_classes=[IsAssignee | IsObjectOwner]) - def close(self, request, *args, **kwargs): - instance = self.get_object() - instance.status = Ticket.STATUS.CLOSED - instance.save() - return Response({'detail': _('Succeed')}) - - def _create_asset_permission(self, instance: Ticket, assets, system_users): - meta = instance.meta - actions = meta.get('actions', Action.CONNECT) - - ap_kwargs = { - 'name': _('From request ticket: {} {}').format(instance.user_display, instance.id), - 'created_by': self.request.user.username, - 'comment': _('{} request assets, approved by {}').format(instance.user_display, - instance.assignee_display), - 'actions': actions, - } - date_start = dt_parser(meta.get('date_start')) - date_expired = dt_parser(meta.get('date_expired')) - if date_start: - ap_kwargs['date_start'] = date_start - if date_expired: - ap_kwargs['date_expired'] = date_expired - - ap = AssetPermission.objects.create(**ap_kwargs) - ap.system_users.add(*system_users) - ap.assets.add(*assets) - ap.users.add(instance.user) - - return ap - - -class AssigneeViewSet(ListModelMixin, JmsGenericViewSet): - serializer_class = serializers.AssigneeSerializer - permission_classes = (IsValidUser,) - filter_fields = ('username', 'email', 'name', 'id', 'source') - search_fields = filter_fields - - def get_queryset(self): - user = self.request.user - org_id = self.request.query_params.get('org_id', Organization.DEFAULT_ID) - - q = Q(role=User.ROLE.ADMIN) - if org_id != Organization.DEFAULT_ID: - q |= Q(m2m_org_members__role=ORG_ROLE.ADMIN, orgs__id=org_id, orgs__members=user) - org_admins = User.objects.filter(q).distinct() - return org_admins diff --git a/apps/tickets/api/ticket.py b/apps/tickets/api/ticket.py index 5a49c746d..79d49b37d 100644 --- a/apps/tickets/api/ticket.py +++ b/apps/tickets/api/ticket.py @@ -2,43 +2,75 @@ # from rest_framework import viewsets -from django.shortcuts import get_object_or_404 +from rest_framework.decorators import action +from rest_framework.exceptions import MethodNotAllowed +from rest_framework.response import Response -from common.permissions import IsValidUser -from common.utils import lazyproperty -from .. import serializers, models, mixins +from common.const.http import POST, PUT +from common.mixins.api import CommonApiMixin +from common.permissions import IsValidUser, IsOrgAdmin + +from tickets import serializers +from tickets.models import Ticket +from tickets.permissions.ticket import IsAssignee, NotClosed -class TicketViewSet(mixins.TicketMixin, viewsets.ModelViewSet): - serializer_class = serializers.TicketSerializer - queryset = models.Ticket.origin_objects.all() +__all__ = ['TicketViewSet'] + + +class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet): permission_classes = (IsValidUser,) - filter_fields = ['status', 'title', 'action', 'user_display'] - search_fields = ['user_display', 'title'] + serializer_class = serializers.TicketDisplaySerializer + serializer_classes = { + 'open': serializers.TicketApplySerializer, + 'approve': serializers.TicketApproveSerializer, + } + filterset_fields = [ + 'id', 'title', 'type', 'action', 'status', 'applicant', 'applicant_display', 'processor', + 'processor_display', 'assignees__id' + ] + search_fields = [ + 'title', 'action', 'type', 'status', 'applicant_display', 'processor_display' + ] + def create(self, request, *args, **kwargs): + raise MethodNotAllowed(self.action) -class TicketCommentViewSet(viewsets.ModelViewSet): - serializer_class = serializers.CommentSerializer - http_method_names = ['get', 'post'] + def update(self, request, *args, **kwargs): + raise MethodNotAllowed(self.action) - def check_permissions(self, request): - ticket = self.ticket - if request.user == ticket.user or \ - request.user in ticket.assignees.all(): - return True - return False - - def get_serializer_context(self): - context = super().get_serializer_context() - context['ticket'] = self.ticket - return context - - @lazyproperty - def ticket(self): - ticket_id = self.kwargs.get('ticket_id') - ticket = get_object_or_404(models.Ticket, pk=ticket_id) - return ticket + def destroy(self, request, *args, **kwargs): + raise MethodNotAllowed(self.action) def get_queryset(self): - queryset = self.ticket.comments.all() + queryset = Ticket.get_user_related_tickets(self.request.user) return queryset + + def perform_create(self, serializer): + instance = serializer.save() + instance.open(applicant=self.request.user) + + @action(detail=False, methods=[POST]) + def open(self, request, *args, **kwargs): + return super().create(request, *args, **kwargs) + + @action(detail=True, methods=[PUT], permission_classes=[IsOrgAdmin, IsAssignee, NotClosed]) + def approve(self, request, *args, **kwargs): + response = super().update(request, *args, **kwargs) + instance = self.get_object() + instance.approve(processor=self.request.user) + return response + + @action(detail=True, methods=[PUT], permission_classes=[IsOrgAdmin, IsAssignee, NotClosed]) + def reject(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance) + instance.reject(processor=request.user) + return Response(serializer.data) + + @action(detail=True, methods=[PUT], permission_classes=[IsOrgAdmin, IsAssignee, NotClosed]) + def close(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance) + instance.close(processor=request.user) + return Response(serializer.data) diff --git a/apps/tickets/const.py b/apps/tickets/const.py new file mode 100644 index 000000000..742d4e7d3 --- /dev/null +++ b/apps/tickets/const.py @@ -0,0 +1,23 @@ +from django.db.models import TextChoices +from django.utils.translation import ugettext_lazy as _ + +TICKET_DETAIL_URL = '/ui/#/tickets/tickets/{id}' + + +class TicketTypeChoices(TextChoices): + general = 'general', _("General") + login_confirm = 'login_confirm', _("Login confirm") + apply_asset = 'apply_asset', _('Apply for asset') + apply_application = 'apply_application', _('Apply for application') + + +class TicketActionChoices(TextChoices): + open = 'open', _('Open') + approve = 'approve', _('Approve') + reject = 'reject', _('Reject') + close = 'close', _('Close') + + +class TicketStatusChoices(TextChoices): + open = 'open', _("Open") + closed = 'closed', _("Closed") diff --git a/apps/tickets/exceptions.py b/apps/tickets/exceptions.py deleted file mode 100644 index 5e5dedd21..000000000 --- a/apps/tickets/exceptions.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -from common.exceptions import JMSException - - -class NotHaveConfirmedAssets(JMSException): - pass - - -class ConfirmedAssetsChanged(JMSException): - pass - - -class NotHaveConfirmedSystemUser(JMSException): - pass - - -class ConfirmedSystemUserChanged(JMSException): - pass - - -class TicketClosed(JMSException): - default_detail = _('Ticket closed') - default_code = 'ticket_closed' - - -class TicketActionAlready(JMSException): - pass - - -class OnlyTicketAssigneeCanOperate(JMSException): - default_detail = _('Only assignee can operate ticket') - default_code = 'can_not_operate' - - -class TicketCanNotOperate(JMSException): - default_detail = _('Ticket can not be operated') - default_code = 'ticket_can_not_be_operated' diff --git a/apps/tickets/handler/__init__.py b/apps/tickets/handler/__init__.py new file mode 100644 index 000000000..42ef0d871 --- /dev/null +++ b/apps/tickets/handler/__init__.py @@ -0,0 +1,7 @@ +from django.utils.module_loading import import_string + + +def get_ticket_handler(ticket): + handler_class_path = 'tickets.handler.{}.Handler'.format(ticket.type) + handler_class = import_string(handler_class_path) + return handler_class(ticket=ticket) diff --git a/apps/tickets/handler/apply_application.py b/apps/tickets/handler/apply_application.py new file mode 100644 index 000000000..28eb82697 --- /dev/null +++ b/apps/tickets/handler/apply_application.py @@ -0,0 +1,125 @@ +from django.utils.translation import ugettext as __ +from orgs.utils import tmp_to_org, tmp_to_root_org +from applications.models import Application +from applications.const import ApplicationCategoryChoices, ApplicationTypeChoices +from assets.models import SystemUser +from perms.models import ApplicationPermission +from .base import BaseHandler + + +class Handler(BaseHandler): + + def _on_approve(self): + super()._on_approve() + self._create_application_permission() + + # display + def _construct_meta_display_of_open(self): + meta_display_fields = ['apply_category_display', 'apply_type_display'] + apply_category = self.ticket.meta.get('apply_category') + apply_category_display = ApplicationCategoryChoices.get_label(apply_category) + apply_type = self.ticket.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)) + return meta_display + + def _construct_meta_display_of_approve(self): + meta_display_fields = ['approve_applications_display', 'approve_system_users_display'] + approve_applications_id = self.ticket.meta.get('approve_applications', []) + approve_system_users_id = self.ticket.meta.get('approve_system_users', []) + with tmp_to_org(self.ticket.org_id): + approve_applications = Application.objects.filter(id__in=approve_applications_id) + system_users = SystemUser.objects.filter(id__in=approve_system_users_id) + approve_applications_display = [str(application) for application in approve_applications] + approve_system_users_display = [str(system_user) for system_user in system_users] + meta_display_values = [approve_applications_display, approve_system_users_display] + meta_display = dict(zip(meta_display_fields, meta_display_values)) + return meta_display + + # body + def _construct_meta_body_of_open(self): + apply_category_display = self.ticket.meta.get('apply_category_display') + apply_type_display = self.ticket.meta.get('apply_type_display') + apply_application_group = self.ticket.meta.get('apply_application_group', []) + apply_system_user_group = self.ticket.meta.get('apply_system_user_group', []) + apply_date_start = self.ticket.meta.get('apply_date_start') + apply_date_expired = self.ticket.meta.get('apply_date_expired') + applied_body = '''{}: {}, + {}: {}, + {}: {}, + {}: {}, + {}: {}, + {}: {}, + '''.format( + __('Applied category'), apply_category_display, + __('Applied type'), apply_type_display, + __('Applied application group'), apply_application_group, + __('Applied system user group'), apply_system_user_group, + __('Applied date start'), apply_date_start, + __('Applied date expired'), apply_date_expired, + ) + return applied_body + + def _construct_meta_body_of_approve(self): + # 审批信息 + approve_applications_display = self.ticket.meta.get('approve_applications_display', []) + approve_system_users_display = self.ticket.meta.get('approve_system_users_display', []) + approve_date_start = self.ticket.meta.get('approve_date_start') + approve_date_expired = self.ticket.meta.get('approve_date_expired') + approved_body = '''{}: {}, + {}: {}, + {}: {}, + {}: {}, + '''.format( + __('Approved applications'), approve_applications_display, + __('Approved system users'), approve_system_users_display, + __('Approved date start'), approve_date_start, + __('Approved date expired'), approve_date_expired + ) + return approved_body + + # permission + def _create_application_permission(self): + with tmp_to_root_org(): + application_permission = ApplicationPermission.objects.filter(id=self.ticket.id).first() + if application_permission: + return application_permission + + apply_category = self.ticket.meta.get('apply_category') + apply_type = self.ticket.meta.get('apply_type') + approved_applications_id = self.ticket.meta.get('approve_applications', []) + approve_system_users_id = self.ticket.meta.get('approve_system_users', []) + approve_date_start = self.ticket.meta.get('approve_date_start') + approve_date_expired = self.ticket.meta.get('approve_date_expired') + permission_name = '{}({})'.format( + __('Created by ticket ({})'.format(self.ticket.title)), str(self.ticket.id)[:4] + ) + permission_comment = __( + 'Created by the ticket, ' + 'ticket title: {}, ' + 'ticket applicant: {}, ' + 'ticket processor: {}, ' + 'ticket ID: {}' + ''.format( + self.ticket.title, self.ticket.applicant_display, + self.ticket.processor_display, str(self.ticket.id) + ) + ) + permissions_data = { + 'id': self.ticket.id, + 'name': permission_name, + 'category': apply_category, + 'type': apply_type, + 'comment': permission_comment, + 'created_by': '{}:{}'.format(str(self.__class__.__name__), str(self.ticket.id)), + 'date_start': approve_date_start, + 'date_expired': approve_date_expired, + } + with tmp_to_org(self.ticket.org_id): + application_permission = ApplicationPermission.objects.create(**permissions_data) + application_permission.users.add(self.ticket.applicant) + application_permission.applications.set(approved_applications_id) + application_permission.system_users.set(approve_system_users_id) + + return application_permission diff --git a/apps/tickets/handler/apply_asset.py b/apps/tickets/handler/apply_asset.py new file mode 100644 index 000000000..4114c04b3 --- /dev/null +++ b/apps/tickets/handler/apply_asset.py @@ -0,0 +1,127 @@ +from .base import BaseHandler +from django.utils.translation import ugettext as __ + +from perms.models import AssetPermission, Action +from assets.models import Asset, SystemUser +from orgs.utils import tmp_to_org, tmp_to_root_org + + +class Handler(BaseHandler): + + def _on_approve(self): + super()._on_approve() + self._create_asset_permission() + + # display + def _construct_meta_display_of_open(self): + meta_display_fields = ['apply_actions_display'] + apply_actions = self.ticket.meta.get('apply_actions', Action.NONE) + apply_actions_display = Action.value_to_choices_display(apply_actions) + meta_display_values = [apply_actions_display] + meta_display = dict(zip(meta_display_fields, meta_display_values)) + return meta_display + + def _construct_meta_display_of_approve(self): + meta_display_fields = [ + 'approve_actions_display', 'approve_assets_display', 'approve_system_users_display' + ] + approve_actions = self.ticket.meta.get('approve_actions', Action.NONE) + approve_actions_display = Action.value_to_choices_display(approve_actions) + approve_assets_id = self.ticket.meta.get('approve_assets', []) + approve_system_users_id = self.ticket.meta.get('approve_system_users', []) + with tmp_to_org(self.ticket.org_id): + assets = Asset.objects.filter(id__in=approve_assets_id) + system_users = SystemUser.objects.filter(id__in=approve_system_users_id) + approve_assets_display = [str(asset) for asset in assets] + approve_system_users_display = [str(system_user) for system_user in system_users] + meta_display_values = [ + approve_actions_display, approve_assets_display, approve_system_users_display + ] + meta_display = dict(zip(meta_display_fields, meta_display_values)) + return meta_display + + # body + def _construct_meta_body_of_open(self): + apply_ip_group = self.ticket.meta.get('apply_ip_group', []) + apply_hostname_group = self.ticket.meta.get('apply_hostname_group', []) + apply_system_user_group = self.ticket.meta.get('apply_system_user_group', []) + apply_actions_display = self.ticket.meta.get('apply_actions_display', []) + apply_date_start = self.ticket.meta.get('apply_date_start') + apply_date_expired = self.ticket.meta.get('apply_date_expired') + applied_body = '''{}: {}, + {}: {}, + {}: {}, + {}: {}, + {}: {} + '''.format( + __('Applied IP group'), apply_ip_group, + __("Applied hostname group"), apply_hostname_group, + __("Applied system user group"), apply_system_user_group, + __("Applied actions"), apply_actions_display, + __('Applied date start'), apply_date_start, + __('Applied date expired'), apply_date_expired, + ) + return applied_body + + def _construct_meta_body_of_approve(self): + approve_assets_display = self.ticket.meta.get('approve_assets_display', []) + approve_system_users_display = self.ticket.meta.get('approve_system_users_display', []) + approve_actions_display = self.ticket.meta.get('approve_actions_display', []) + approve_date_start = self.ticket.meta.get('approve_date_start') + approve_date_expired = self.ticket.meta.get('approve_date_expired') + approved_body = '''{}: {}, + {}: {}, + {}: {}, + {}: {}, + {}: {} + '''.format( + __('Approved assets'), approve_assets_display, + __('Approved system users'), approve_system_users_display, + __('Approved actions'), ', '.join(approve_actions_display), + __('Approved date start'), approve_date_start, + __('Approved date expired'), approve_date_expired, + ) + return approved_body + + # permission + def _create_asset_permission(self): + with tmp_to_root_org(): + asset_permission = AssetPermission.objects.filter(id=self.ticket.id).first() + if asset_permission: + return asset_permission + + approve_assets_id = self.ticket.meta.get('approve_assets', []) + approve_system_users_id = self.ticket.meta.get('approve_system_users', []) + approve_actions = self.ticket.meta.get('approve_actions', Action.NONE) + approve_date_start = self.ticket.meta.get('approve_date_start') + approve_date_expired = self.ticket.meta.get('approve_date_expired') + permission_name = '{}({})'.format( + __('Created by ticket ({})'.format(self.ticket.title)), str(self.ticket.id)[:4] + ) + permission_comment = __( + 'Created by the ticket, ' + 'ticket title: {}, ' + 'ticket applicant: {}, ' + 'ticket processor: {}, ' + 'ticket ID: {}' + ''.format( + self.ticket.title, self.ticket.applicant_display, self.ticket.processor_display, + str(self.ticket.id) + ) + ) + permission_data = { + 'id': self.ticket.id, + 'name': permission_name, + 'comment': permission_comment, + 'created_by': '{}:{}'.format(str(self.__class__.__name__), str(self.ticket.id)), + 'actions': approve_actions, + 'date_start': approve_date_start, + 'date_expired': approve_date_expired, + } + with tmp_to_org(self.ticket.org_id): + asset_permission = AssetPermission.objects.create(**permission_data) + asset_permission.users.add(self.ticket.applicant) + asset_permission.assets.set(approve_assets_id) + asset_permission.system_users.set(approve_system_users_id) + + return asset_permission diff --git a/apps/tickets/handler/base.py b/apps/tickets/handler/base.py new file mode 100644 index 000000000..e1820dc0b --- /dev/null +++ b/apps/tickets/handler/base.py @@ -0,0 +1,122 @@ +from django.utils.translation import ugettext as __ +from common.utils import get_logger +from tickets.utils import send_ticket_processed_mail_to_applicant + + +logger = get_logger(__name__) + + +class BaseHandler(object): + + def __init__(self, ticket): + self.ticket = ticket + + # on action + def _on_open(self): + self.ticket.applicant_display = str(self.ticket.applicant) + meta_display = getattr(self, '_construct_meta_display_of_open', lambda: {})() + self.ticket.meta.update(meta_display) + self.ticket.save() + + def _on_approve(self): + meta_display = getattr(self, '_construct_meta_display_of_approve', lambda: {})() + self.ticket.meta.update(meta_display) + self.__on_process() + + def _on_reject(self): + self.__on_process() + + def _on_close(self): + self.__on_process() + + def __on_process(self): + self.ticket.processor_display = str(self.ticket.processor) + self.ticket.set_status_closed() + self._send_processed_mail_to_applicant() + self.ticket.save() + + def dispatch(self, action): + self._create_comment_on_action() + method = getattr(self, f'_on_{action}', lambda: None) + return method() + + # email + def _send_processed_mail_to_applicant(self): + msg = 'Ticket ({}) has processed, send mail to applicant ({})'.format( + self.ticket.title, self.ticket.applicant_display + ) + logger.debug(msg) + send_ticket_processed_mail_to_applicant(self.ticket) + + # comments + def _create_comment_on_action(self): + user = self.ticket.applicant if self.ticket.action_open else self.ticket.processor + user_display = str(user) + action_display = self.ticket.get_action_display() + data = { + 'body': __('User {} {} the ticket'.format(user_display, action_display)), + 'user': user, + 'user_display': user_display + } + return self.ticket.comments.create(**data) + + # body + def get_body(self): + old_body = self.ticket.meta.get('body') + if old_body: + # 之前版本的body + return old_body + basic_body = self._construct_basic_body() + meta_body = self._construct_meta_body() + return basic_body + meta_body + + def _construct_basic_body(self): + body = ''' + {}: + {}: {}, + {}: {}, + {}: {}, + {}: {}, + {}: {}, + {}: {}, + {}: {} + '''.format( + __("Ticket basic info"), + __('Ticket title'), self.ticket.title, + __('Ticket type'), self.ticket.get_type_display(), + __('Ticket applicant'), self.ticket.applicant_display, + __('Ticket assignees'), self.ticket.assignees_display, + __('Ticket processor'), self.ticket.processor_display, + __('Ticket action'), self.ticket.get_action_display(), + __('Ticket status'), self.ticket.get_status_display() + ) + return body + + def _construct_meta_body(self): + body = '' + open_body = self._base_construct_meta_body_of_open() + body += open_body + if self.ticket.action_approve: + approve_body = self._base_construct_meta_body_of_approve() + body += approve_body + return body + + def _base_construct_meta_body_of_open(self): + open_body = ''' + {}: + {} + '''.format( + __('Ticket applied info'), + getattr(self, '_construct_meta_body_of_open', lambda: 'No')() + ) + return open_body + + def _base_construct_meta_body_of_approve(self): + approve_body = ''' + {}: + {} + '''.format( + __('Ticket approved info'), + getattr(self, '_construct_meta_body_of_approve', lambda: 'No')() + ) + return approve_body diff --git a/apps/tickets/handler/general.py b/apps/tickets/handler/general.py new file mode 100644 index 000000000..e4df867a3 --- /dev/null +++ b/apps/tickets/handler/general.py @@ -0,0 +1,5 @@ +from .base import BaseHandler + + +class Handler(BaseHandler): + pass diff --git a/apps/tickets/handler/login_confirm.py b/apps/tickets/handler/login_confirm.py new file mode 100644 index 000000000..21419022f --- /dev/null +++ b/apps/tickets/handler/login_confirm.py @@ -0,0 +1,20 @@ +from django.utils.translation import ugettext as __ +from .base import BaseHandler + + +class Handler(BaseHandler): + + # body + def _construct_meta_body_of_open(self): + apply_login_ip = self.ticket.meta.get('apply_login_ip') + apply_login_city = self.ticket.meta.get('apply_login_city') + apply_login_datetime = self.ticket.meta.get('apply_login_datetime') + applied_body = '''{}: {}, + {}: {}, + {}: {} + '''.format( + __("Applied login IP"), apply_login_ip, + __("Applied login city"), apply_login_city, + __("Applied login datetime"), apply_login_datetime, + ) + return applied_body diff --git a/apps/tickets/migrations/0007_auto_20201224_1821.py b/apps/tickets/migrations/0007_auto_20201224_1821.py new file mode 100644 index 000000000..a16771ef9 --- /dev/null +++ b/apps/tickets/migrations/0007_auto_20201224_1821.py @@ -0,0 +1,187 @@ +# Generated by Django 3.1 on 2020-12-24 10:21 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import tickets.models.ticket + +TICKET_TYPE_APPLY_ASSET = 'apply_asset' + + +def migrate_field_type(tp): + if tp == 'request_asset': + return TICKET_TYPE_APPLY_ASSET + return tp + + +def migrate_field_meta(tp, old_meta): + if tp != TICKET_TYPE_APPLY_ASSET or not old_meta: + return old_meta + old_meta_hostname = old_meta.get('hostname') + old_meta_system_user = old_meta.get('system_user') + new_meta = { + 'apply_ip_group': old_meta.get('ips', []), + 'apply_hostname_group': [old_meta_hostname] if old_meta_hostname else [], + 'apply_system_user_group': [old_meta_system_user] if old_meta_system_user else [], + 'apply_actions': old_meta.get('actions'), + 'apply_actions_display': [], + 'apply_date_start': old_meta.get('date_start'), + 'apply_date_expired': old_meta.get('date_expired'), + + 'approve_assets': old_meta.get('confirmed_assets', []), + 'approve_assets_display': [], + 'approve_system_users': old_meta.get('confirmed_system_users', []), + 'approve_system_users_display': [], + 'approve_actions': old_meta.get('actions'), + 'approve_actions_display': [], + 'approve_date_start': old_meta.get('date_start'), + 'approve_date_expired': old_meta.get('date_expired'), + } + return new_meta + + +ACTION_OPEN = 'open' +ACTION_CLOSE = 'close' +STATUS_OPEN = 'open' +STATUS_CLOSED = 'closed' + + +def migrate_field_action(old_action, old_status): + if old_action: + return old_action + if old_status == STATUS_OPEN: + return ACTION_OPEN + if old_status == STATUS_CLOSED: + return ACTION_CLOSE + + +def migrate_field_assignees_display(assignees_display): + if not assignees_display: + return [] + assignees_display = assignees_display.split(', ') + return assignees_display + + +def migrate_tickets_fields_name(apps, schema_editor): + ticket_model = apps.get_model("tickets", "Ticket") + tickets = ticket_model.origin_objects.all() + + for ticket in tickets: + ticket.applicant = ticket.user + ticket.applicant_display = ticket.user_display + ticket.processor = ticket.assignee + ticket.processor_display = ticket.assignee_display + ticket.assignees_display_new = migrate_field_assignees_display(ticket.assignees_display) + ticket.action = migrate_field_action(ticket.action, ticket.status) + ticket.type = migrate_field_type(ticket.type) + ticket.meta = migrate_field_meta(ticket.type, ticket.meta) + ticket.meta['body'] = ticket.body + + fields = [ + 'applicant', 'applicant_display', 'processor', 'processor_display', + 'assignees_display_new', 'action', 'type', 'meta' + ] + ticket_model.origin_objects.bulk_update(tickets, fields) + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('tickets', '0006_auto_20201023_1628'), + ] + + operations = [ + # model ticket + migrations.AddField( + model_name='ticket', + name='applicant', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='applied_tickets', to=settings.AUTH_USER_MODEL, verbose_name='Applicant'), + ), + migrations.AddField( + model_name='ticket', + name='applicant_display', + field=models.CharField(default='', max_length=256, verbose_name='Applicant display'), + ), + migrations.AddField( + model_name='ticket', + name='processor', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='processed_tickets', to=settings.AUTH_USER_MODEL, verbose_name='Processor'), + ), + migrations.AddField( + model_name='ticket', + name='processor_display', + field=models.CharField(blank=True, default='', max_length=256, null=True, verbose_name='Processor display'), + ), + migrations.AddField( + model_name='ticket', + name='assignees_display_new', + field=models.JSONField(default=list, encoder=tickets.models.ticket.ModelJSONFieldEncoder, verbose_name='Assignees display'), + ), + migrations.AlterField( + model_name='ticket', + name='assignees', + field=models.ManyToManyField(related_name='assigned_tickets', to=settings.AUTH_USER_MODEL, verbose_name='Assignees'), + ), + migrations.AlterField( + model_name='ticket', + name='meta', + field=models.JSONField(default=dict, encoder=tickets.models.ticket.ModelJSONFieldEncoder, verbose_name='Meta'), + ), + migrations.AlterField( + model_name='ticket', + name='type', + field=models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm'), ('apply_asset', 'Apply for asset'), ('apply_application', 'Apply for application')], default='general', max_length=64, verbose_name='Type'), + ), + migrations.AlterField( + model_name='ticket', + name='action', + field=models.CharField(choices=[('open', 'Open'), ('approve', 'Approve'), ('reject', 'Reject'), ('close', 'Close')], default='open', max_length=16, verbose_name='Action'), + ), + migrations.AlterField( + model_name='ticket', + name='status', + field=models.CharField(choices=[('open', 'Open'), ('closed', 'Closed')], default='open', max_length=16, verbose_name='Status'), + ), + migrations.RunPython(migrate_tickets_fields_name), + migrations.RemoveField( + model_name='ticket', + name='user', + ), + migrations.RemoveField( + model_name='ticket', + name='user_display', + ), + migrations.RemoveField( + model_name='ticket', + name='assignee', + ), + migrations.RemoveField( + model_name='ticket', + name='assignee_display', + ), + migrations.RemoveField( + model_name='ticket', + name='body', + ), + migrations.RemoveField( + model_name='ticket', + name='assignees_display', + ), + migrations.RenameField( + model_name='ticket', + old_name='assignees_display_new', + new_name='assignees_display', + ), + migrations.AlterModelManagers( + name='ticket', + managers=[ + ], + ), + # model comment + migrations.AlterField( + model_name='comment', + name='user_display', + field=models.CharField(max_length=256, verbose_name='User display name'), + ), + ] diff --git a/apps/tickets/mixins.py b/apps/tickets/mixins.py deleted file mode 100644 index c4a48d866..000000000 --- a/apps/tickets/mixins.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -# -from django.db.models import Q -from .models import Ticket - - -class TicketMixin: - def get_queryset(self): - queryset = super().get_queryset() - assign = self.request.GET.get('assign', None) - if assign is None: - queryset = Ticket.get_related_tickets(self.request.user, queryset) - elif assign in ['1']: - queryset = Ticket.get_assigned_tickets(self.request.user, queryset) - else: - queryset = Ticket.get_my_tickets(self.request.user, queryset) - return queryset diff --git a/apps/tickets/models/__init__.py b/apps/tickets/models/__init__.py index 0b5dd0e7d..4f7ca772b 100644 --- a/apps/tickets/models/__init__.py +++ b/apps/tickets/models/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- # from .ticket import * +from .comment import * diff --git a/apps/tickets/models/comment.py b/apps/tickets/models/comment.py new file mode 100644 index 000000000..11ebcd546 --- /dev/null +++ b/apps/tickets/models/comment.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from common.mixins.models import CommonModelMixin + +__all__ = ['Comment'] + + +class Comment(CommonModelMixin): + ticket = models.ForeignKey( + 'tickets.Ticket', on_delete=models.CASCADE, related_name='comments' + ) + user = models.ForeignKey( + 'users.User', on_delete=models.SET_NULL, null=True, related_name='comments', + verbose_name=_("User") + ) + user_display = models.CharField(max_length=256, verbose_name=_("User display name")) + body = models.TextField(verbose_name=_("Body")) + + class Meta: + ordering = ('date_created', ) + + def set_display_fields(self): + self.user_display = str(self.user) diff --git a/apps/tickets/models/ticket.py b/apps/tickets/models/ticket.py index 8c5f50aea..97d99e4b3 100644 --- a/apps/tickets/models/ticket.py +++ b/apps/tickets/models/ticket.py @@ -1,141 +1,199 @@ # -*- coding: utf-8 -*- # - +import json +import uuid +from datetime import datetime from django.db import models from django.db.models import Q from django.utils.translation import ugettext_lazy as _ +from django.conf import settings -from common.db.models import ChoiceSet from common.mixins.models import CommonModelMixin -from common.fields.model import JsonDictTextField from orgs.mixins.models import OrgModelMixin +from orgs.utils import tmp_to_root_org, tmp_to_org +from tickets.const import TicketTypeChoices, TicketActionChoices, TicketStatusChoices +from tickets.signals import post_change_ticket_action +from tickets.handler import get_ticket_handler -__all__ = ['Ticket', 'Comment'] +__all__ = ['Ticket', 'ModelJSONFieldEncoder'] -class Ticket(OrgModelMixin, CommonModelMixin): - class STATUS(ChoiceSet): - OPEN = 'open', _("Open") - CLOSED = 'closed', _("Closed") - - class TYPE(ChoiceSet): - GENERAL = 'general', _("General") - LOGIN_CONFIRM = 'login_confirm', _("Login confirm") - REQUEST_ASSET_PERM = 'request_asset', _('Request asset permission') - - class ACTION(ChoiceSet): - APPROVE = 'approve', _('Approve') - REJECT = 'reject', _('Reject') - - user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_requested', verbose_name=_("User")) - user_display = models.CharField(max_length=128, verbose_name=_("User display name")) - - title = models.CharField(max_length=256, verbose_name=_("Title")) - body = models.TextField(verbose_name=_("Body")) - meta = JsonDictTextField(verbose_name=_("Meta"), default='{}') - assignee = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_handled', verbose_name=_("Assignee")) - assignee_display = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Assignee display name"), default='') - assignees = models.ManyToManyField('users.User', related_name='%(class)s_assigned', verbose_name=_("Assignees")) - assignees_display = models.CharField(max_length=128, verbose_name=_("Assignees display name"), blank=True) - type = models.CharField(max_length=16, choices=TYPE.choices, default=TYPE.GENERAL, verbose_name=_("Type")) - status = models.CharField(choices=STATUS.choices, max_length=16, default='open') - action = models.CharField(choices=ACTION.choices, max_length=16, default='', blank=True) - comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) - - origin_objects = models.Manager() - - def __str__(self): - return '{}: {}'.format(self.user_display, self.title) - - @property - def body_as_html(self): - return self.body.replace('\n', '
') - - @property - def status_display(self): - return self.get_status_display() - - @property - def type_display(self): - return self.get_type_display() - - @property - def action_display(self): - return self.get_action_display() - - def create_status_comment(self, status, user): - if status == self.STATUS.CLOSED: - action = _("Close") +class ModelJSONFieldEncoder(json.JSONEncoder): + """ 解决一些类型的字段不能序列化的问题 """ + def default(self, obj): + if isinstance(obj, datetime): + return obj.strftime(settings.DATETIME_DISPLAY_FORMAT) + if isinstance(obj, uuid.UUID): + return str(obj) + if isinstance(obj, type(_("ugettext_lazy"))): + return str(obj) else: - action = _("Open") - body = _('{} {} this ticket').format(self.user, action) - self.comments.create(user=user, body=body) + return super().default(obj) - def perform_status(self, status, user, extra_comment=None): - self.create_comment( - self.STATUS.get(status), - user, - extra_comment - ) - self.status = status - self.assignee = user - self.save() - def create_comment(self, action_display, user, extra_comment=None): - body = '{} {} {}'.format(user, action_display, _("this ticket")) - if extra_comment is not None: - body += extra_comment - self.comments.create(body=body, user=user, user_display=str(user)) - - def perform_action(self, action, user, extra_comment=None): - self.create_comment( - self.ACTION.get(action), - user, - extra_comment - ) - self.action = action - self.status = self.STATUS.CLOSED - self.assignee = user - self.save() - - def is_assignee(self, user): - return self.assignees.filter(id=user.id).exists() - - def is_user(self, user): - return self.user == user - - @classmethod - def get_related_tickets(cls, user, queryset=None): - if queryset is None: - queryset = cls.objects.all() - queryset = queryset.filter( - Q(assignees=user) | Q(user=user) - ).distinct() - return queryset - - @classmethod - def get_assigned_tickets(cls, user, queryset=None): - if queryset is None: - queryset = cls.objects.all() - queryset = queryset.filter(assignees=user) - return queryset - - @classmethod - def get_my_tickets(cls, user, queryset=None): - if queryset is None: - queryset = cls.objects.all() - queryset = queryset.filter(user=user) - return queryset +class Ticket(CommonModelMixin, OrgModelMixin): + title = models.CharField(max_length=256, verbose_name=_("Title")) + type = models.CharField( + max_length=64, choices=TicketTypeChoices.choices, + default=TicketTypeChoices.general.value, verbose_name=_("Type") + ) + meta = models.JSONField(encoder=ModelJSONFieldEncoder, default=dict, verbose_name=_("Meta")) + action = models.CharField( + choices=TicketActionChoices.choices, max_length=16, + default=TicketActionChoices.open.value, verbose_name=_("Action") + ) + status = models.CharField( + max_length=16, choices=TicketStatusChoices.choices, + default=TicketStatusChoices.open.value, verbose_name=_("Status") + ) + # 申请人 + applicant = models.ForeignKey( + 'users.User', related_name='applied_tickets', on_delete=models.SET_NULL, null=True, + verbose_name=_("Applicant") + ) + applicant_display = models.CharField( + max_length=256, default='', verbose_name=_("Applicant display") + ) + # 处理人 + processor = models.ForeignKey( + 'users.User', related_name='processed_tickets', on_delete=models.SET_NULL, null=True, + verbose_name=_("Processor") + ) + processor_display = models.CharField( + max_length=256, blank=True, null=True, default='', verbose_name=_("Processor display") + ) + # 受理人列表 + assignees = models.ManyToManyField( + 'users.User', related_name='assigned_tickets', verbose_name=_("Assignees") + ) + assignees_display = models.JSONField( + encoder=ModelJSONFieldEncoder, default=list, verbose_name=_('Assignees display') + ) + # 评论 + comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) class Meta: ordering = ('-date_created',) + def __str__(self): + return '{}({})'.format(self.title, self.applicant_display) -class Comment(CommonModelMixin): - ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name='comments') - user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, verbose_name=_("User"), related_name='comments') - user_display = models.CharField(max_length=128, verbose_name=_("User display name")) - body = models.TextField(verbose_name=_("Body")) + # type + @property + def type_apply_asset(self): + return self.type == TicketTypeChoices.apply_asset.value - class Meta: - ordering = ('date_created', ) + @property + def type_apply_application(self): + return self.type == TicketTypeChoices.apply_application.value + + @property + def type_login_confirm(self): + return self.type == TicketTypeChoices.login_confirm.value + + # status + @property + def status_closed(self): + return self.status == TicketStatusChoices.closed.value + + @property + def status_open(self): + return self.status == TicketStatusChoices.open.value + + def set_status_closed(self): + self.status = TicketStatusChoices.closed.value + + # action + @property + def action_open(self): + return self.action == TicketActionChoices.open.value + + @property + def action_approve(self): + return self.action == TicketActionChoices.approve.value + + @property + def action_reject(self): + return self.action == TicketActionChoices.reject.value + + @property + def action_close(self): + return self.action == TicketActionChoices.close.value + + # action changed + def open(self, applicant): + self.applicant = applicant + self._change_action(action=TicketActionChoices.open.value) + + def approve(self, processor): + self.processor = processor + self._change_action(action=TicketActionChoices.approve.value) + + def reject(self, processor): + self.processor = processor + self._change_action(action=TicketActionChoices.reject.value) + + def close(self, processor): + self.processor = processor + self._change_action(action=TicketActionChoices.close.value) + + def _change_action(self, action): + self.action = action + self.save() + post_change_ticket_action.send(sender=self.__class__, ticket=self, action=action) + + # ticket + def has_assignee(self, assignee): + return self.assignees.filter(id=assignee.id).exists() + + @classmethod + def get_user_related_tickets(cls, user): + queries = None + tickets = cls.all() + if user.is_superuser: + pass + elif user.is_super_auditor: + pass + elif user.is_org_admin: + admin_orgs_id = [ + str(org_id) for org_id in user.admin_orgs.values_list('id', flat=True) + ] + assigned_tickets_id = [ + str(ticket_id) for ticket_id in user.assigned_tickets.values_list('id', flat=True) + ] + queries = Q(applicant=user) + queries |= Q(processor=user) + queries |= Q(org_id__in=admin_orgs_id) + queries |= Q(id__in=assigned_tickets_id) + elif user.is_org_auditor: + audit_orgs_id = [ + str(org_id) for org_id in user.audit_orgs.values_list('id', flat=True) + ] + queries = Q(org_id__in=audit_orgs_id) + elif user.is_common_user: + queries = Q(applicant=user) + else: + tickets = cls.objects.none() + if queries: + tickets = tickets.filter(queries) + return tickets.distinct() + + @classmethod + def all(cls): + with tmp_to_root_org(): + return Ticket.objects.all() + + def save(self, *args, **kwargs): + """ 确保保存的org_id的是自身的值 """ + with tmp_to_org(self.org_id): + return super().save(*args, **kwargs) + + @property + def handler(self): + return get_ticket_handler(ticket=self) + + # body + @property + def body(self): + _body = self.handler.get_body() + return _body diff --git a/apps/tickets/permissions.py b/apps/tickets/permissions.py deleted file mode 100644 index 80db4cb94..000000000 --- a/apps/tickets/permissions.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from rest_framework.permissions import BasePermission - - -class IsAssignee(BasePermission): - def has_object_permission(self, request, view, obj): - return obj.is_assignee(request.user) diff --git a/apps/tickets/permissions/__init__.py b/apps/tickets/permissions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/tickets/permissions/comment.py b/apps/tickets/permissions/comment.py new file mode 100644 index 000000000..c478c7529 --- /dev/null +++ b/apps/tickets/permissions/comment.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import permissions + + +class IsSwagger(permissions.BasePermission): + def has_permission(self, request, view): + return getattr(view, 'swagger_fake_view', False) + + +class IsApplicant(permissions.BasePermission): + def has_permission(self, request, view): + return request.user == view.ticket.applicant + + +class IsAssignee(permissions.BasePermission): + def has_permission(self, request, view): + return view.ticket.has_assignee(request.user) diff --git a/apps/tickets/permissions/ticket.py b/apps/tickets/permissions/ticket.py new file mode 100644 index 000000000..c16db9fe6 --- /dev/null +++ b/apps/tickets/permissions/ticket.py @@ -0,0 +1,12 @@ + +from rest_framework import permissions + + +class IsAssignee(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + return obj.has_assignee(request.user) + + +class NotClosed(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + return not obj.status_closed diff --git a/apps/tickets/serializers/__init__.py b/apps/tickets/serializers/__init__.py index 4eb820587..6b519ef80 100644 --- a/apps/tickets/serializers/__init__.py +++ b/apps/tickets/serializers/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- # from .ticket import * -from .request_asset_perm import * +from .assignee import * +from .comment import * diff --git a/apps/tickets/serializers/assignee.py b/apps/tickets/serializers/assignee.py new file mode 100644 index 000000000..217c26fa9 --- /dev/null +++ b/apps/tickets/serializers/assignee.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +__all__ = ['AssigneeSerializer'] + + +class AssigneeSerializer(serializers.Serializer): + id = serializers.UUIDField() + name = serializers.CharField() + username = serializers.CharField() diff --git a/apps/tickets/serializers/comment.py b/apps/tickets/serializers/comment.py new file mode 100644 index 000000000..4eaf49cf7 --- /dev/null +++ b/apps/tickets/serializers/comment.py @@ -0,0 +1,29 @@ +from rest_framework import serializers +from ..models import Comment +from common.drf.fields import ReadableHiddenField + +__all__ = ['CommentSerializer'] + + +class CurrentTicket(object): + ticket = None + + def set_context(self, serializer_field): + self.ticket = serializer_field.context['ticket'] + + def __call__(self): + return self.ticket + + +class CommentSerializer(serializers.ModelSerializer): + ticket = ReadableHiddenField(default=CurrentTicket()) + user = ReadableHiddenField(default=serializers.CurrentUserDefault()) + + class Meta: + model = Comment + fields = [ + 'id', 'ticket', 'body', 'user', 'user_display', 'date_created', 'date_updated' + ] + read_only_fields = [ + 'user_display', 'date_created', 'date_updated' + ] diff --git a/apps/tickets/serializers/request_asset_perm.py b/apps/tickets/serializers/request_asset_perm.py deleted file mode 100644 index 9bc70b79a..000000000 --- a/apps/tickets/serializers/request_asset_perm.py +++ /dev/null @@ -1,241 +0,0 @@ -from rest_framework import serializers -from django.conf import settings -from django.utils.translation import ugettext_lazy as _ -from django.urls import reverse -from django.db.models import Q - -from common.utils.timezone import dt_parser, dt_formater -from orgs.utils import tmp_to_root_org -from orgs.models import Organization, ROLE as ORG_ROLE -from assets.models import Asset, SystemUser -from users.models.user import User -from perms.serializers import ActionsField -from perms.models import Action -from ..models import Ticket - - -class RequestAssetPermTicketSerializer(serializers.ModelSerializer): - actions = ActionsField(source='meta.actions', choices=Action.DB_CHOICES, - default=Action.CONNECT) - ips = serializers.ListField(child=serializers.IPAddressField(), source='meta.ips', - default=list, label=_('IP group')) - hostname = serializers.CharField(max_length=256, source='meta.hostname', default='', - allow_blank=True, label=_('Hostname')) - system_user = serializers.CharField(max_length=256, source='meta.system_user', default='', - allow_blank=True, label=_('System user')) - date_start = serializers.DateTimeField(source='meta.date_start', allow_null=True, - required=False, label=_('Date start')) - date_expired = serializers.DateTimeField(source='meta.date_expired', allow_null=True, - required=False, label=_('Date expired')) - confirmed_assets = serializers.ListField(child=serializers.UUIDField(), - source='meta.confirmed_assets', - default=list, required=False, - label=_('Confirmed assets')) - confirmed_system_users = serializers.ListField(child=serializers.UUIDField(), - source='meta.confirmed_system_users', - default=list, required=False, - label=_('Confirmed system user')) - assets_waitlist_url = serializers.SerializerMethodField() - system_users_waitlist_url = serializers.SerializerMethodField() - - class Meta: - model = Ticket - mini_fields = ['id', 'title'] - small_fields = [ - 'status', 'action', 'date_created', 'date_updated', 'system_users_waitlist_url', - 'type', 'type_display', 'action_display', 'ips', 'confirmed_assets', - 'date_start', 'date_expired', 'confirmed_system_users', 'hostname', - 'assets_waitlist_url', 'system_user', 'org_id', 'actions', 'comment' - ] - m2m_fields = [ - 'user', 'user_display', 'assignees', 'assignees_display', - 'assignee', 'assignee_display' - ] - - fields = mini_fields + small_fields + m2m_fields - read_only_fields = [ - 'user_display', 'assignees_display', 'type', 'user', 'status', - 'date_created', 'date_updated', 'action', 'id', 'assignee', - 'assignee_display', - ] - extra_kwargs = { - 'status': {'label': _('Status')}, - 'action': {'label': _('Action')}, - 'user_display': {'label': _('User')}, - 'org_id': {'required': True} - } - - def validate(self, attrs): - org_id = attrs.get('org_id') - assignees = attrs.get('assignees') - - instance = self.instance - if instance is not None: - if org_id and not assignees: - assignees = list(instance.assignees.all()) - elif assignees and not org_id: - org_id = instance.org_id - elif assignees and org_id: - pass - else: - return attrs - - user = self.context['request'].user - org = Organization.get_instance(org_id) - if org is None: - raise serializers.ValidationError(_('Invalid `org_id`')) - - q = Q(role=User.ROLE.ADMIN) - if not org.is_default(): - q |= Q(m2m_org_members__role=ORG_ROLE.ADMIN, orgs__id=org_id, orgs__members=user) - - q &= Q(id__in=[assignee.id for assignee in assignees]) - count = User.objects.filter(q).distinct().count() - if count != len(assignees): - raise serializers.ValidationError(_('Field `assignees` must be organization admin or superuser')) - return attrs - - def get_system_users_waitlist_url(self, instance: Ticket): - if not self._is_assignee(instance): - return None - return reverse('api-assets:system-user-list') - - def get_assets_waitlist_url(self, instance: Ticket): - if not self._is_assignee(instance): - return None - - asset_api = reverse('api-assets:asset-list') - query = '' - - meta = instance.meta - hostname = meta.get('hostname') - if hostname: - query = '?search=%s' % hostname - - return asset_api + query - - def _recommend_assets(self, data, instance): - confirmed_assets = data.get('confirmed_assets') - if not confirmed_assets and self._is_assignee(instance): - ips = data.get('ips') - hostname = data.get('hostname') - limit = 5 - - q = Q(id=None) - if ips: - limit = len(ips) + 2 - q |= Q(ip__in=ips) - if hostname: - q |= Q(hostname__icontains=hostname) - - recomand_assets_id = Asset.objects.filter(q)[:limit].values_list('id', flat=True) - data['confirmed_assets'] = [str(i) for i in recomand_assets_id] - - def _recommend_system_users(self, data, instance): - confirmed_system_users = data.get('confirmed_system_users') - system_user = data.get('system_user') - - if all((not confirmed_system_users, self._is_assignee(instance), system_user)): - recomand_system_users_id = SystemUser.objects.filter( - name__icontains=system_user - )[:3].values_list('id', flat=True) - data['confirmed_system_users'] = [str(i) for i in recomand_system_users_id] - - def to_representation(self, instance): - data = super().to_representation(instance) - self._recommend_assets(data, instance) - self._recommend_system_users(data, instance) - return data - - def _create_body(self, validated_data): - meta = validated_data['meta'] - type = Ticket.TYPE.get(validated_data.get('type', '')) - date_start = dt_parser(meta.get('date_start')).strftime(settings.DATETIME_DISPLAY_FORMAT) - date_expired = dt_parser(meta.get('date_expired')).strftime(settings.DATETIME_DISPLAY_FORMAT) - - validated_data['body'] = _(''' - Type: {type}
- User: {username}
- Ip group: {ips}
- Hostname: {hostname}
- System user: {system_user}
- Date start: {date_start}
- Date expired: {date_expired}
- ''').format( - type=type, - username=validated_data.get('user', ''), - ips=', '.join(meta.get('ips', [])), - hostname=meta.get('hostname', ''), - system_user=meta.get('system_user', ''), - date_start=date_start, - date_expired=date_expired - ) - - def create(self, validated_data): - # `type` 与 `user` 用户不可提交, - validated_data['type'] = self.Meta.model.TYPE.REQUEST_ASSET_PERM - validated_data['user'] = self.context['request'].user - # `confirmed` 相关字段只能审批人修改,所以创建时直接清理掉 - self._pop_confirmed_fields() - self._create_body(validated_data) - return super().create(validated_data) - - def save(self, **kwargs): - """ - 做了一些数据转换 - """ - meta = self.validated_data.get('meta', {}) - - org_id = self.validated_data.get('org_id') - if org_id is not None and org_id == Organization.DEFAULT_ID: - self.validated_data['org_id'] = '' - - # 时间的转换,好烦😭,可能有更好的办法吧 - date_start = meta.get('date_start') - if date_start: - meta['date_start'] = dt_formater(date_start) - - date_expired = meta.get('date_expired') - if date_expired: - meta['date_expired'] = dt_formater(date_expired) - - # UUID 的转换 - confirmed_system_users = meta.get('confirmed_system_users') - if confirmed_system_users: - meta['confirmed_system_users'] = [str(system_user) for system_user in confirmed_system_users] - - confirmed_assets = meta.get('confirmed_assets') - if confirmed_assets: - meta['confirmed_assets'] = [str(asset) for asset in confirmed_assets] - - with tmp_to_root_org(): - return super().save(**kwargs) - - def update(self, instance, validated_data): - new_meta = validated_data['meta'] - if not self._is_assignee(instance): - self._pop_confirmed_fields() - - # Json 字段保存的坑😭 - old_meta = instance.meta - meta = {} - meta.update(old_meta) - meta.update(new_meta) - validated_data['meta'] = meta - - return super().update(instance, validated_data) - - def _pop_confirmed_fields(self): - meta = self.validated_data['meta'] - meta.pop('confirmed_assets', None) - meta.pop('confirmed_system_users', None) - - def _is_assignee(self, obj: Ticket): - user = self.context['request'].user - return obj.is_assignee(user) - - -class AssigneeSerializer(serializers.Serializer): - id = serializers.UUIDField() - name = serializers.CharField() - username = serializers.CharField() diff --git a/apps/tickets/serializers/ticket.py b/apps/tickets/serializers/ticket.py deleted file mode 100644 index 34724be3a..000000000 --- a/apps/tickets/serializers/ticket.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: utf-8 -*- -# -from django.utils.translation import ugettext_lazy as _ -from rest_framework import serializers - -from ..exceptions import ( - TicketClosed, OnlyTicketAssigneeCanOperate, - TicketCanNotOperate -) -from ..models import Ticket, Comment - -__all__ = ['TicketSerializer', 'CommentSerializer'] - - -class TicketSerializer(serializers.ModelSerializer): - class Meta: - model = Ticket - fields = [ - 'id', 'user', 'user_display', 'title', 'body', - 'assignees', 'assignees_display', 'assignee', 'assignee_display', - 'status', 'action', 'date_created', 'date_updated', - 'type', 'type_display', 'action_display', - ] - read_only_fields = [ - 'user_display', 'assignees_display', - 'date_created', 'date_updated', - ] - extra_kwargs = { - 'status': {'label': _('Status')}, - 'action': {'label': _('Action')}, - 'user_display': {'label': _('User')} - } - - def create(self, validated_data): - validated_data.pop('action') - return super().create(validated_data) - - def update(self, instance, validated_data): - action = validated_data.get('action') - user = self.context['request'].user - - if instance.type not in (Ticket.TYPE.GENERAL, - Ticket.TYPE.LOGIN_CONFIRM): - # 暂时的兼容操作吧,后期重构工单 - raise TicketCanNotOperate - - if instance.status == instance.STATUS.CLOSED: - raise TicketClosed - - if action: - if user not in instance.assignees.all(): - raise OnlyTicketAssigneeCanOperate - - # 有 `action` 时忽略 `status` - validated_data.pop('status', None) - - instance = super().update(instance, validated_data) - if not instance.status == instance.STATUS.CLOSED and action: - instance.perform_action(action, user) - else: - status = validated_data.get('status') - instance = super().update(instance, validated_data) - if status: - instance.perform_status(status, user) - - return instance - - -class CurrentTicket(object): - ticket = None - - def set_context(self, serializer_field): - self.ticket = serializer_field.context['ticket'] - - def __call__(self): - return self.ticket - - -class CommentSerializer(serializers.ModelSerializer): - user = serializers.HiddenField( - default=serializers.CurrentUserDefault(), - ) - ticket = serializers.HiddenField( - default=CurrentTicket() - ) - - class Meta: - model = Comment - fields = [ - 'id', 'ticket', 'body', 'user', 'user_display', - 'date_created', 'date_updated' - ] - read_only_fields = [ - 'user_display', 'date_created', 'date_updated' - ] diff --git a/apps/tickets/serializers/ticket/__init__.py b/apps/tickets/serializers/ticket/__init__.py new file mode 100644 index 000000000..bb2ee74d6 --- /dev/null +++ b/apps/tickets/serializers/ticket/__init__.py @@ -0,0 +1,2 @@ +from .ticket import * +from .meta import * diff --git a/apps/tickets/serializers/ticket/meta/__init__.py b/apps/tickets/serializers/ticket/meta/__init__.py new file mode 100644 index 000000000..7b5fbad28 --- /dev/null +++ b/apps/tickets/serializers/ticket/meta/__init__.py @@ -0,0 +1 @@ +from .meta import * diff --git a/apps/tickets/serializers/ticket/meta/meta.py b/apps/tickets/serializers/ticket/meta/meta.py new file mode 100644 index 000000000..1b3c071a8 --- /dev/null +++ b/apps/tickets/serializers/ticket/meta/meta.py @@ -0,0 +1,33 @@ +from tickets import const +from .ticket_type import apply_asset, apply_application, login_confirm + +__all__ = [ + 'type_serializer_classes_mapping', +] + +# ticket action +# ------------- + +action_open = const.TicketActionChoices.open.value +action_approve = const.TicketActionChoices.approve.value + + +# defines `meta` field dynamic mapping serializers +# ------------------------------------------------ + +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/__init__.py b/apps/tickets/serializers/ticket/meta/ticket_type/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py b/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py new file mode 100644 index 000000000..220c04199 --- /dev/null +++ b/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py @@ -0,0 +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 orgs.utils import tmp_to_org +from tickets.models import Ticket + +__all__ = [ + 'ApplyApplicationSerializer', 'ApplySerializer', 'ApproveSerializer', +] + + +class ApplySerializer(serializers.Serializer): + # 申请信息 + apply_category = serializers.ChoiceField( + required=True, choices=ApplicationCategoryChoices.choices, label=_('Category'), + allow_null=True, + ) + apply_category_display = serializers.CharField( + read_only=True, label=_('Category display'), allow_null=True, + ) + apply_type = serializers.ChoiceField( + required=True, choices=ApplicationTypeChoices.choices, label=_('Type'), + allow_null=True + ) + apply_type_display = serializers.CharField( + 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, allow_null=True + ) + apply_system_user_group = serializers.ListField( + required=False, child=serializers.CharField(), label=_('System user group'), + default=list, allow_null=True + ) + apply_date_start = serializers.DateTimeField( + required=True, label=_('Date start'), allow_null=True + ) + apply_date_expired = serializers.DateTimeField( + required=True, label=_('Date expired'), allow_null=True + ) + + +class ApproveSerializer(serializers.Serializer): + # 审批信息 + approve_applications = serializers.ListField( + required=True, child=serializers.UUIDField(), label=_('Approve applications'), + allow_null=True + ) + approve_applications_display = serializers.ListField( + required=False, read_only=True, child=serializers.CharField(), + label=_('Approve applications display'), allow_null=True, + default=list + ) + approve_system_users = serializers.ListField( + required=True, child=serializers.UUIDField(), label=_('Approve system users'), + allow_null=True + ) + approve_system_users_display = serializers.ListField( + required=False, read_only=True, child=serializers.CharField(), + label=_('Approve system user display'), allow_null=True, + default=list + ) + approve_date_start = serializers.DateTimeField( + required=True, label=_('Date start'), allow_null=True + ) + approve_date_expired = serializers.DateTimeField( + required=True, label=_('Date expired'), allow_null=True + ) + + def validate_approve_applications(self, approve_applications): + 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): + 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 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 new file mode 100644 index 000000000..ab613e19c --- /dev/null +++ b/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py @@ -0,0 +1,144 @@ +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 orgs.utils import tmp_to_org +from tickets.models import Ticket + + +__all__ = [ + 'ApplyAssetSerializer', 'ApplySerializer', 'ApproveSerializer', +] + + +class ApplySerializer(serializers.Serializer): + # 申请信息 + apply_ip_group = serializers.ListField( + required=False, child=serializers.IPAddressField(), label=_('IP group'), + default=list, allow_null=True, + ) + apply_hostname_group = serializers.ListField( + required=False, child=serializers.CharField(), label=_('Hostname group'), + default=list, allow_null=True, + ) + apply_system_user_group = serializers.ListField( + required=False, child=serializers.CharField(), label=_('System user group'), + default=list, allow_null=True + ) + apply_actions = ActionsField( + required=True, allow_null=True + ) + apply_actions_display = serializers.ListField( + required=False, read_only=True, child=serializers.CharField(), + label=_('Approve assets display'), allow_null=True, + default=list, + ) + apply_date_start = serializers.DateTimeField( + required=True, label=_('Date start'), allow_null=True, + ) + apply_date_expired = serializers.DateTimeField( + required=True, label=_('Date expired'), allow_null=True, + ) + + +class ApproveSerializer(serializers.Serializer): + # 审批信息 + approve_assets = serializers.ListField( + required=True, allow_null=True, child=serializers.UUIDField(), label=_('Approve assets') + ) + approve_assets_display = serializers.ListField( + required=False, read_only=True, child=serializers.CharField(), + label=_('Approve assets display'), allow_null=True, + default=list, + ) + approve_system_users = serializers.ListField( + required=True, allow_null=True, child=serializers.UUIDField(), + label=_('Approve system users') + ) + approve_system_users_display = serializers.ListField( + required=False, read_only=True, child=serializers.CharField(), + label=_('Approve assets display'), allow_null=True, + default=list, + ) + approve_actions = ActionsField( + required=True, allow_null=True, + ) + approve_actions_display = serializers.ListField( + required=False, read_only=True, child=serializers.CharField(), + label=_('Approve assets display'), allow_null=True, + default=list, + ) + approve_date_start = serializers.DateTimeField( + required=True, label=_('Date start'), allow_null=True, + ) + approve_date_expired = serializers.DateTimeField( + required=True, label=_('Date expired'), allow_null=True + ) + + def validate_approve_assets(self, approve_assets): + 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): + 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 `SystemUser` are found under Organization `{}`'.format(self.root.instance.org_name) + )) + + +class ApplyAssetSerializer(ApplySerializer, ApproveSerializer): + # 推荐信息 + 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 new file mode 100644 index 000000000..9308d0ee2 --- /dev/null +++ b/apps/tickets/serializers/ticket/meta/ticket_type/login_confirm.py @@ -0,0 +1,25 @@ + +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + + +__all__ = [ + 'ApplySerializer', 'LoginConfirmSerializer', +] + + +class ApplySerializer(serializers.Serializer): + # 申请信息 + apply_login_ip = serializers.IPAddressField( + required=True, label=_('Login ip'), allow_null=True + ) + apply_login_city = serializers.CharField( + required=True, max_length=64, label=_('Login city'), allow_null=True + ) + apply_login_datetime = serializers.DateTimeField( + required=True, label=_('Login datetime'), allow_null=True + ) + + +class LoginConfirmSerializer(ApplySerializer): + pass diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py new file mode 100644 index 000000000..b6ffc5140 --- /dev/null +++ b/apps/tickets/serializers/ticket/ticket.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers +from common.drf.serializers import MethodSerializer +from orgs.utils import get_org_by_id +from orgs.mixins.serializers import OrgResourceModelSerializerMixin +from users.models import User +from tickets.models import Ticket +from .meta import type_serializer_classes_mapping + + +__all__ = [ + 'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketApproveSerializer', +] + + +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')) + meta = MethodSerializer() + + class Meta: + model = Ticket + fields = [ + 'id', 'title', 'type', 'type_display', + 'meta', 'action', 'action_display', 'status', 'status_display', + 'applicant', 'applicant_display', 'processor', 'processor_display', + 'assignees', 'assignees_display', 'comment', + 'date_created', 'date_updated', + 'org_id', 'org_name', + 'body' + ] + + def get_meta_serializer(self): + request = self.context['request'] + default_serializer_class = serializers.Serializer + if isinstance(self.instance, Ticket): + _type = self.instance.type + else: + _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: + model = Ticket + fields = TicketSerializer.Meta.fields + read_only_fields = fields + + +class TicketApplySerializer(TicketSerializer): + org_id = serializers.CharField( + required=True, max_length=36, allow_blank=True, label=_("Organization") + ) + + class Meta: + model = Ticket + fields = TicketSerializer.Meta.fields + writeable_fields = [ + 'id', 'title', 'type', 'meta', 'assignees', 'comment', 'org_id' + ] + read_only_fields = list(set(fields) - set(writeable_fields)) + extra_kwargs = { + 'type': {'required': True}, + } + + def validate_type(self, tp): + request_type = self.context['request'].query_params.get('type') + if tp != request_type: + error = _( + 'The `type` in the submission data (`{}`) is different from the type ' + 'in the request url (`{}`)'.format(tp, request_type) + ) + raise serializers.ValidationError(error) + return tp + + @staticmethod + def validate_org_id(org_id): + org = get_org_by_id(org_id) + if not org: + error = _('The organization `{}` does not exist'.format(org_id)) + raise serializers.ValidationError(error) + return org_id + + def validate_assignees(self, assignees): + org_id = self.initial_data.get('org_id') + self.validate_org_id(org_id) + org = get_org_by_id(org_id) + admins = User.get_super_and_org_admins(org) + valid_assignees = list(set(assignees) & set(admins)) + if not valid_assignees: + error = _('None of the assignees belong to Organization `{}` admins'.format(org.name)) + raise serializers.ValidationError(error) + return valid_assignees + + +class TicketApproveSerializer(TicketSerializer): + + class Meta: + model = Ticket + fields = TicketSerializer.Meta.fields + writeable_fields = ['meta'] + read_only_fields = list(set(fields) - set(writeable_fields)) + + def validate_meta(self, meta): + _meta = self.instance.meta if self.instance else {} + _meta.update(meta) + return _meta diff --git a/apps/tickets/signals.py b/apps/tickets/signals.py new file mode 100644 index 000000000..10951716d --- /dev/null +++ b/apps/tickets/signals.py @@ -0,0 +1,4 @@ +from django.dispatch import Signal + + +post_change_ticket_action = Signal() diff --git a/apps/tickets/signals_handler.py b/apps/tickets/signals_handler.py deleted file mode 100644 index 811f6dabf..000000000 --- a/apps/tickets/signals_handler.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -# -from django.dispatch import receiver -from django.db.models.signals import m2m_changed, post_save, pre_save - -from common.utils import get_logger -from .models import Ticket, Comment -from .utils import ( - send_new_ticket_mail_to_assignees, - send_ticket_action_mail_to_user -) - - -logger = get_logger(__name__) - - -@receiver(m2m_changed, sender=Ticket.assignees.through) -def on_ticket_assignees_set(sender, instance=None, action=None, - reverse=False, model=None, - pk_set=None, **kwargs): - if action == 'post_add': - logger.debug('New ticket create, send mail: {}'.format(instance.id)) - assignees = model.objects.filter(pk__in=pk_set) - send_new_ticket_mail_to_assignees(instance, assignees) - if action.startswith('post') and not reverse: - instance.assignees_display = ', '.join([ - str(u) for u in instance.assignees.all() - ]) - instance.save() - - -@receiver(post_save, sender=Ticket) -def on_ticket_status_change(sender, instance=None, created=False, **kwargs): - if created or instance.status == "open": - return - logger.debug('Ticket changed, send mail: {}'.format(instance.id)) - send_ticket_action_mail_to_user(instance) - - -@receiver(pre_save, sender=Ticket) -def on_ticket_create(sender, instance=None, **kwargs): - instance.user_display = str(instance.user) - if instance.assignee: - instance.assignee_display = str(instance.assignee) - - -@receiver(pre_save, sender=Comment) -def on_comment_create(sender, instance=None, **kwargs): - instance.user_display = str(instance.user) diff --git a/apps/tickets/signals_handler/__init__.py b/apps/tickets/signals_handler/__init__.py new file mode 100644 index 000000000..ec0439b54 --- /dev/null +++ b/apps/tickets/signals_handler/__init__.py @@ -0,0 +1,2 @@ +from .ticket import * +from .comment import * diff --git a/apps/tickets/signals_handler/comment.py b/apps/tickets/signals_handler/comment.py new file mode 100644 index 000000000..a308ae973 --- /dev/null +++ b/apps/tickets/signals_handler/comment.py @@ -0,0 +1,9 @@ +from django.db.models.signals import pre_save +from django.dispatch import receiver + +from ..models import Comment + + +@receiver(pre_save, sender=Comment) +def on_comment_pre_save(sender, instance, **kwargs): + instance.set_display_fields() diff --git a/apps/tickets/signals_handler/ticket.py b/apps/tickets/signals_handler/ticket.py new file mode 100644 index 000000000..7e8bc8d5d --- /dev/null +++ b/apps/tickets/signals_handler/ticket.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# +from django.dispatch import receiver +from django.db.models.signals import m2m_changed + +from common.utils import get_logger +from tickets.models import Ticket +from tickets.utils import send_ticket_applied_mail_to_assignees +from ..signals import post_change_ticket_action + + +logger = get_logger(__name__) + + +@receiver(post_change_ticket_action, sender=Ticket) +def on_post_change_ticket_action(sender, ticket, action, **kwargs): + ticket.handler.dispatch(action) + + +@receiver(m2m_changed, sender=Ticket.assignees.through) +def on_ticket_assignees_changed(sender, instance, action, reverse, model, pk_set, **kwargs): + if reverse: + return + if action != 'post_add': + return + logger.debug('Receives ticket and assignees changed signal, ticket: {}'.format(instance.title)) + instance.assignees_display = [str(assignee) for assignee in instance.assignees.all()] + instance.save() + assignees = model.objects.filter(pk__in=pk_set) + assignees_display = [str(assignee) for assignee in assignees] + logger.debug('Send applied email to assignees: {}'.format(assignees_display)) + send_ticket_applied_mail_to_assignees(instance, assignees) + diff --git a/apps/tickets/tests.py b/apps/tickets/tests.py deleted file mode 100644 index ee696d223..000000000 --- a/apps/tickets/tests.py +++ /dev/null @@ -1,89 +0,0 @@ -import datetime - -from common.utils.timezone import now -from django.urls import reverse -from rest_framework.test import APITestCase -from rest_framework import status - -from orgs.models import Organization, OrganizationMember, ROLE as ORG_ROLE -from orgs.utils import set_current_org -from users.models.user import User -from assets.models import Asset, AdminUser, SystemUser - - -class TicketTest(APITestCase): - def setUp(self): - Organization.objects.bulk_create([ - Organization(name='org-01'), - Organization(name='org-02'), - Organization(name='org-03'), - ]) - org_01, org_02, org_03 = Organization.objects.all() - self.org_01, self.org_02, self.org_03 = org_01, org_02, org_03 - - set_current_org(org_01) - - AdminUser.objects.bulk_create([ - AdminUser(name='au-01', username='au-01'), - AdminUser(name='au-02', username='au-02'), - AdminUser(name='au-03', username='au-03'), - ]) - - SystemUser.objects.bulk_create([ - SystemUser(name='su-01', username='su-01'), - SystemUser(name='su-02', username='su-02'), - SystemUser(name='su-03', username='su-03'), - ]) - - admin_users = AdminUser.objects.all() - Asset.objects.bulk_create([ - Asset(hostname='asset-01', ip='192.168.1.1', public_ip='192.168.1.1', admin_user=admin_users[0]), - Asset(hostname='asset-02', ip='192.168.1.2', public_ip='192.168.1.2', admin_user=admin_users[0]), - Asset(hostname='asset-03', ip='192.168.1.3', public_ip='192.168.1.3', admin_user=admin_users[0]), - ]) - - new_user = User.objects.create - new_org_member = OrganizationMember.objects.create - - u = new_user(name='user-01', username='user-01', email='user-01@jms.com') - new_org_member(org=org_01, user=u, role=ORG_ROLE.USER) - new_org_member(org=org_02, user=u, role=ORG_ROLE.USER) - self.user_01 = u - - u = new_user(name='org-admin-01', username='org-admin-01', email='org-admin-01@jms.com') - new_org_member(org=org_01, user=u, role=ORG_ROLE.ADMIN) - self.org_admin_01 = u - - u = new_user(name='org-admin-02', username='org-admin-02', email='org-admin-02@jms.com') - new_org_member(org=org_02, user=u, role=ORG_ROLE.ADMIN) - self.org_admin_02 = u - - def test_create_request_asset_perm(self): - url = reverse('api-tickets:ticket-request-asset-perm') - ticket_url = reverse('api-tickets:ticket') - - self.client.force_login(self.user_01) - - date_start = now() - date_expired = date_start + datetime.timedelta(days=7) - - data = { - "title": "request-01", - "ips": [ - "192.168.1.1" - ], - "date_start": date_start, - "date_expired": date_expired, - "hostname": "", - "system_user": "", - "org_id": self.org_01.id, - "assignees": [ - str(self.org_admin_01.id), - str(self.org_admin_02.id), - ] - } - - self.client.post(data) - - self.client.force_login(self.org_admin_01) - res = self.client.get(ticket_url, params={'assgin': 1}) diff --git a/apps/tickets/tests/__init__.py b/apps/tickets/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/tickets/urls/api_urls.py b/apps/tickets/urls/api_urls.py index 36700db92..93286f645 100644 --- a/apps/tickets/urls/api_urls.py +++ b/apps/tickets/urls/api_urls.py @@ -7,13 +7,9 @@ from .. import api app_name = 'tickets' router = BulkRouter() -router.register('tickets/request-asset-perm/assignees', api.AssigneeViewSet, 'ticket-request-asset-perm-assignee') -router.register('tickets/request-asset-perm', api.RequestAssetPermTicketViewSet, 'ticket-request-asset-perm') router.register('tickets', api.TicketViewSet, 'ticket') -router.register('tickets/(?P[0-9a-zA-Z\-]{36})/comments', api.TicketCommentViewSet, 'ticket-comment') - - -urlpatterns = [ -] +router.register('assignees', api.AssigneeViewSet, 'assignee') +router.register('comments', api.CommentViewSet, 'comment') +urlpatterns = [] urlpatterns += router.urls diff --git a/apps/tickets/utils.py b/apps/tickets/utils.py index f3d6ee91e..ebd4c22b8 100644 --- a/apps/tickets/utils.py +++ b/apps/tickets/utils.py @@ -4,55 +4,67 @@ from urllib.parse import urljoin from django.conf import settings from django.utils.translation import ugettext as _ -from common.const.front_urls import TICKET_DETAIL from common.utils import get_logger from common.tasks import send_mail_async +from . import const -logger = get_logger(__name__) -from tickets.models import Ticket +logger = get_logger(__file__) -def send_new_ticket_mail_to_assignees(ticket: Ticket, assignees): - recipient_list = [user.email for user in assignees] - user = ticket.user - if not recipient_list: - logger.error("Ticket not has assignees: {}".format(ticket.id)) +def send_ticket_applied_mail_to_assignees(ticket, assignees): + if not assignees: + logger.debug("Not found assignees, ticket: {}({}), assignees: {}".format( + ticket, str(ticket.id), assignees) + ) return - subject = '{}: {}'.format(_("New ticket"), ticket.title) - # 这里要设置前端地址,因为要直接跳转到页面 - detail_url = urljoin(settings.SITE_URL, TICKET_DETAIL.format(id=ticket.id)) - message = _(""" -
+ subject = _('New Ticket: {} ({})'.format(ticket.title, ticket.get_type_display())) + ticket_detail_url = urljoin( + settings.SITE_URL, const.TICKET_DETAIL_URL.format(id=str(ticket.id)) + ) + message = _( + """

Your has a new ticket

- {body} + Ticket:
- click here to review + {body} +
+ click here to review
- """).format(body=ticket.body, user=user, url=detail_url) + """.format( + body=ticket.body.replace('\n', '
'), + ticket_detail_url=ticket_detail_url + ) + ) + if settings.DEBUG: + logger.debug(message) + recipient_list = [assignee.email for assignee in assignees] send_mail_async.delay(subject, message, recipient_list, html_message=message) -def send_ticket_action_mail_to_user(ticket): - if not ticket.user: - logger.error("Ticket not has user: {}".format(ticket.id)) +def send_ticket_processed_mail_to_applicant(ticket): + if not ticket.applicant: + logger.error("Not found applicant: {}({})".format(ticket.title, ticket.id)) return - user = ticket.user - recipient_list = [user.email] - subject = '{}: {}'.format(_("Ticket has been reply"), ticket.title) - message = _(""" + subject = _('Ticket has processed: {} ({})').format(ticket.title, ticket.get_type_display()) + message = _( + """
-

Your ticket has been replay

+

Your ticket has been processed

- Title: {ticket.title} + Ticket:
- Assignee: {ticket.assignee_display} -
- Status: {ticket.status_display} + {body}
- """).format(ticket=ticket) + """.format( + body=ticket.body.replace('\n', '
'), + ) + ) + if settings.DEBUG: + logger.debug(message) + recipient_list = [ticket.applicant.email] send_mail_async.delay(subject, message, recipient_list, html_message=message) diff --git a/apps/users/api/group.py b/apps/users/api/group.py index f91b1d3bd..27196c2f3 100644 --- a/apps/users/api/group.py +++ b/apps/users/api/group.py @@ -12,7 +12,7 @@ __all__ = ['UserGroupViewSet'] class UserGroupViewSet(OrgBulkModelViewSet): model = UserGroup - filter_fields = ("name",) - search_fields = filter_fields + filterset_fields = ("name",) + search_fields = filterset_fields permission_classes = (IsOrgAdmin,) serializer_class = UserGroupSerializer diff --git a/apps/users/api/relation.py b/apps/users/api/relation.py index bbcb5932b..050d9e4e6 100644 --- a/apps/users/api/relation.py +++ b/apps/users/api/relation.py @@ -12,8 +12,8 @@ __all__ = ['UserUserGroupRelationViewSet'] class UserUserGroupRelationViewSet(JMSBulkRelationModelViewSet): - filter_fields = ('user', 'usergroup') - search_fields = filter_fields + filterset_fields = ('user', 'usergroup') + search_fields = filterset_fields serializer_class = serializers.UserUserGroupRelationSerializer permission_classes = (IsOrgAdmin,) m2m_field = User.groups.field diff --git a/apps/users/api/user.py b/apps/users/api/user.py index b0b209e19..1ac03af5e 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -32,8 +32,8 @@ __all__ = [ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet): - filter_fields = ('username', 'email', 'name', 'id', 'source') - search_fields = filter_fields + filterset_fields = ('username', 'email', 'name', 'id', 'source') + search_fields = filterset_fields permission_classes = (IsOrgAdmin, CanUpdateDeleteUser) serializer_classes = { 'default': UserSerializer, diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 2dcbd452d..36e5eb330 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -18,7 +18,7 @@ from django.shortcuts import reverse from common.local import LOCAL_DYNAMIC_SETTINGS from orgs.utils import current_org -from orgs.models import OrganizationMember +from orgs.models import OrganizationMember, Organization from common.utils import date_expired_default, get_logger, lazyproperty from common import fields from common.const import choices @@ -327,7 +327,30 @@ class RoleMixin: def remove(self): if not current_org.is_real(): return - OrganizationMember.objects.remove_users(current_org, [self]) + org = Organization.get_instance(current_org.id) + OrganizationMember.objects.remove_users(org, [self]) + + @classmethod + def get_super_admins(cls): + return cls.objects.filter(role=cls.ROLE.ADMIN) + + @classmethod + def get_org_admins(cls, org=None): + from orgs.models import Organization + if not isinstance(org, Organization): + org = current_org + org_admins = org.admins + return org_admins + + @classmethod + def get_super_and_org_admins(cls, org=None): + super_admins = cls.get_super_admins() + super_admins_id = list(super_admins.values_list('id', flat=True)) + org_admins = cls.get_org_admins(org) + org_admins_id = list(org_admins.values_list('id', flat=True)) + admins_id = set(org_admins_id + super_admins_id) + admins = User.objects.filter(id__in=admins_id) + return admins class TokenMixin: diff --git a/apps/users/serializers/group.py b/apps/users/serializers/group.py index 102b562a8..b7a6d204c 100644 --- a/apps/users/serializers/group.py +++ b/apps/users/serializers/group.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from django.db.models import Prefetch from rest_framework import serializers -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer from django.db.models import Count from ..models import User, UserGroup diff --git a/apps/users/views/profile/reset.py b/apps/users/views/profile/reset.py index c25726561..50897e112 100644 --- a/apps/users/views/profile/reset.py +++ b/apps/users/views/profile/reset.py @@ -39,20 +39,23 @@ class UserForgotPasswordView(FormView): form_class = forms.UserForgotPasswordForm def form_valid(self, form): - request = self.request email = form.cleaned_data['email'] user = get_object_or_none(User, email=email) if not user: error = _('Email address invalid, please input again') form.add_error('email', error) return self.form_invalid(form) - elif not user.can_update_password(): - error = _('User auth from {}, go there change password') - form.add_error('email', error.format(user.get_source_display())) + + if not user.is_local: + error = _( + 'The user is from A, please go to the corresponding system to change the password' + ''.format(user.get_source_display()) + ) + form.add_error('email', error) return self.form_invalid(form) - else: - send_reset_password_mail(user) - return redirect('authentication:forgot-password-sendmail-success') + + send_reset_password_mail(user) + return redirect('authentication:forgot-password-sendmail-success') class UserForgotPasswordSendmailSuccessView(TemplateView): diff --git a/jms b/jms index 7e4f2faed..da671373c 100755 --- a/jms +++ b/jms @@ -197,7 +197,7 @@ def get_start_gunicorn_kwargs(): print("\n- Start Gunicorn WSGI HTTP Server") prepare() bind = '{}:{}'.format(HTTP_HOST, HTTP_PORT) - log_format = '%(h)s %(t)s "%(r)s" %(s)s %(b)s ' + log_format = '%(h)s %(t)s %(L)ss "%(r)s" %(s)s %(b)s ' cmd = [ 'gunicorn', 'jumpserver.wsgi', diff --git a/requirements/requirements.txt b/requirements/requirements.txt index b635353de..9a3eeb1b9 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -77,7 +77,7 @@ drf-nested-routers==0.91 aliyun-python-sdk-core-v3==2.9.1 aliyun-python-sdk-ecs==4.10.1 rest_condition==1.0.3 -python-ldap==3.1.0 +python-ldap==3.3.1 tencentcloud-sdk-python==3.0.40 django-radius==1.4.0 ipip-ipdb==1.2.1 @@ -108,3 +108,4 @@ adal==1.2.5 openpyxl==3.0.5 pyexcel==0.6.6 pyexcel-xlsx==0.6.0 +data-tree==0.0.1