From 7fa94008c97607dd3c6f5c5e0ed27224584d1310 Mon Sep 17 00:00:00 2001 From: xinwen Date: Thu, 19 Nov 2020 14:49:50 +0800 Subject: [PATCH 01/57] =?UTF-8?q?fix(old-api):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E6=97=A7=E7=9A=84=E7=BB=84=E7=BB=87=E4=B8=8E=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=85=B3=E8=81=94=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/orgs/middleware.py | 1 - apps/orgs/serializers.py | 35 +++++++++++++++++++---------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/apps/orgs/middleware.py b/apps/orgs/middleware.py index efbee2dde..2448fffc3 100644 --- a/apps/orgs/middleware.py +++ b/apps/orgs/middleware.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # -from .models import Organization from .utils import get_org_from_request, set_current_org diff --git a/apps/orgs/serializers.py b/apps/orgs/serializers.py index 7a57b4fac..c4402d433 100644 --- a/apps/orgs/serializers.py +++ b/apps/orgs/serializers.py @@ -74,26 +74,29 @@ class OrgMemberSerializer(BulkModelSerializer): ).distinct() -class OrgMemberAdminSerializer(BulkModelSerializer): +class OrgMemberOldBaseSerializer(BulkModelSerializer): + organization = serializers.PrimaryKeyRelatedField( + label=_('Organization'), queryset=Organization.objects.all(), required=True, source='org' + ) + + def to_internal_value(self, data): + view = self.context['view'] + org_id = view.kwargs.get('org_id') + if org_id: + data['organization'] = org_id + return super().to_internal_value(data) + + class Meta: + model = OrganizationMember + fields = ('id', 'organization', 'user', 'role') + + +class OrgMemberAdminSerializer(OrgMemberOldBaseSerializer): role = serializers.HiddenField(default=ROLE.ADMIN) - organization = serializers.PrimaryKeyRelatedField( - label=_('Organization'), queryset=Organization.objects.all(), required=True, source='org' - ) - - class Meta: - model = OrganizationMember - fields = ('id', 'organization', 'user', 'role') -class OrgMemberUserSerializer(BulkModelSerializer): +class OrgMemberUserSerializer(OrgMemberOldBaseSerializer): role = serializers.HiddenField(default=ROLE.USER) - organization = serializers.PrimaryKeyRelatedField( - label=_('Organization'), queryset=Organization.objects.all(), required=True, source='org' - ) - - class Meta: - model = OrganizationMember - fields = ('id', 'organization', 'user', 'role') class OrgRetrieveSerializer(OrgReadSerializer): From 6d39a51c3666a1fe76778a348c913f37f19eb757 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Thu, 19 Nov 2020 15:50:31 +0800 Subject: [PATCH 02/57] =?UTF-8?q?[fix]:=20=E5=85=BC=E5=AE=B9django=203=20(?= =?UTF-8?q?#5038)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(django): 修改版本依赖 * [fix]: 兼容django 3 * fix(merge): 去掉不用的JSONField * fix(requirements): 修改加密库的版本 Co-authored-by: ibuler --- .../migrations/0007_auto_20201119_1110.py | 18 +++++++++++++++ apps/applications/models/application.py | 3 +-- apps/authentication/backends/api.py | 2 +- apps/common/fields/form.py | 2 +- apps/common/fields/model.py | 6 ++--- apps/common/fields/serializer.py | 2 +- apps/jumpserver/conf.py | 2 +- apps/jumpserver/context_processor.py | 2 +- apps/jumpserver/settings/libs.py | 2 +- .../migrations/0031_auto_20201118_1801.py | 18 +++++++++++++++ requirements/requirements.txt | 22 +++++++++---------- requirements/rpm_requirements.txt | 2 +- 12 files changed, 58 insertions(+), 23 deletions(-) create mode 100644 apps/applications/migrations/0007_auto_20201119_1110.py create mode 100644 apps/users/migrations/0031_auto_20201118_1801.py diff --git a/apps/applications/migrations/0007_auto_20201119_1110.py b/apps/applications/migrations/0007_auto_20201119_1110.py new file mode 100644 index 000000000..e206f8404 --- /dev/null +++ b/apps/applications/migrations/0007_auto_20201119_1110.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-11-19 03:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0006_application'), + ] + + operations = [ + migrations.AlterField( + model_name='application', + name='attrs', + field=models.JSONField(), + ), + ] diff --git a/apps/applications/models/application.py b/apps/applications/models/application.py index 8bf2c7401..1c8ae98f0 100644 --- a/apps/applications/models/application.py +++ b/apps/applications/models/application.py @@ -2,7 +2,6 @@ from itertools import chain from django.db import models from django.utils.translation import ugettext_lazy as _ -from django_mysql.models import JSONField, QuerySet from orgs.mixins.models import OrgModelMixin from common.mixins import CommonModelMixin @@ -123,7 +122,7 @@ class Application(CommonModelMixin, OrgModelMixin): 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 = JSONField() + attrs = models.JSONField() comment = models.TextField( max_length=128, default='', blank=True, verbose_name=_('Comment') ) diff --git a/apps/authentication/backends/api.py b/apps/authentication/backends/api.py index 4cdbbbe53..1fd315abb 100644 --- a/apps/authentication/backends/api.py +++ b/apps/authentication/backends/api.py @@ -6,7 +6,7 @@ import time from django.core.cache import cache from django.utils.translation import ugettext as _ -from django.utils.six import text_type +from six import text_type from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend from rest_framework import HTTP_HEADER_ENCODING diff --git a/apps/common/fields/form.py b/apps/common/fields/form.py index c4cdc78ad..fd144ec92 100644 --- a/apps/common/fields/form.py +++ b/apps/common/fields/form.py @@ -3,7 +3,7 @@ import json from django import forms -from django.utils import six +import six from django.core.exceptions import ValidationError from django.utils.translation import ugettext as _ from ..utils import signer diff --git a/apps/common/fields/model.py b/apps/common/fields/model.py index 1161944a6..4a4f3525d 100644 --- a/apps/common/fields/model.py +++ b/apps/common/fields/model.py @@ -31,7 +31,7 @@ class JsonMixin: def json_encode(data): return json.dumps(data) - def from_db_value(self, value, expression, connection, context): + def from_db_value(self, value, expression, connection, context=None): if value is None: return value return self.json_decode(value) @@ -54,7 +54,7 @@ class JsonMixin: class JsonTypeMixin(JsonMixin): tp = dict - def from_db_value(self, value, expression, connection, context): + def from_db_value(self, value, expression, connection, context=None): value = super().from_db_value(value, expression, connection, context) if not isinstance(value, self.tp): value = self.tp() @@ -116,7 +116,7 @@ class EncryptMixin: def decrypt_from_signer(self, value): return signer.unsign(value) or '' - def from_db_value(self, value, expression, connection, context): + def from_db_value(self, value, expression, connection, context=None): if value is None: return value value = force_text(value) diff --git a/apps/common/fields/serializer.py b/apps/common/fields/serializer.py index e7a6e7d9c..9cd630650 100644 --- a/apps/common/fields/serializer.py +++ b/apps/common/fields/serializer.py @@ -2,7 +2,7 @@ # from rest_framework import serializers -from django.utils import six +import six __all__ = [ diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index e23ddc23c..4d8b7c0ce 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -16,7 +16,7 @@ import json import yaml from importlib import import_module from django.urls import reverse_lazy -from django.contrib.staticfiles.templatetags.staticfiles import static +from django.templatetags.static import static from urllib.parse import urljoin, urlparse from django.utils.translation import ugettext_lazy as _ diff --git a/apps/jumpserver/context_processor.py b/apps/jumpserver/context_processor.py index 0a49b957d..cf0ea559d 100644 --- a/apps/jumpserver/context_processor.py +++ b/apps/jumpserver/context_processor.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -from django.contrib.staticfiles.templatetags.staticfiles import static +from django.templatetags.static import static from django.conf import settings from django.utils.translation import gettext_lazy as _ diff --git a/apps/jumpserver/settings/libs.py b/apps/jumpserver/settings/libs.py index 9e4b56e21..e60932464 100644 --- a/apps/jumpserver/settings/libs.py +++ b/apps/jumpserver/settings/libs.py @@ -11,7 +11,7 @@ REST_FRAMEWORK = { ), 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.JSONRenderer', - 'rest_framework.renderers.BrowsableAPIRenderer', + # 'rest_framework.renderers.BrowsableAPIRenderer', 'common.drf.renders.JMSCSVRender', ), 'DEFAULT_PARSER_CLASSES': ( diff --git a/apps/users/migrations/0031_auto_20201118_1801.py b/apps/users/migrations/0031_auto_20201118_1801.py new file mode 100644 index 000000000..6c0f22303 --- /dev/null +++ b/apps/users/migrations/0031_auto_20201118_1801.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-11-18 10:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0030_auto_20200819_2041'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='first_name', + field=models.CharField(blank=True, max_length=150, verbose_name='first name'), + ), + ] diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 8fbaf4cf7..dc7ca9b85 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -12,20 +12,20 @@ chardet==3.0.4 configparser==3.5.0 coreapi==2.3.3 coreschema==0.0.4 -cryptography==2.8 +cryptography==3.2 decorator==4.1.2 -Django==2.2.13 -django-auth-ldap==1.7.0 -django-bootstrap3==9.1.0 +Django==3.1 +django-auth-ldap==2.2.0 +django-bootstrap3==14.2.0 django-celery-beat==2.0 -django-filter==2.0.0 -django-formtools==2.1 +django-filter==2.4.0 +django-formtools==2.2 django-ranged-response==0.2.0 django-redis-cache==2.1.1 -django-rest-swagger==2.1.2 -django-simple-captcha==0.5.6 +django-rest-swagger==2.2.0 +django-simple-captcha==0.5.13 django-timezone-field==4.0 -djangorestframework==3.9.4 +djangorestframework==3.12.2 djangorestframework-bulk==0.2.1 docutils==0.14 ecdsa==0.13.3 @@ -44,7 +44,7 @@ jmespath==0.9.3 kombu==4.6.8 ldap3==2.4 MarkupSafe==1.1.1 -mysqlclient==1.3.14 +mysqlclient==2.0.1 olefile==0.44 openapi-codec==1.3.2 paramiko==2.4.2 @@ -69,7 +69,7 @@ sshpubkeys==3.1.0 uritemplate==3.0.0 urllib3==1.25.2 vine==1.3.0 -drf-yasg==1.9.1 +drf-yasg==1.20.0 Werkzeug==0.15.3 drf-nested-routers==0.91 aliyun-python-sdk-core-v3==2.9.1 diff --git a/requirements/rpm_requirements.txt b/requirements/rpm_requirements.txt index 2e02f07bd..b6a192d1a 100644 --- a/requirements/rpm_requirements.txt +++ b/requirements/rpm_requirements.txt @@ -1 +1 @@ -gcc krb5-devel libtiff-devel libjpeg-devel libzip-devel freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel sshpass openldap-devel mariadb-devel mysql-devel mysql libffi-devel openssh-clients telnet openldap-clients +gcc krb5-devel libtiff-devel libjpeg-devel libzip-devel freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel sshpass openldap-devel mariadb-devel mysql-community-devel mysql libffi-devel openssh-clients telnet openldap-clients From f2fd9f59903356387b5cdefadaba0efc029414e6 Mon Sep 17 00:00:00 2001 From: xinwen Date: Fri, 20 Nov 2020 14:54:59 +0800 Subject: [PATCH 03/57] =?UTF-8?q?perf(assets):=20=E9=99=90=E5=88=B6?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E6=8E=88=E6=9D=83=E8=B5=84=E4=BA=A7=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E7=9A=84=E6=9D=A1=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/asset/user_permission/user_permission_assets.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 949f12c67..35b9da257 100644 --- a/apps/perms/api/asset/user_permission/user_permission_assets.py +++ b/apps/perms/api/asset/user_permission/user_permission_assets.py @@ -3,6 +3,7 @@ from perms.api.asset.user_permission.mixin import UserNodeGrantStatusDispatchMixin from rest_framework.generics import ListAPIView from rest_framework.response import Response +from rest_framework.request import Request from django.conf import settings from assets.api.mixin import SerializeToTreeNodeMixin @@ -55,8 +56,12 @@ class AssetsAsTreeMixin(SerializeToTreeNodeMixin): """ 将 资产 序列化成树的结构返回 """ - def list(self, request, *args, **kwargs): + def list(self, request: Request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) + if request.query_params.get('search'): + # 如果用户搜索的条件不精准,会导致返回大量的无意义数据。 + # 这里限制一下返回数据的最大条数 + queryset = queryset[:999] data = self.serialize_assets(queryset, None) return Response(data=data) From bf3056abc406536f58e466b83c5fce5432cc93c4 Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 20 Nov 2020 15:23:33 +0800 Subject: [PATCH 04/57] =?UTF-8?q?fix(django3):=20=E4=BF=AE=E5=A4=8Ddjango3?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/common/validators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/common/validators.py b/apps/common/validators.py index d9b5f74a4..0ce334552 100644 --- a/apps/common/validators.py +++ b/apps/common/validators.py @@ -14,9 +14,9 @@ alphanumeric = RegexValidator(r'^[0-9a-zA-Z_@\-\.]*$', _('Special char not allow class ProjectUniqueValidator(UniqueTogetherValidator): - def __call__(self, attrs): + def __call__(self, attrs, serializer): try: - super().__call__(attrs) + super().__call__(attrs, serializer) except ValidationError as e: errors = {} for field in self.fields: From 73ccf3be5fa623e105f363af84cbad54d7e34f03 Mon Sep 17 00:00:00 2001 From: xinwen Date: Sun, 22 Nov 2020 11:06:39 +0800 Subject: [PATCH 05/57] =?UTF-8?q?fix(perms):=20=E5=BD=93=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=8E=88=E6=9D=83=E4=B8=BA=E7=A9=BA=E6=97=B6=EF=BC=8C=E6=B8=85?= =?UTF-8?q?=E7=A9=BA=E6=97=A7=E7=9A=84=E6=8E=88=E6=9D=83=E6=A0=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/perms/utils/asset/user_permission.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/perms/utils/asset/user_permission.py b/apps/perms/utils/asset/user_permission.py index 274a27d4d..b3529966d 100644 --- a/apps/perms/utils/asset/user_permission.py +++ b/apps/perms/utils/asset/user_permission.py @@ -215,7 +215,7 @@ def compute_tmp_mapping_node_from_perm(user: User, asset_perms_id=None): return [*leaf_nodes, *ancestors] -def create_mapping_nodes(user, nodes, clear=True): +def create_mapping_nodes(user, nodes): to_create = [] for node in nodes: _granted = getattr(node, TMP_GRANTED_FIELD, False) @@ -231,8 +231,6 @@ def create_mapping_nodes(user, nodes, clear=True): assets_amount=_granted_assets_amount, )) - if clear: - UserGrantedMappingNode.objects.filter(user=user).delete() UserGrantedMappingNode.objects.bulk_create(to_create) @@ -254,6 +252,9 @@ def set_node_granted_assets_amount(user, node, asset_perms_id=None): @tmp_to_root_org() def rebuild_user_mapping_nodes(user): logger.info(f'>>> {dt_formater(now())} start rebuild {user} mapping nodes') + + # 先删除旧的授权树🌲 + UserGrantedMappingNode.objects.filter(user=user).delete() asset_perms_id = get_user_all_assetpermissions_id(user) if not asset_perms_id: # 没有授权直接返回 From 21993b0d893e21ac5a056e1cd8ac63042fe90b54 Mon Sep 17 00:00:00 2001 From: xinwen Date: Sat, 21 Nov 2020 19:42:23 +0800 Subject: [PATCH 06/57] =?UTF-8?q?perf(perms):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=8E=88=E6=9D=83=E8=B5=84=E4=BA=A7=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=8A=A0=E8=BD=BD=E9=80=9F=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/node.py | 2 +- apps/perms/utils/asset/user_permission.py | 24 ++--------------------- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 5eb91e575..273ac667f 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -173,7 +173,7 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi): return [] assets = self.instance.get_assets().only( "id", "hostname", "ip", "os", - "org_id", "protocols", + "org_id", "protocols", "is_active" ) return self.serialize_assets(assets, self.instance.key) diff --git a/apps/perms/utils/asset/user_permission.py b/apps/perms/utils/asset/user_permission.py index b3529966d..aebc08e71 100644 --- a/apps/perms/utils/asset/user_permission.py +++ b/apps/perms/utils/asset/user_permission.py @@ -34,27 +34,6 @@ TMP_ASSET_GRANTED_FIELD = '_asset_granted' TMP_GRANTED_ASSETS_AMOUNT_FIELD = '_granted_assets_amount' -# 使用场景 -# Asset.objects.filter(get_user_resources_q_granted_by_permissions(user)) -def get_user_resources_q_granted_by_permissions(user: User): - """ - 获取用户关联的 asset permission 或者 用户组关联的 asset permission 获取规则, - 前提 AssetPermission 对象中的 related_name 为 granted_by_permissions - :param user: - :return: - """ - _now = now() - return reduce(and_, ( - Q(granted_by_permissions__date_start__lt=_now), - Q(granted_by_permissions__date_expired__gt=_now), - Q(granted_by_permissions__is_active=True), - ( - Q(granted_by_permissions__users=user) | - Q(granted_by_permissions__user_groups__users=user) - ) - )) - - # 使用场景 # `Node.objects.annotate(**node_annotate_mapping_node)` node_annotate_mapping_node = { @@ -385,7 +364,8 @@ def get_node_all_granted_assets(user: User, key): if only_asset_granted_nodes_qs: only_asset_granted_nodes_q = reduce(or_, only_asset_granted_nodes_qs) - only_asset_granted_nodes_q &= get_user_resources_q_granted_by_permissions(user) + asset_perms_id = get_user_all_assetpermissions_id(user) + only_asset_granted_nodes_q &= Q(granted_by_permissions__id__in=list(asset_perms_id)) q.append(only_asset_granted_nodes_q) if q: From a7c704bea326b859704501ffbacf0f5138c43a44 Mon Sep 17 00:00:00 2001 From: xinwen Date: Fri, 20 Nov 2020 16:12:24 +0800 Subject: [PATCH 07/57] =?UTF-8?q?perf(celery-task):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E8=8A=82=E7=82=B9=E8=B5=84=E4=BA=A7=E6=95=B0?= =?UTF-8?q?=E9=87=8F=E7=9A=84=20Celery=20=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/tasks/nodes_amount.py | 7 +++---- apps/assets/utils.py | 9 +++++++-- apps/ops/celery/__init__.py | 13 ------------- apps/perms/tasks.py | 5 ++++- jms | 15 ++++++++++++--- 5 files changed, 26 insertions(+), 23 deletions(-) diff --git a/apps/assets/tasks/nodes_amount.py b/apps/assets/tasks/nodes_amount.py index 4d53be525..0ec0810a0 100644 --- a/apps/assets/tasks/nodes_amount.py +++ b/apps/assets/tasks/nodes_amount.py @@ -1,14 +1,13 @@ from celery import shared_task +from ops.celery.decorator import register_as_period_task from assets.utils import check_node_assets_amount from common.utils import get_logger -from common.utils.timezone import now logger = get_logger(__file__) -@shared_task() +@register_as_period_task(crontab='0 2 * * *') +@shared_task(queue='node_assets_amount') def check_node_assets_amount_celery_task(): - logger.info(f'>>> {now()} begin check_node_assets_amount_celery_task ...') check_node_assets_amount() - logger.info(f'>>> {now()} end check_node_assets_amount_celery_task ...') diff --git a/apps/assets/utils.py b/apps/assets/utils.py index 7fc5a19ac..f04c06d0b 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils.py @@ -1,5 +1,7 @@ # ~*~ coding: utf-8 ~*~ # +import time + from django.db.models import Q from common.utils import get_logger, dict_get_any, is_uuid, get_object_or_none @@ -12,15 +14,18 @@ logger = get_logger(__file__) def check_node_assets_amount(): for node in Node.objects.all(): + logger.info(f'Check node assets amount: {node}') assets_amount = Asset.objects.filter( Q(nodes__key__istartswith=f'{node.key}:') | Q(nodes=node) ).distinct().count() if node.assets_amount != assets_amount: - print(f'>>> wrong assets amount ' - f'{node.assets_amount} right is {assets_amount}') + logger.warn(f'Node wrong assets amount ' + f'{node.assets_amount} right is {assets_amount}') node.assets_amount = assets_amount node.save() + # 防止自检程序给数据库的压力太大 + time.sleep(2) def is_asset_exists_in_node(asset_pk, node_key): diff --git a/apps/ops/celery/__init__.py b/apps/ops/celery/__init__.py index b8ed56be1..0ded6bc52 100644 --- a/apps/ops/celery/__init__.py +++ b/apps/ops/celery/__init__.py @@ -29,16 +29,3 @@ configs["CELERY_ROUTES"] = { app.namespace = 'CELERY' app.conf.update(configs) app.autodiscover_tasks(lambda: [app_config.split('.')[0] for app_config in settings.INSTALLED_APPS]) - -app.conf.beat_schedule = { - 'check-asset-permission-expired': { - 'task': 'perms.tasks.check_asset_permission_expired', - 'schedule': settings.PERM_EXPIRED_CHECK_PERIODIC, - 'args': () - }, - 'check-node-assets-amount': { - 'task': 'assets.tasks.nodes_amount.check_node_assets_amount_celery_task', - 'schedule': crontab(minute=0, hour=0), - 'args': () - }, -} diff --git a/apps/perms/tasks.py b/apps/perms/tasks.py index 7e940d594..fbf2ce8be 100644 --- a/apps/perms/tasks.py +++ b/apps/perms/tasks.py @@ -5,10 +5,12 @@ from datetime import timedelta from django.db import transaction from django.db.models import Q from django.db.transaction import atomic +from django.conf import settings from celery import shared_task from common.utils import get_logger from common.utils.timezone import now, dt_formater, dt_parser from users.models import User +from ops.celery.decorator import register_as_period_task from assets.models import Node from perms.models import RebuildUserTreeTask, AssetPermission from perms.utils.asset.user_permission import rebuild_user_mapping_nodes_if_need_with_lock, lock @@ -33,7 +35,8 @@ def dispatch_mapping_node_tasks(): rebuild_user_mapping_nodes_celery_task.delay(id) -@shared_task(queue='check_asset_perm_expired') +@register_as_period_task(interval=settings.PERM_EXPIRED_CHECK_PERIODIC) +@shared_task(queue='celery_check_asset_perm_expired') @atomic() def check_asset_permission_expired(): """ diff --git a/jms b/jms index 3969b5418..d3ede99ff 100755 --- a/jms +++ b/jms @@ -156,7 +156,10 @@ def is_running(s, unlink=True): def parse_service(s): web_services = ['gunicorn', 'flower', 'daphne'] - celery_services = ["celery_ansible", "celery_default", "celery_node_tree", "check_asset_perm_expired"] + celery_services = [ + "celery_ansible", "celery_default", "celery_node_tree", + "celery_check_asset_perm_expired", "celery_node_assets_amount" + ] task_services = celery_services + ['beat'] all_services = web_services + task_services if s == 'all': @@ -225,9 +228,14 @@ def get_start_celery_node_tree_kwargs(): return get_start_worker_kwargs('node_tree', 2) +def get_start_celery_node_assets_amount_kwargs(): + print("\n- Start Celery as Distributed Task Queue: NodeAssetsAmount") + return get_start_worker_kwargs('celery_node_assets_amount', 1) + + def get_start_celery_check_asset_perm_expired_kwargs(): print("\n- Start Celery as Distributed Task Queue: CheckAseetPermissionExpired") - return get_start_worker_kwargs('check_asset_perm_expired', 1) + return get_start_worker_kwargs('celery_check_asset_perm_expired', 1) def get_start_worker_kwargs(queue, num): @@ -366,7 +374,8 @@ def start_service(s): "celery_ansible": get_start_celery_ansible_kwargs, "celery_default": get_start_celery_default_kwargs, "celery_node_tree": get_start_celery_node_tree_kwargs, - "check_asset_perm_expired": get_start_celery_check_asset_perm_expired_kwargs, + "celery_node_assets_amount": get_start_celery_node_assets_amount_kwargs, + "celery_check_asset_perm_expired": get_start_celery_check_asset_perm_expired_kwargs, "beat": get_start_beat_kwargs, "flower": get_start_flower_kwargs, "daphne": get_start_daphne_kwargs, From 68b22cbdec0e1102ee3b1e060705c2755f25ee99 Mon Sep 17 00:00:00 2001 From: xinwen Date: Sun, 22 Nov 2020 14:09:38 +0800 Subject: [PATCH 08/57] =?UTF-8?q?fix(perms):=20=E4=BF=AE=E5=A4=8D=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=BB=84=E6=8E=88=E6=9D=83=E6=A0=91=E4=B8=8E=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/perms/api/asset/user_group_permission.py | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/apps/perms/api/asset/user_group_permission.py b/apps/perms/api/asset/user_group_permission.py index b595e4132..fa71da9b5 100644 --- a/apps/perms/api/asset/user_group_permission.py +++ b/apps/perms/api/asset/user_group_permission.py @@ -32,9 +32,6 @@ class UserGroupMixin: class UserGroupGrantedAssetsApi(ListAPIView): - """ - 获取用户组直接授权的资产 - """ permission_classes = (IsOrgAdminOrAppUser,) serializer_class = serializers.AssetGrantedSerializer only_fields = serializers.AssetGrantedSerializer.Meta.only_fields @@ -44,11 +41,27 @@ class UserGroupGrantedAssetsApi(ListAPIView): def get_queryset(self): user_group_id = self.kwargs.get('pk', '') - return Asset.objects.filter( - Q(granted_by_permissions__user_groups__id=user_group_id) + asset_perms_id = list(AssetPermission.objects.valid().filter( + user_groups__id=user_group_id + ).distinct().values_list('id', flat=True)) + + granted_node_keys = Node.objects.filter( + granted_by_permissions__id__in=asset_perms_id, + ).distinct().values_list('key', flat=True) + + granted_q = Q() + for _key in granted_node_keys: + granted_q |= Q(nodes__key__startswith=f'{_key}:') + granted_q |= Q(nodes__key=_key) + + granted_q |= Q(granted_by_permissions__id__in=asset_perms_id) + + assets = Asset.objects.filter( + granted_q ).distinct().only( *self.only_fields ) + return assets class UserGroupGrantedNodeAssetsApi(ListAPIView): @@ -66,7 +79,7 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView): granted = AssetPermission.objects.filter( user_groups__id=user_group_id, nodes__id=node_id - ).exists() + ).valid().exists() if granted: assets = Asset.objects.filter( Q(nodes__key__startswith=f'{node.key}:') | @@ -74,8 +87,12 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView): ) return assets else: + asset_perms_id = list(AssetPermission.objects.valid().filter( + user_groups__id=user_group_id + ).distinct().values_list('id', flat=True)) + granted_node_keys = Node.objects.filter( - granted_by_permissions__user_groups__id=user_group_id, + granted_by_permissions__id__in=asset_perms_id, key__startswith=f'{node.key}:' ).distinct().values_list('key', flat=True) @@ -85,7 +102,7 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView): granted_node_q |= Q(nodes__key=_key) granted_asset_q = ( - Q(granted_by_permissions__user_groups__id=user_group_id) & + Q(granted_by_permissions__id__in=asset_perms_id) & ( Q(nodes__key__startswith=f'{node.key}:') | Q(nodes__key=node.key) @@ -129,12 +146,16 @@ class UserGroupGrantedNodeChildrenAsTreeApi(SerializeToTreeNodeMixin, ListAPIVie group_id = self.kwargs.get('pk') node_key = self.request.query_params.get('key', None) + asset_perms_id = list(AssetPermission.objects.valid().filter( + user_groups__id=group_id + ).distinct().values_list('id', flat=True)) + granted_keys = Node.objects.filter( - granted_by_permissions__user_groups__id=group_id + granted_by_permissions__id__in=asset_perms_id ).values_list('key', flat=True) asset_granted_keys = Node.objects.filter( - assets__granted_by_permissions__user_groups__id=group_id + assets__granted_by_permissions__id__in=asset_perms_id ).values_list('key', flat=True) if node_key is None: From 7be7c8cee1286b264772d59227a23a3138e566cb Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 22 Nov 2020 16:50:48 +0800 Subject: [PATCH 09/57] =?UTF-8?q?fix(perms):=20=E4=BF=AE=E5=A4=8D=E6=88=91?= =?UTF-8?q?=E7=9A=84=E8=B5=84=E4=BA=A7=E9=A1=B5=E9=9D=A2=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/perms/urls/asset_permission.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/perms/urls/asset_permission.py b/apps/perms/urls/asset_permission.py index de441bbfc..f7e10e0fc 100644 --- a/apps/perms/urls/asset_permission.py +++ b/apps/perms/urls/asset_permission.py @@ -21,7 +21,7 @@ user_permission_urlpatterns = [ # --------------------------------------------------------- # 以 serializer 格式返回 path('/assets/', api.UserAllGrantedAssetsApi.as_view(), name='user-assets'), - path('assets/', api.MyAllAssetsAsTreeApi.as_view(), name='my-assets'), + path('assets/', api.MyAllGrantedAssetsApi.as_view(), name='my-assets'), # Tree Node 的数据格式返回 path('/assets/tree/', api.UserDirectGrantedAssetsAsTreeForAdminApi.as_view(), name='user-assets-as-tree'), From 39ab5978be32d9e2aa33732716d42236bbde1104 Mon Sep 17 00:00:00 2001 From: xinwen Date: Sun, 22 Nov 2020 17:18:46 +0800 Subject: [PATCH 10/57] =?UTF-8?q?perf(perms):=20=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=89=80=E6=9C=89=E6=8E=88=E6=9D=83=E6=97=B6?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E6=88=90=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/perms/utils/asset/user_permission.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/perms/utils/asset/user_permission.py b/apps/perms/utils/asset/user_permission.py index aebc08e71..5b9836400 100644 --- a/apps/perms/utils/asset/user_permission.py +++ b/apps/perms/utils/asset/user_permission.py @@ -465,6 +465,9 @@ def get_user_all_assetpermissions_id(user: User): asset_perms_id = AssetPermission.objects.valid().filter( Q(users=user) | Q(user_groups__users=user) ).distinct().values_list('id', flat=True) + + # !!! 这个很重要,必须转换成 list,避免 Django 生成嵌套子查询 + asset_perms_id = list(asset_perms_id) return asset_perms_id From 439999381d7830c886e1e9509e6845cd6bbf5968 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 22 Nov 2020 17:48:55 +0800 Subject: [PATCH 11/57] =?UTF-8?q?perf(build):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E6=97=B6=E7=94=A8=E7=9A=84mirror?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index b030c25b0..57f378890 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,7 @@ WORKDIR /opt/jumpserver COPY ./requirements ./requirements RUN useradd jumpserver +RUN wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-6.repo RUN yum -y install epel-release && \ echo -e "[mysql]\nname=mysql\nbaseurl=${MYSQL_MIRROR}\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo RUN yum -y install $(cat requirements/rpm_requirements.txt) From 975cc41bce2f8691b2f983459d5e3c4ce59e8ecd Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 22 Nov 2020 18:08:37 +0800 Subject: [PATCH 12/57] =?UTF-8?q?perf(build):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BD=BF=E7=94=A8pip=20mirror?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 57f378890..59f615a19 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,8 @@ RUN cd utils && bash -ixeu build.sh FROM registry.fit2cloud.com/public/python:v3 ARG PIP_MIRROR=https://pypi.douban.com/simple ENV PIP_MIRROR=$PIP_MIRROR +ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple +ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR ARG MYSQL_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/ ENV MYSQL_MIRROR=$MYSQL_MIRROR @@ -24,7 +26,7 @@ RUN yum -y install epel-release && \ RUN yum -y install $(cat requirements/rpm_requirements.txt) RUN pip install --upgrade pip setuptools==49.6.0 wheel -i ${PIP_MIRROR} && \ pip config set global.index-url ${PIP_MIRROR} -RUN pip install $(grep 'jms' requirements/requirements.txt) -i https://pypi.org/simple +RUN pip install $(grep 'jms' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} RUN pip install -r requirements/requirements.txt COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver From 75d7530ea5baca9e1e1189723ba52c0d667a2450 Mon Sep 17 00:00:00 2001 From: xinwen Date: Sat, 21 Nov 2020 22:23:10 +0800 Subject: [PATCH 13/57] =?UTF-8?q?fix(perms):=20=E5=9C=A8=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=89=80=E7=BB=91=E5=AE=9A=E7=9A=84=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E8=A7=84=E5=88=99=E4=B8=AD=EF=BC=8C=E5=A6=82=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E7=BB=99=E7=94=A8=E6=88=B7=E7=BB=84=EF=BC=8C=E5=BD=93?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=BB=84=E5=A2=9E=E5=8A=A0=E6=88=90=E5=91=98?= =?UTF-8?q?=E5=90=8E=EF=BC=8C=E5=8A=A8=E6=80=81=E7=B3=BB=E7=BB=9F=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=B8=8B=E6=B2=A1=E6=9C=89=E7=9B=B8=E5=BA=94=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E7=94=A8=E6=88=B7=EF=BC=8C=E5=9B=A0=E6=AD=A4=E4=B9=9F?= =?UTF-8?q?=E4=B8=8D=E4=BC=9A=E8=87=AA=E5=8A=A8=E6=8E=A8=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/perms/signals_handler.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/apps/perms/signals_handler.py b/apps/perms/signals_handler.py index fe39804f6..decffde54 100644 --- a/apps/perms/signals_handler.py +++ b/apps/perms/signals_handler.py @@ -6,7 +6,7 @@ from django.dispatch import receiver 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 +from assets.models import Asset, SystemUser from common.utils import get_logger from common.exceptions import M2MReverseNotAllowed from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR @@ -208,3 +208,26 @@ def on_node_asset_change(action, instance, reverse, pk_set, **kwargs): node_pk_set = pk_set create_rebuild_user_tree_task_by_related_nodes_or_assets.delay(node_pk_set, asset_pk_set) + + +@receiver(m2m_changed, sender=User.groups.through) +def on_user_groups_change(instance, action, reverse, pk_set, model, **kwargs): + """ + UserGroup 增加 User 时,增加的 User 需要与 UserGroup 关联的动态系统用户相关联 + """ + user: User + + if action != POST_ADD: + return + + if not reverse: + # 一个用户添加了多个用户组 + users_id = [instance.id] + system_users = SystemUser.objects.filter(groups__id__in=pk_set).distinct() + else: + # 一个用户组添加了多个用户 + users_id = pk_set + system_users = SystemUser.objects.filter(groups__id=instance.pk).distinct() + + for system_user in system_users: + system_user.users.add(*users_id) From 3041697edc4311c36b507ad5c7177de297dfc697 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Tue, 24 Nov 2020 19:09:14 +0800 Subject: [PATCH 14/57] =?UTF-8?q?fix(orgs):=20=E5=85=BC=E5=AE=B9=E6=97=A7?= =?UTF-8?q?=E7=9A=84=E7=BB=84=E7=BB=87=E7=94=A8=E6=88=B7=E5=85=B3=E7=B3=BB?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=20(#5088)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: xinwen --- apps/orgs/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/orgs/api.py b/apps/orgs/api.py index ff3a38007..077b7eba7 100644 --- a/apps/orgs/api.py +++ b/apps/orgs/api.py @@ -92,6 +92,7 @@ class OrgMemberAdminRelationBulkViewSet(JMSBulkRelationModelViewSet): serializer_class = OrgMemberAdminSerializer filterset_class = OrgMemberRelationFilterSet search_fields = ('user__name', 'user__username', 'org__name') + lookup_field = 'user_id' def get_queryset(self): queryset = super().get_queryset() @@ -116,6 +117,7 @@ class OrgMemberUserRelationBulkViewSet(JMSBulkRelationModelViewSet): serializer_class = OrgMemberUserSerializer filterset_class = OrgMemberRelationFilterSet search_fields = ('user__name', 'user__username', 'org__name') + lookup_field = 'user_id' def get_queryset(self): queryset = super().get_queryset() From 91081d9423c03172bf42ed7b629be26eae50606c Mon Sep 17 00:00:00 2001 From: xinwen Date: Tue, 24 Nov 2020 19:31:45 +0800 Subject: [PATCH 15/57] =?UTF-8?q?refactor(perms):=20=E5=9C=A8=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E7=94=A8=E6=88=B7=E6=89=80=E7=BB=91=E5=AE=9A=E7=9A=84?= =?UTF-8?q?=E6=8E=88=E6=9D=83=E8=A7=84=E5=88=99=E4=B8=AD=EF=BC=8C=E5=A6=82?= =?UTF-8?q?=E6=8E=88=E6=9D=83=E7=BB=99=E7=94=A8=E6=88=B7=E7=BB=84=EF=BC=8C?= =?UTF-8?q?=E5=BD=93=E7=94=A8=E6=88=B7=E7=BB=84=E5=A2=9E=E5=8A=A0=E6=88=90?= =?UTF-8?q?=E5=91=98=E5=90=8E=EF=BC=8C=E5=8A=A8=E6=80=81=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=B8=8B=E6=B2=A1=E6=9C=89=E7=9B=B8=E5=BA=94?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=94=A8=E6=88=B7=EF=BC=8C=E5=9B=A0=E6=AD=A4?= =?UTF-8?q?=E4=B9=9F=E4=B8=8D=E4=BC=9A=E8=87=AA=E5=8A=A8=E6=8E=A8=E9=80=81?= =?UTF-8?q?=20(#5084)=20(#5086)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/perms/signals_handler.py | 59 +++++++++++++++++++++-------------- apps/users/signals_handler.py | 11 ------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/apps/perms/signals_handler.py b/apps/perms/signals_handler.py index decffde54..5b33fcb35 100644 --- a/apps/perms/signals_handler.py +++ b/apps/perms/signals_handler.py @@ -16,6 +16,42 @@ from .models import AssetPermission, RemoteAppPermission logger = get_logger(__file__) +def handle_rebuild_user_tree(instance, action, reverse, pk_set, **kwargs): + if action.startswith('post'): + if reverse: + create_rebuild_user_tree_task(pk_set) + else: + create_rebuild_user_tree_task([instance.id]) + + +def handle_bind_groups_systemuser(instance, action, reverse, pk_set, **kwargs): + """ + UserGroup 增加 User 时,增加的 User 需要与 UserGroup 关联的动态系统用户相关联 + """ + user: User + + if action != POST_ADD: + return + + if not reverse: + # 一个用户添加了多个用户组 + users_id = [instance.id] + system_users = SystemUser.objects.filter(groups__id__in=pk_set).distinct() + else: + # 一个用户组添加了多个用户 + users_id = pk_set + system_users = SystemUser.objects.filter(groups__id=instance.pk).distinct() + + for system_user in system_users: + system_user.users.add(*users_id) + + +@receiver(m2m_changed, sender=User.groups.through) +def on_user_groups_change(**kwargs): + handle_rebuild_user_tree(**kwargs) + handle_bind_groups_systemuser(**kwargs) + + @receiver([pre_save], sender=AssetPermission) def on_asset_perm_deactive(instance: AssetPermission, **kwargs): try: @@ -208,26 +244,3 @@ def on_node_asset_change(action, instance, reverse, pk_set, **kwargs): node_pk_set = pk_set create_rebuild_user_tree_task_by_related_nodes_or_assets.delay(node_pk_set, asset_pk_set) - - -@receiver(m2m_changed, sender=User.groups.through) -def on_user_groups_change(instance, action, reverse, pk_set, model, **kwargs): - """ - UserGroup 增加 User 时,增加的 User 需要与 UserGroup 关联的动态系统用户相关联 - """ - user: User - - if action != POST_ADD: - return - - if not reverse: - # 一个用户添加了多个用户组 - users_id = [instance.id] - system_users = SystemUser.objects.filter(groups__id__in=pk_set).distinct() - else: - # 一个用户组添加了多个用户 - users_id = pk_set - system_users = SystemUser.objects.filter(groups__id=instance.pk).distinct() - - for system_user in system_users: - system_user.users.add(*users_id) diff --git a/apps/users/signals_handler.py b/apps/users/signals_handler.py index 3fdf6ddc6..a25d4ea20 100644 --- a/apps/users/signals_handler.py +++ b/apps/users/signals_handler.py @@ -2,14 +2,12 @@ # from django.dispatch import receiver -from django.db.models.signals import m2m_changed from django_auth_ldap.backend import populate_user from django.conf import settings from django_cas_ng.signals import cas_user_authenticated from jms_oidc_rp.signals import openid_create_or_update_user -from perms.tasks import create_rebuild_user_tree_task from common.utils import get_logger from .signals import post_user_create from .models import User @@ -27,15 +25,6 @@ def on_user_create(sender, user=None, **kwargs): send_user_created_mail(user) -@receiver(m2m_changed, sender=User.groups.through) -def on_user_groups_change(instance, action, reverse, pk_set, **kwargs): - if action.startswith('post'): - if reverse: - create_rebuild_user_tree_task(pk_set) - else: - create_rebuild_user_tree_task([instance.id]) - - @receiver(cas_user_authenticated) def on_cas_user_authenticated(sender, user, created, **kwargs): if created: From a4667f33127d582ac5d2f128af070358edb974d1 Mon Sep 17 00:00:00 2001 From: xinwen Date: Mon, 23 Nov 2020 17:26:06 +0800 Subject: [PATCH 16/57] =?UTF-8?q?fix(Node):=20Node=20=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E7=9A=84=E6=97=B6=E5=80=99=EF=BC=8C=E5=9C=A8=E4=BF=A1=E5=8F=B7?= =?UTF-8?q?=E9=87=8C=E8=AE=BE=E7=BD=AE=20parent=5Fkey?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/node.py | 4 +--- apps/assets/models/node.py | 2 +- apps/assets/signals_handler.py | 7 ++++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 273ac667f..924dcb788 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -201,10 +201,8 @@ class NodeAddChildrenApi(generics.UpdateAPIView): def put(self, request, *args, **kwargs): instance = self.get_object() nodes_id = request.data.get("nodes") - children = [get_object_or_none(Node, id=pk) for pk in nodes_id] + children = Node.objects.filter(id__in=nodes_id) for node in children: - if not node: - continue node.parent = instance return Response("OK") diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index e88afccdc..da8d07ebc 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -103,7 +103,7 @@ class FamilyMixin: if value is None: value = child_key child = self.__class__.objects.create( - id=_id, key=child_key, value=value, parent_key=self.key, + id=_id, key=child_key, value=value ) return child diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index 1c2813aa9..da7f6d397 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -4,7 +4,7 @@ from operator import add, sub from assets.utils import is_asset_exists_in_node from django.db.models.signals import ( - post_save, m2m_changed, pre_delete, post_delete + post_save, m2m_changed, pre_delete, post_delete, pre_save ) from django.db.models import Q, F from django.dispatch import receiver @@ -37,6 +37,11 @@ def test_asset_conn_on_created(asset): test_asset_connectivity_util.delay([asset]) +@receiver(pre_save, sender=Node) +def on_node_pre_save(sender, instance: Node, **kwargs): + instance.parent_key = instance.compute_parent_key() + + @receiver(post_save, sender=Asset) @on_transaction_commit def on_asset_created_or_update(sender, instance=None, created=False, **kwargs): From f26b7a470a351d6f36eafa3f42b9b85a0120830c Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Fri, 20 Nov 2020 20:23:09 +0800 Subject: [PATCH 17/57] =?UTF-8?q?perf(celery-task):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E8=8A=82=E7=82=B9=E8=B5=84=E4=BA=A7=E6=95=B0?= =?UTF-8?q?=E9=87=8F=E7=9A=84=20Celery=20=E4=BB=BB=E5=8A=A1=20(#5052)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: xinwen --- apps/assets/tasks/nodes_amount.py | 2 +- apps/assets/utils.py | 2 +- jms | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/assets/tasks/nodes_amount.py b/apps/assets/tasks/nodes_amount.py index 0ec0810a0..3ae191788 100644 --- a/apps/assets/tasks/nodes_amount.py +++ b/apps/assets/tasks/nodes_amount.py @@ -8,6 +8,6 @@ logger = get_logger(__file__) @register_as_period_task(crontab='0 2 * * *') -@shared_task(queue='node_assets_amount') +@shared_task(queue='celery_heavy_tasks') def check_node_assets_amount_celery_task(): check_node_assets_amount() diff --git a/apps/assets/utils.py b/apps/assets/utils.py index f04c06d0b..c4c112c16 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils.py @@ -25,7 +25,7 @@ def check_node_assets_amount(): node.assets_amount = assets_amount node.save() # 防止自检程序给数据库的压力太大 - time.sleep(2) + time.sleep(0.1) def is_asset_exists_in_node(asset_pk, node_key): diff --git a/jms b/jms index d3ede99ff..4cc7e916d 100755 --- a/jms +++ b/jms @@ -158,7 +158,7 @@ def parse_service(s): web_services = ['gunicorn', 'flower', 'daphne'] celery_services = [ "celery_ansible", "celery_default", "celery_node_tree", - "celery_check_asset_perm_expired", "celery_node_assets_amount" + "celery_check_asset_perm_expired", "celery_heavy_tasks" ] task_services = celery_services + ['beat'] all_services = web_services + task_services @@ -228,9 +228,9 @@ def get_start_celery_node_tree_kwargs(): return get_start_worker_kwargs('node_tree', 2) -def get_start_celery_node_assets_amount_kwargs(): - print("\n- Start Celery as Distributed Task Queue: NodeAssetsAmount") - return get_start_worker_kwargs('celery_node_assets_amount', 1) +def get_start_celery_heavy_tasks_kwargs(): + print("\n- Start Celery as Distributed Task Queue: HeavyTasks") + return get_start_worker_kwargs('celery_heavy_tasks', 1) def get_start_celery_check_asset_perm_expired_kwargs(): @@ -374,7 +374,7 @@ def start_service(s): "celery_ansible": get_start_celery_ansible_kwargs, "celery_default": get_start_celery_default_kwargs, "celery_node_tree": get_start_celery_node_tree_kwargs, - "celery_node_assets_amount": get_start_celery_node_assets_amount_kwargs, + "celery_heavy_tasks": get_start_celery_heavy_tasks_kwargs, "celery_check_asset_perm_expired": get_start_celery_check_asset_perm_expired_kwargs, "beat": get_start_beat_kwargs, "flower": get_start_flower_kwargs, From df2f1b3e6e14344649d790816c13279ce9801fe9 Mon Sep 17 00:00:00 2001 From: xinwen Date: Thu, 26 Nov 2020 10:34:27 +0800 Subject: [PATCH 18/57] =?UTF-8?q?perf(User):=20=E7=94=A8=E6=88=B7=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=9C=A8=E5=A4=A7=E8=A7=84=E6=A8=A1=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=83=85=E5=86=B5=E4=B8=8B=E6=85=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/users/api/user.py | 17 +++++++++++++---- apps/users/models/user.py | 18 +++++++----------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/apps/users/api/user.py b/apps/users/api/user.py index 65bd8f4d1..b0b209e19 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -6,8 +6,8 @@ from rest_framework.decorators import action from rest_framework import generics from rest_framework.response import Response from rest_framework_bulk import BulkModelViewSet +from django.db.models import Prefetch -from common.db.aggregates import GroupConcat from common.permissions import ( IsOrgAdmin, IsOrgAdminOrAppUser, CanUpdateDeleteUser, IsSuperUser @@ -44,9 +44,18 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet): extra_filter_backends = [OrgRoleUserFilterBackend] def get_queryset(self): - return super().get_queryset().annotate( - gc_m2m_org_members__role=GroupConcat('m2m_org_members__role'), - ).prefetch_related('groups') + queryset = super().get_queryset().prefetch_related( + 'groups' + ) + if current_org.is_real(): + # 为在列表中计算用户在真实组织里的角色 + queryset = queryset.prefetch_related( + Prefetch( + 'm2m_org_members', + queryset=OrganizationMember.objects.filter(org__id=current_org.id) + ) + ) + return queryset def send_created_signal(self, users): if not isinstance(users, list): diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 81f0f593b..56c979738 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -170,22 +170,18 @@ class RoleMixin: from orgs.models import ROLE as ORG_ROLE if not current_org.is_real(): + # 不是真实的组织,取 User 本身的角色 if self.is_superuser: return [ORG_ROLE.ADMIN] else: return [ORG_ROLE.USER] - if hasattr(self, 'gc_m2m_org_members__role'): - names = self.gc_m2m_org_members__role - if isinstance(names, str): - roles = set(self.gc_m2m_org_members__role.split(',')) - else: - roles = set() - else: - roles = set(self.m2m_org_members.filter( - org_id=current_org.id - ).values_list('role', flat=True)) - roles = list(roles) + # 是真实组织,取 OrganizationMember 中的角色 + roles = [ + org_member.role + for org_member in self.m2m_org_members.all() + if org_member.org_id == current_org.id + ] roles.sort() return roles From 610aaf52444048caf849b6be83bcd96b5cd88ae7 Mon Sep 17 00:00:00 2001 From: xinwen Date: Wed, 25 Nov 2020 13:28:01 +0800 Subject: [PATCH 19/57] =?UTF-8?q?fix(assets):=20=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E7=94=A8=E6=88=B7=E5=92=8C=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=85=B3=E7=B3=BB=E5=8F=98=E5=8C=96=E6=97=B6=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E6=8E=A8=E9=80=81=E5=88=B0=E8=B5=84=E4=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/signals_handler.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index da7f6d397..f89c8da94 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -96,22 +96,24 @@ def on_system_user_assets_change(instance, action, model, pk_set, **kwargs): @receiver(m2m_changed, sender=SystemUser.users.through) -def on_system_user_users_change(sender, instance=None, action='', model=None, pk_set=None, **kwargs): +def on_system_user_users_change(sender, instance: SystemUser, action, model, pk_set, reverse, **kwargs): """ 当系统用户和用户关系发生变化时,应该重新推送系统用户资产中 """ if action != POST_ADD: return + + if reverse: + raise M2MReverseNotAllowed + if not instance.username_same_with_user: return + logger.debug("System user users change signal recv: {}".format(instance)) - queryset = model.objects.filter(pk__in=pk_set) - if model == SystemUser: - system_users = queryset - else: - system_users = [instance] - for s in system_users: - push_system_user_to_assets_manual.delay(s) + usernames = model.objects.filter(pk__in=pk_set).values_list('username', flat=True) + + for username in usernames: + push_system_user_to_assets_manual.delay(instance, username) @receiver(m2m_changed, sender=SystemUser.nodes.through) From 6d427b9834472f4831b317b8b0572477aa5be142 Mon Sep 17 00:00:00 2001 From: Bai Date: Fri, 27 Nov 2020 17:29:00 +0800 Subject: [PATCH 20/57] =?UTF-8?q?fix:=20=E7=A6=81=E6=AD=A2=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E7=BB=84=E7=BB=87=E6=A0=B9=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/node.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 924dcb788..096c65939 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -61,6 +61,9 @@ class NodeViewSet(OrgModelViewSet): def destroy(self, request, *args, **kwargs): node = self.get_object() + if node.is_org_root(): + error = _("You can't delete the root node ({})".format(node.value)) + return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN) if node.has_children_or_has_assets(): error = _("Deletion failed and the node contains children or assets") return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN) From c3b09dd80092676ea5b1c0116858dd48fb892195 Mon Sep 17 00:00:00 2001 From: Bai Date: Thu, 26 Nov 2020 19:18:06 +0800 Subject: [PATCH 21/57] =?UTF-8?q?perf(perms):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=8E=88=E6=9D=83=E6=A0=91=E8=BF=94=E5=9B=9E?= =?UTF-8?q?org=5Fname=E5=AD=97=E6=AE=B5=EF=BC=9B=E6=B7=BB=E5=8A=A0thread?= =?UTF-8?q?=5Flocal=E5=B1=9E=E6=80=A7org=5Fmapper=E5=87=8F=E5=B0=91?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E6=AC=A1=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/mixin.py | 1 + apps/orgs/mixins/models.py | 4 ++-- apps/orgs/utils.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/apps/assets/api/mixin.py b/apps/assets/api/mixin.py index 4374f399a..54eb91b41 100644 --- a/apps/assets/api/mixin.py +++ b/apps/assets/api/mixin.py @@ -69,6 +69,7 @@ class SerializeToTreeNodeMixin: 'ip': asset.ip, 'protocols': asset.protocols_as_list, 'platform': asset.platform_base, + 'org_name': asset.org_name }, } } diff --git a/apps/orgs/mixins/models.py b/apps/orgs/mixins/models.py index c6c18902d..af3a8a30f 100644 --- a/apps/orgs/mixins/models.py +++ b/apps/orgs/mixins/models.py @@ -8,7 +8,7 @@ from django.core.exceptions import ValidationError from common.utils import get_logger from ..utils import ( set_current_org, get_current_org, current_org, - filter_org_queryset + filter_org_queryset, get_org_name_by_id ) from ..models import Organization @@ -76,7 +76,7 @@ class OrgModelMixin(models.Model): @property def org_name(self): - return self.org.name + return get_org_name_by_id(self.org_id) @property def fullname(self, attr=None): diff --git a/apps/orgs/utils.py b/apps/orgs/utils.py index 7a4576e23..d86e707c5 100644 --- a/apps/orgs/utils.py +++ b/apps/orgs/utils.py @@ -65,6 +65,38 @@ def get_current_org_id(): return org_id +def construct_org_mapper(): + orgs = Organization.objects.all() + org_mapper = {str(org.id): org for org in orgs} + default_org = Organization.default() + org_mapper.update({ + '': default_org, + Organization.DEFAULT_ID: default_org + }) + return org_mapper + + +def set_org_mapper(org_mapper): + setattr(thread_local, 'org_mapper', org_mapper) + + +def get_org_mapper(): + org_mapper = _find('org_mapper') + if org_mapper is None: + org_mapper = construct_org_mapper() + set_org_mapper(org_mapper) + return org_mapper + + +def get_org_name_by_id(org_id): + org_id = str(org_id) + org_mapper = get_org_mapper() + org = org_mapper.get(org_id) + if not org: + org = Organization.objects.filter(id=org_id).first() + return org.name + + def get_current_org_id_for_serializer(): org_id = get_current_org_id() if org_id == Organization.DEFAULT_ID: From bbd6cae3d7c0ea40064ea29fe82f000c5f7db9f6 Mon Sep 17 00:00:00 2001 From: Bai Date: Fri, 27 Nov 2020 16:58:57 +0800 Subject: [PATCH 22/57] =?UTF-8?q?perf(org):=20=E4=BC=98=E5=8C=96=E8=8E=B7?= =?UTF-8?q?=E5=8F=96org=5Fname=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/orgs/utils.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/orgs/utils.py b/apps/orgs/utils.py index d86e707c5..356fac9bd 100644 --- a/apps/orgs/utils.py +++ b/apps/orgs/utils.py @@ -71,7 +71,9 @@ def construct_org_mapper(): default_org = Organization.default() org_mapper.update({ '': default_org, - Organization.DEFAULT_ID: default_org + Organization.DEFAULT_ID: default_org, + Organization.ROOT_ID: Organization.root(), + Organization.SYSTEM_ID: Organization.system() }) return org_mapper @@ -92,9 +94,11 @@ def get_org_name_by_id(org_id): org_id = str(org_id) org_mapper = get_org_mapper() org = org_mapper.get(org_id) - if not org: - org = Organization.objects.filter(id=org_id).first() - return org.name + if org: + org_name = org.name + else: + org_name = 'Not Found' + return org_name def get_current_org_id_for_serializer(): From bb807e62512818774cb1f2f307fd588ee9782cf0 Mon Sep 17 00:00:00 2001 From: xinwen Date: Thu, 26 Nov 2020 19:53:15 +0800 Subject: [PATCH 23/57] =?UTF-8?q?fix(perms):=20=E6=96=B0=E5=BB=BA=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E6=97=B6=E5=8A=A8=E6=80=81=E7=94=A8=E6=88=B7=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E6=8E=A8=E9=80=81=E4=B8=8D=E6=88=90=E5=8A=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/signals_handler.py | 3 +++ apps/assets/tasks/push_system_user.py | 27 +++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index f89c8da94..061d7d84c 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -78,6 +78,7 @@ def on_system_user_update(instance: SystemUser, created, **kwargs): @receiver(m2m_changed, sender=SystemUser.assets.through) +@on_transaction_commit def on_system_user_assets_change(instance, action, model, pk_set, **kwargs): """ 当系统用户和资产关系发生变化时,应该重新推送系统用户到新添加的资产中 @@ -96,6 +97,7 @@ def on_system_user_assets_change(instance, action, model, pk_set, **kwargs): @receiver(m2m_changed, sender=SystemUser.users.through) +@on_transaction_commit def on_system_user_users_change(sender, instance: SystemUser, action, model, pk_set, reverse, **kwargs): """ 当系统用户和用户关系发生变化时,应该重新推送系统用户资产中 @@ -117,6 +119,7 @@ def on_system_user_users_change(sender, instance: SystemUser, action, model, pk_ @receiver(m2m_changed, sender=SystemUser.nodes.through) +@on_transaction_commit def on_system_user_nodes_change(sender, instance=None, action=None, model=None, pk_set=None, **kwargs): """ 当系统用户和节点关系发生变化时,应该将节点下资产关联到新的系统用户上 diff --git a/apps/assets/tasks/push_system_user.py b/apps/assets/tasks/push_system_user.py index 0bb9be407..5b61c656e 100644 --- a/apps/assets/tasks/push_system_user.py +++ b/apps/assets/tasks/push_system_user.py @@ -2,13 +2,13 @@ from itertools import groupby from celery import shared_task -from common.db.utils import get_object_if_need, get_objects_if_need, get_objects +from common.db.utils import get_object_if_need, get_objects from django.utils.translation import ugettext as _ from django.db.models import Empty from common.utils import encrypt_password, get_logger from assets.models import SystemUser, Asset -from orgs.utils import org_aware_func +from orgs.utils import org_aware_func, tmp_to_root_org from . import const from .utils import clean_ansible_task_hosts, group_asset_by_platform @@ -229,7 +229,11 @@ def push_system_user_util(system_user, assets, task_name, username=None): @shared_task(queue="ansible") +@tmp_to_root_org() def push_system_user_to_assets_manual(system_user, username=None): + """ + 将系统用户推送到与它关联的所有资产上 + """ system_user = get_object_if_need(SystemUser, system_user) assets = system_user.get_related_assets() task_name = _("Push system users to assets: {}").format(system_user.name) @@ -237,7 +241,11 @@ def push_system_user_to_assets_manual(system_user, username=None): @shared_task(queue="ansible") +@tmp_to_root_org() def push_system_user_a_asset_manual(system_user, asset, username=None): + """ + 将系统用户推送到一个资产上 + """ if username is None: username = system_user.username task_name = _("Push system users to asset: {}({}) => {}").format( @@ -247,10 +255,25 @@ def push_system_user_a_asset_manual(system_user, asset, username=None): @shared_task(queue="ansible") +@tmp_to_root_org() def push_system_user_to_assets(system_user_id, assets_id, username=None): + """ + 推送系统用户到指定的若干资产上 + """ system_user = SystemUser.objects.get(id=system_user_id) assets = get_objects(Asset, assets_id) task_name = _("Push system users to assets: {}").format(system_user.name) + + if username is None and system_user.username_same_with_user: + # 动态系统用户,把与系统用户关联的所有用户推送到新关联的资产上 + usernames = system_user.users.all().values_list('username', flat=True).distinct() + ret = [] + for username in usernames: + ret.append( + push_system_user_util(system_user, assets, task_name, username=username) + ) + return ret + return push_system_user_util(system_user, assets, task_name, username=username) # @shared_task From e656ba70ec1383a64a1c177eecedccfdc38f2c71 Mon Sep 17 00:00:00 2001 From: xinwen Date: Tue, 1 Dec 2020 18:30:25 +0800 Subject: [PATCH 24/57] =?UTF-8?q?fix(assets):=20=E6=8E=A8=E9=80=81?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E7=B3=BB=E7=BB=9F=E7=94=A8=E6=88=B7=E6=9C=AA?= =?UTF-8?q?=E6=8C=87=E5=AE=9A=20username=20=E5=8F=96=E5=85=A8=E9=83=A8=20u?= =?UTF-8?q?sernames?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/tasks/push_system_user.py | 77 ++++++++++++++++----------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/apps/assets/tasks/push_system_user.py b/apps/assets/tasks/push_system_user.py index 5b61c656e..7506d1fe2 100644 --- a/apps/assets/tasks/push_system_user.py +++ b/apps/assets/tasks/push_system_user.py @@ -7,7 +7,7 @@ from django.utils.translation import ugettext as _ from django.db.models import Empty from common.utils import encrypt_password, get_logger -from assets.models import SystemUser, Asset +from assets.models import SystemUser, Asset, AuthBook from orgs.utils import org_aware_func, tmp_to_root_org from . import const from .utils import clean_ansible_task_hosts, group_asset_by_platform @@ -190,15 +190,12 @@ def get_push_system_user_tasks(system_user, platform="unixlike", username=None): @org_aware_func("system_user") def push_system_user_util(system_user, assets, task_name, username=None): from ops.utils import update_or_create_ansible_task - hosts = clean_ansible_task_hosts(assets, system_user=system_user) - if not hosts: + assets = clean_ansible_task_hosts(assets, system_user=system_user) + if not assets: return {} - platform_hosts_map = {} - hosts_sorted = sorted(hosts, key=group_asset_by_platform) - platform_hosts = groupby(hosts_sorted, key=group_asset_by_platform) - for i in platform_hosts: - platform_hosts_map[i[0]] = list(i[1]) + assets_sorted = sorted(assets, key=group_asset_by_platform) + platform_hosts = groupby(assets_sorted, key=group_asset_by_platform) def run_task(_tasks, _hosts): if not _tasks: @@ -209,23 +206,51 @@ def push_system_user_util(system_user, assets, task_name, username=None): ) task.run() - for platform, _hosts in platform_hosts_map.items(): - if not _hosts: + if system_user.username_same_with_user: + if username is None: + # 动态系统用户,但是没有指定 username + usernames = list(system_user.users.all().values_list('username', flat=True).distinct()) + else: + usernames = [username] + else: + # 非动态系统用户指定 username 无效 + assert username is None, 'Only Dynamic user can assign `username`' + usernames = [system_user.username] + + for platform, _assets in platform_hosts: + _assets = list(_assets) + if not _assets: continue print(_("Start push system user for platform: [{}]").format(platform)) - print(_("Hosts count: {}").format(len(_hosts))) + print(_("Hosts count: {}").format(len(_assets))) - # 如果没有特殊密码设置,就不需要单独推送某台机器了 - if not system_user.has_special_auth(username=username): - logger.debug("System user not has special auth") - tasks = get_push_system_user_tasks(system_user, platform, username=username) - run_task(tasks, _hosts) - continue + id_asset_map = {_asset.id: _asset for _asset in _assets} + assets_id = id_asset_map.keys() + no_special_auth = [] + special_auth_set = set() - for _host in _hosts: - system_user.load_asset_special_auth(_host, username=username) - tasks = get_push_system_user_tasks(system_user, platform, username=username) - run_task(tasks, [_host]) + auth_books = AuthBook.objects.filter(username__in=usernames, asset_id__in=assets_id) + + for auth_book in auth_books: + special_auth_set.add((auth_book.username, auth_book.asset_id)) + + for _username in usernames: + no_special_assets = [] + for asset_id in assets_id: + if (_username, asset_id) not in special_auth_set: + no_special_assets.append(id_asset_map[asset_id]) + if no_special_assets: + no_special_auth.append((_username, no_special_assets)) + + for _username, no_special_assets in no_special_auth: + tasks = get_push_system_user_tasks(system_user, platform, username=_username) + run_task(tasks, no_special_assets) + + for auth_book in auth_books: + system_user._merge_auth(auth_book) + tasks = get_push_system_user_tasks(system_user, platform, username=auth_book.username) + asset = id_asset_map[auth_book.asset_id] + run_task(tasks, [asset]) @shared_task(queue="ansible") @@ -264,16 +289,6 @@ def push_system_user_to_assets(system_user_id, assets_id, username=None): assets = get_objects(Asset, assets_id) task_name = _("Push system users to assets: {}").format(system_user.name) - if username is None and system_user.username_same_with_user: - # 动态系统用户,把与系统用户关联的所有用户推送到新关联的资产上 - usernames = system_user.users.all().values_list('username', flat=True).distinct() - ret = [] - for username in usernames: - ret.append( - push_system_user_util(system_user, assets, task_name, username=username) - ) - return ret - return push_system_user_util(system_user, assets, task_name, username=username) # @shared_task From c2d592827338a71ad030078ad83a2d7fe3c32953 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Wed, 2 Dec 2020 11:09:39 +0800 Subject: [PATCH 25/57] =?UTF-8?q?build(pip):=20=E9=94=81=E5=AE=9Apip?= =?UTF-8?q?=E7=89=88=E6=9C=AC=20(#5152)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * build(pip): 锁定pip版本 * fix: 锁定pip版本 * fix(req): 锁定加密库版本 * fix(build): 引用pip缓存 Co-authored-by: ibuler --- Dockerfile | 6 +++--- requirements/requirements.txt | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 59f615a19..4ce7f521a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,10 +24,10 @@ RUN wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Cen RUN yum -y install epel-release && \ echo -e "[mysql]\nname=mysql\nbaseurl=${MYSQL_MIRROR}\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo RUN yum -y install $(cat requirements/rpm_requirements.txt) -RUN pip install --upgrade pip setuptools==49.6.0 wheel -i ${PIP_MIRROR} && \ +RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel -i ${PIP_MIRROR} && \ pip config set global.index-url ${PIP_MIRROR} -RUN pip install $(grep 'jms' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} -RUN pip install -r requirements/requirements.txt +RUN pip install --no-cache-dir $(grep 'jms' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} +RUN pip install --no-cache-dir -r requirements/requirements.txt COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config diff --git a/requirements/requirements.txt b/requirements/requirements.txt index dc7ca9b85..29872d16a 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -53,6 +53,8 @@ Pillow==7.1.0 pyasn1==0.4.8 pycparser==2.19 pycrypto==2.6.1 +pycryptodome==3.9.9 +pycryptodomex==3.9.9 pyotp==2.2.6 PyNaCl==1.2.1 python-dateutil==2.6.1 From 86fcd3c2512a2d81081e6e03729bed381cb84b32 Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 1 Dec 2020 14:08:18 +0800 Subject: [PATCH 26/57] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E6=96=87=E4=BB=B6=EF=BC=88=E5=A6=82=E6=9E=9C=E9=9C=80?= =?UTF-8?q?=E8=A6=81=EF=BC=8C=E5=B0=86Default=E8=8A=82=E7=82=B9=E7=9A=84ke?= =?UTF-8?q?y=E4=BB=8E0=E4=BF=AE=E6=94=B9=E4=B8=BA1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0063_migrate_default_node_key.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 apps/assets/migrations/0063_migrate_default_node_key.py diff --git a/apps/assets/migrations/0063_migrate_default_node_key.py b/apps/assets/migrations/0063_migrate_default_node_key.py new file mode 100644 index 000000000..609bc9590 --- /dev/null +++ b/apps/assets/migrations/0063_migrate_default_node_key.py @@ -0,0 +1,72 @@ +# Generated by Jiangjie.Bai on 2020-12-01 10:47 + +from django.db import migrations +from django.db.models import Q + +default_node_value = 'Default' # Always +old_default_node_key = '0' # Version <= 1.4.3 +new_default_node_key = '1' # Version >= 1.4.4 + + +def compute_parent_key(key): + try: + return key[:key.rindex(':')] + except ValueError: + return '' + + +def migrate_default_node_key(apps, schema_editor): + """ 将已经存在的Default节点的key从0修改为1 """ + # 1.4.3版本中Default节点的key为0 + print('') + Node = apps.get_model('assets', 'Node') + Asset = apps.get_model('assets', 'Asset') + + # key为0的节点 + old_default_node = Node.objects.filter(key=old_default_node_key, value=default_node_value).first() + if not old_default_node: + print(f'Check old default node `key={old_default_node_key} value={default_node_value}` not exists') + return + print(f'Check old default node `key={old_default_node_key} value={default_node_value}` exists') + # key为1的节点 + new_default_node = Node.objects.filter(key=new_default_node_key, value=default_node_value).first() + if new_default_node: + print(f'Check new default node `key={new_default_node_key} value={default_node_value}` exists') + all_assets = Asset.objects.filter( + Q(nodes__key__startswith=f'{new_default_node_key}:') | Q(nodes__key=new_default_node_key) + ).distinct() + if all_assets: + print(f'Check new default node has assets (count: {len(all_assets)})') + return + all_children = Node.objects.filter(Q(key__startswith=f'{new_default_node_key}:')) + if all_children: + print(f'Check new default node has children nodes (count: {len(all_children)})') + return + print(f'Check new default node not has assets and children nodes, delete it.') + new_default_node.delete() + # 执行修改 + print(f'Modify old default node key from `{old_default_node_key}` to `{new_default_node_key}`') + nodes = Node.objects.filter( + Q(key__istartswith=f'{old_default_node_key}:') | Q(key=old_default_node_key) + ) + for node in nodes: + old_key = node.key + key_list = old_key.split(':', maxsplit=1) + key_list[0] = new_default_node_key + new_key = ':'.join(key_list) + node.key = new_key + node.parent_key = compute_parent_key(node.key) + # 批量更新 + Node.objects.bulk_update(nodes, ['key', 'parent_key']) + print('Bulk update key and parent_key') + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0062_auto_20201117_1938'), + ] + + operations = [ + migrations.RunPython(migrate_default_node_key) + ] From af40e46a755473b6982cbfaca762afc92756457c Mon Sep 17 00:00:00 2001 From: Bai Date: Thu, 3 Dec 2020 10:20:11 +0800 Subject: [PATCH 27/57] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E8=BF=81?= =?UTF-8?q?=E7=A7=BBDefault=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/migrations/0063_migrate_default_node_key.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/assets/migrations/0063_migrate_default_node_key.py b/apps/assets/migrations/0063_migrate_default_node_key.py index 609bc9590..fc294264f 100644 --- a/apps/assets/migrations/0063_migrate_default_node_key.py +++ b/apps/assets/migrations/0063_migrate_default_node_key.py @@ -38,14 +38,14 @@ def migrate_default_node_key(apps, schema_editor): if all_assets: print(f'Check new default node has assets (count: {len(all_assets)})') return - all_children = Node.objects.filter(Q(key__startswith=f'{new_default_node_key}:')) + all_children = Node.objects.filter(key__startswith=f'{new_default_node_key}:') if all_children: print(f'Check new default node has children nodes (count: {len(all_children)})') return print(f'Check new default node not has assets and children nodes, delete it.') new_default_node.delete() # 执行修改 - print(f'Modify old default node key from `{old_default_node_key}` to `{new_default_node_key}`') + print(f'Modify old default node `key` from `{old_default_node_key}` to `{new_default_node_key}`') nodes = Node.objects.filter( Q(key__istartswith=f'{old_default_node_key}:') | Q(key=old_default_node_key) ) @@ -57,8 +57,8 @@ def migrate_default_node_key(apps, schema_editor): node.key = new_key node.parent_key = compute_parent_key(node.key) # 批量更新 + print(f'Bulk update nodes `key` and `parent_key`, (count: {len(nodes)})') Node.objects.bulk_update(nodes, ['key', 'parent_key']) - print('Bulk update key and parent_key') class Migration(migrations.Migration): From 89ec6ba6ef64df8476b0d3eea417f9904493f7aa Mon Sep 17 00:00:00 2001 From: Bai Date: Thu, 3 Dec 2020 10:41:52 +0800 Subject: [PATCH 28/57] =?UTF-8?q?fix:=20Node=20ordering=20[`parent=5Fkey`,?= =?UTF-8?q?=20`value`];=20=E4=BF=AE=E5=A4=8D=E9=BB=98=E8=AE=A4=E7=BB=84?= =?UTF-8?q?=E7=BB=87Default=E8=8A=82=E7=82=B9=E6=98=BE=E7=A4=BA=E9=97=AE?= =?UTF-8?q?=E9=A2=98(=E5=AD=98=E5=9C=A8key=E4=B8=BA0=E7=9A=84Default?= =?UTF-8?q?=E8=8A=82=E7=82=B9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/models/node.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index da8d07ebc..f8d5f9691 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -354,7 +354,8 @@ class SomeNodesMixin: def org_root(cls): root = cls.objects.filter(parent_key='')\ .filter(key__regex=r'^[0-9]+$')\ - .exclude(key__startswith='-') + .exclude(key__startswith='-')\ + .order_by('key') if root: return root[0] else: @@ -411,7 +412,7 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin): class Meta: verbose_name = _("Node") - ordering = ['value'] + ordering = ['parent_key', 'value'] def __str__(self): return self.full_value From 3354ab8ce9734124f9bac8a02aa439326ee94803 Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 2 Dec 2020 12:28:40 +0800 Subject: [PATCH 29/57] fix(req): fix wheel version --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4ce7f521a..bb83ae686 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ RUN wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Cen RUN yum -y install epel-release && \ echo -e "[mysql]\nname=mysql\nbaseurl=${MYSQL_MIRROR}\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo RUN yum -y install $(cat requirements/rpm_requirements.txt) -RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel -i ${PIP_MIRROR} && \ +RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} && \ pip config set global.index-url ${PIP_MIRROR} RUN pip install --no-cache-dir $(grep 'jms' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} RUN pip install --no-cache-dir -r requirements/requirements.txt From 36e9d8101a47f25fa9b05437ff3b27600625017e Mon Sep 17 00:00:00 2001 From: Bai Date: Thu, 3 Dec 2020 11:05:38 +0800 Subject: [PATCH 30/57] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E6=96=87=E4=BB=B6:=20Node=20ordering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0064_auto_20201203_1100.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 apps/assets/migrations/0064_auto_20201203_1100.py diff --git a/apps/assets/migrations/0064_auto_20201203_1100.py b/apps/assets/migrations/0064_auto_20201203_1100.py new file mode 100644 index 000000000..a8119a883 --- /dev/null +++ b/apps/assets/migrations/0064_auto_20201203_1100.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1 on 2020-12-03 03:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0063_migrate_default_node_key'), + ] + + operations = [ + migrations.AlterModelOptions( + name='node', + options={'ordering': ['parent_key', 'value'], 'verbose_name': 'Node'}, + ), + ] From 6385cb3f86e68915b7750cb6d42ffaee72ecbacf Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 3 Dec 2020 12:24:46 +0800 Subject: [PATCH 31/57] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=EF=BC=8C=E4=B8=8D=E5=86=8D=E8=87=AA=E5=8A=A8=E8=BF=90?= =?UTF-8?q?=E8=A1=8Cmigrations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jms | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/jms b/jms index 4cc7e916d..e92c1c236 100755 --- a/jms +++ b/jms @@ -85,6 +85,17 @@ def check_database_connection(): sys.exit(10) +def check_migrations(): + apps_dir = os.path.join(BASE_DIR, 'apps') + code = subprocess.call("python manage.py showmigrations | grep '\[.\]' | grep -v '\[X\]'", shell=True, cwd=apps_dir) + + if code == 1: + return + for i in range(3): + print("!!! Warning: Has SQL migrations not perform, 有 SQL 变更没有执行") + print("You should run ./PROC upgrade first, 请先运行 ./PROC upgrade, 进行表结构变更") + + def make_migrations(): logging.info("Check database structure change ...") os.chdir(os.path.join(BASE_DIR, 'apps')) @@ -102,8 +113,7 @@ def collect_static(): def prepare(): check_database_connection() - make_migrations() - collect_static() + check_migrations() def check_pid(pid): @@ -512,6 +522,11 @@ def show_service_status(s): print("{} is stopped".format(ns)) +def upgrade(): + collect_static() + make_migrations() + + if __name__ == '__main__': parser = argparse.ArgumentParser( description=""" @@ -524,7 +539,7 @@ if __name__ == '__main__': ) parser.add_argument( 'action', type=str, - choices=("start", "stop", "restart", "status"), + choices=("start", "stop", "restart", "status", "upgrade"), help="Action to run" ) parser.add_argument( @@ -559,5 +574,7 @@ if __name__ == '__main__': stop_service(srv) time.sleep(5) start_services_and_watch(srv) + elif action == "upgrade": + upgrade() else: show_service_status(srv) From 96cd307d1fb7bf8f2ab00c7d495cd4d3cf134f7b Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 3 Dec 2020 14:01:26 +0800 Subject: [PATCH 32/57] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96entrypoint.sh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- entrypoint.sh | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index f509203d4..fa798f642 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -7,15 +7,13 @@ function cleanup() fi } -service="all" -if [[ "$1" != "" ]];then - service=$1 -fi +action="${1-start}" +service="${2-all}" trap cleanup EXIT -if [[ "$1" == "bash" ]];then +if [[ "$action" == "bash" || "$action" == "sh" ]];then bash else - python jms start ${service} + python jms "${action}" "${service}" fi From c8d54b28e2ff24dc8a27c2a41b586a6db9c178ef Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 3 Dec 2020 14:03:42 +0800 Subject: [PATCH 33/57] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jms | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jms b/jms index e92c1c236..56f5ede59 100755 --- a/jms +++ b/jms @@ -96,7 +96,7 @@ def check_migrations(): print("You should run ./PROC upgrade first, 请先运行 ./PROC upgrade, 进行表结构变更") -def make_migrations(): +def perform_db_migrate(): logging.info("Check database structure change ...") os.chdir(os.path.join(BASE_DIR, 'apps')) logging.info("Migrate model change to database ...") @@ -524,7 +524,7 @@ def show_service_status(s): def upgrade(): collect_static() - make_migrations() + perform_db_migrate() if __name__ == '__main__': From 662c9092dc10b4871e0c293fb67b86dad4775983 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Thu, 3 Dec 2020 19:14:28 +0800 Subject: [PATCH 34/57] =?UTF-8?q?reactor(dockerfile):=20=E4=BD=BF=E7=94=A8?= =?UTF-8?q?debian=E6=9E=84=E5=BB=BAdocker=20(#5169)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ibuler --- Dockerfile | 34 +++++++++++++----------- apps/common/utils/common.py | 2 +- requirements/deb_buster_requirements.txt | 33 +++++++++++++++++++++++ requirements/requirements.txt | 2 +- requirements/rpm_requirements.txt | 2 +- 5 files changed, 54 insertions(+), 19 deletions(-) create mode 100644 requirements/deb_buster_requirements.txt diff --git a/Dockerfile b/Dockerfile index bb83ae686..f88dbe4b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ -FROM registry.fit2cloud.com/public/python:v3 as stage-build -MAINTAINER Jumpserver Team +# 编译代码 +FROM python:3.8.6-slim as stage-build +MAINTAINER JumpServer Team ARG VERSION ENV VERSION=$VERSION @@ -8,29 +9,30 @@ ADD . . RUN cd utils && bash -ixeu build.sh -FROM registry.fit2cloud.com/public/python:v3 +# 构建运行时环境 +FROM python:3.8.6-slim ARG PIP_MIRROR=https://pypi.douban.com/simple ENV PIP_MIRROR=$PIP_MIRROR ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR -ARG MYSQL_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/ -ENV MYSQL_MIRROR=$MYSQL_MIRROR WORKDIR /opt/jumpserver -COPY ./requirements ./requirements -RUN useradd jumpserver -RUN wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-6.repo -RUN yum -y install epel-release && \ - echo -e "[mysql]\nname=mysql\nbaseurl=${MYSQL_MIRROR}\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo -RUN yum -y install $(cat requirements/rpm_requirements.txt) -RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} && \ - pip config set global.index-url ${PIP_MIRROR} -RUN pip install --no-cache-dir $(grep 'jms' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} -RUN pip install --no-cache-dir -r requirements/requirements.txt +COPY ./requirements/deb_buster_requirements.txt ./requirements/deb_buster_requirements.txt +RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \ + && sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \ + && apt update +RUN grep -v '^#' ./requirements/deb_buster_requirements.txt | xargs apt -y install + +COPY ./requirements/requirements.txt ./requirements/requirements.txt +RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \ + && pip config set global.index-url ${PIP_MIRROR} \ + && pip install --no-cache-dir $(grep 'jms' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \ + && pip install --no-cache-dir -r requirements/requirements.txt COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver -RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config +RUN mkdir -p /root/.ssh/ \ + && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config RUN echo > config.yml VOLUME /opt/jumpserver/data diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index 382cd2412..25b2e771b 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -41,7 +41,7 @@ def timesince(dt, since='', default="just now"): 3 days, 5 hours. """ - if since is '': + if not since: since = datetime.datetime.utcnow() if since is None: diff --git a/requirements/deb_buster_requirements.txt b/requirements/deb_buster_requirements.txt new file mode 100644 index 000000000..5d873b060 --- /dev/null +++ b/requirements/deb_buster_requirements.txt @@ -0,0 +1,33 @@ +# common +gcc +cmake + +# mysql-client +default-libmysqlclient-dev + +# Pillow +# libffi-dev +# libfreetype6-dev +# libfribidi-dev +# libharfbuzz-dev +# libjpeg-turbo-progs +# libjpeg62-turbo-dev +# liblcms2-dev +# libopenjp2-7-dev +# libtiff5-dev +# libwebp-dev +# python3-tk +# zlib1g-dev + + +# ldap +openssl +libssl-dev +libldap2-dev +libsasl2-dev +libkrb5-dev +sqlite + +# ansible +sshpass + diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 29872d16a..391481090 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -58,7 +58,7 @@ pycryptodomex==3.9.9 pyotp==2.2.6 PyNaCl==1.2.1 python-dateutil==2.6.1 -python-gssapi==0.6.4 +#python-gssapi==0.6.4 pytz==2018.3 PyYAML==5.1 redis==3.5.3 diff --git a/requirements/rpm_requirements.txt b/requirements/rpm_requirements.txt index b6a192d1a..0501d2860 100644 --- a/requirements/rpm_requirements.txt +++ b/requirements/rpm_requirements.txt @@ -1 +1 @@ -gcc krb5-devel libtiff-devel libjpeg-devel libzip-devel freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel sshpass openldap-devel mariadb-devel mysql-community-devel mysql libffi-devel openssh-clients telnet openldap-clients +gcc make krb5-devel libtiff-devel libjpeg-devel libzip-devel freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel sshpass openldap-devel mariadb-devel mysql-community-devel mysql libffi-devel openssh-clients telnet openldap-clients From 75ef413ea5c3e2b7900500aca24b00beb6ed6e9a Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Fri, 4 Dec 2020 10:24:10 +0800 Subject: [PATCH 35/57] =?UTF-8?q?fix(applications):=20=E4=BF=AE=E6=94=B9at?= =?UTF-8?q?trs=E4=B8=8D=E8=83=BD=E4=B8=BAnull=20(#5171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bai --- apps/applications/serializers/application.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/applications/serializers/application.py b/apps/applications/serializers/application.py index e44e0ab95..a8dccd28d 100644 --- a/apps/applications/serializers/application.py +++ b/apps/applications/serializers/application.py @@ -27,10 +27,7 @@ class ApplicationSerializer(BulkOrgResourceModelSerializer): ] def create(self, validated_data): - attrs = validated_data.pop('attrs', {}) instance = super().create(validated_data) - instance.attrs = attrs - instance.save() return instance def update(self, instance, validated_data): From 3447eeda685cf695184b4ba9d6219a5d3599feb1 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Fri, 4 Dec 2020 13:10:59 +0800 Subject: [PATCH 36/57] =?UTF-8?q?fix(applications):=20=E4=BF=AE=E6=94=B9at?= =?UTF-8?q?trs=E4=B8=8D=E8=83=BD=E4=B8=BAnull=20(#5172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bai --- apps/applications/serializers/application.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/applications/serializers/application.py b/apps/applications/serializers/application.py index a8dccd28d..5ffcc65fb 100644 --- a/apps/applications/serializers/application.py +++ b/apps/applications/serializers/application.py @@ -27,6 +27,7 @@ class ApplicationSerializer(BulkOrgResourceModelSerializer): ] def create(self, validated_data): + validated_data['attrs'] = validated_data.pop('attrs', {}) instance = super().create(validated_data) return instance From 619b521ea14e00fb9713cfd585cb6ac449029b3c Mon Sep 17 00:00:00 2001 From: ibuler Date: Sat, 5 Dec 2020 13:19:44 +0800 Subject: [PATCH 37/57] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E8=AF=AD?= =?UTF-8?q?=E8=A8=80i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jms | 13 +++++++------ requirements/deb_buster_requirements.txt | 4 ++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/jms b/jms index 56f5ede59..a0b628db1 100755 --- a/jms +++ b/jms @@ -81,7 +81,7 @@ def check_database_connection(): logging.info("Database connect success") return time.sleep(1) - logging.info("Connection database failed, exist") + logging.error("Connection database failed, exist") sys.exit(10) @@ -93,7 +93,8 @@ def check_migrations(): return for i in range(3): print("!!! Warning: Has SQL migrations not perform, 有 SQL 变更没有执行") - print("You should run ./PROC upgrade first, 请先运行 ./PROC upgrade, 进行表结构变更") + print("You should run `./PROC upgrade_db` first, 请先运行 ./PROC upgrade_db, 进行表结构变更") + sys.exit(1) def perform_db_migrate(): @@ -522,7 +523,7 @@ def show_service_status(s): print("{} is stopped".format(ns)) -def upgrade(): +def upgrade_db(): collect_static() perform_db_migrate() @@ -539,7 +540,7 @@ if __name__ == '__main__': ) parser.add_argument( 'action', type=str, - choices=("start", "stop", "restart", "status", "upgrade"), + choices=("start", "stop", "restart", "status", "upgrade_db"), help="Action to run" ) parser.add_argument( @@ -574,7 +575,7 @@ if __name__ == '__main__': stop_service(srv) time.sleep(5) start_services_and_watch(srv) - elif action == "upgrade": - upgrade() + elif action == "upgrade_db": + upgrade_db() else: show_service_status(srv) diff --git a/requirements/deb_buster_requirements.txt b/requirements/deb_buster_requirements.txt index 5d873b060..50bde94aa 100644 --- a/requirements/deb_buster_requirements.txt +++ b/requirements/deb_buster_requirements.txt @@ -1,6 +1,10 @@ # common gcc cmake +curl +wget +vim +locales # mysql-client default-libmysqlclient-dev From 43b5e97b95cb71fbb1370fb4cc175f140160f080 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Mon, 7 Dec 2020 15:23:05 +0800 Subject: [PATCH 38/57] =?UTF-8?q?feat(excel):=20=E6=B7=BB=E5=8A=A0Excel?= =?UTF-8?q?=E5=AF=BC=E5=85=A5/=E5=AF=BC=E5=87=BA=20(#5124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(drf_renderer): 添加 ExcelRenderer 支持导出excel文件格式; 优化CSVRenderer, 抽象 BaseRenderer * perf(renderer): 支持导出资源详情 * refactor(drf_parser): 添加 ExcelParser 支持导入excel文件格式; 优化CSVParser, 抽象 BaseParser * refactor(drf_parser): 添加 ExcelParser 支持导入excel文件格式; 优化CSVParser, 抽象 BaseParser 2 * perf(renderer): 捕获renderer处理异常 * perf: 添加excel依赖包 * perf(drf): 优化导入导出错误日志 * perf: 添加依赖包 pyexcel-io==0.6.4 * perf: 添加依赖包pyexcel-xlsx==0.6.0 * feat: 修改drf/renderer&parser变量命名 * feat: 修改drf/renderer的bug * feat: 修改drf/renderer&parser变量命名 Co-authored-by: Bai --- apps/common/drf/parsers/__init__.py | 3 +- apps/common/drf/parsers/base.py | 132 ++++++++++++++++++++++++++++ apps/common/drf/parsers/csv.py | 122 ++----------------------- apps/common/drf/parsers/excel.py | 14 +++ apps/common/drf/renders/__init__.py | 1 + apps/common/drf/renders/base.py | 132 ++++++++++++++++++++++++++++ apps/common/drf/renders/csv.py | 83 ++++------------- apps/common/drf/renders/excel.py | 28 ++++++ apps/jumpserver/settings/libs.py | 7 +- requirements/requirements.txt | 3 + 10 files changed, 339 insertions(+), 186 deletions(-) create mode 100644 apps/common/drf/parsers/base.py create mode 100644 apps/common/drf/parsers/excel.py create mode 100644 apps/common/drf/renders/base.py create mode 100644 apps/common/drf/renders/excel.py diff --git a/apps/common/drf/parsers/__init__.py b/apps/common/drf/parsers/__init__.py index 671c86586..75dc28249 100644 --- a/apps/common/drf/parsers/__init__.py +++ b/apps/common/drf/parsers/__init__.py @@ -1 +1,2 @@ -from .csv import * \ No newline at end of file +from .csv import * +from .excel import * \ No newline at end of file diff --git a/apps/common/drf/parsers/base.py b/apps/common/drf/parsers/base.py new file mode 100644 index 000000000..605dcdd08 --- /dev/null +++ b/apps/common/drf/parsers/base.py @@ -0,0 +1,132 @@ +import abc +import json +import codecs +from django.utils.translation import ugettext_lazy as _ +from rest_framework.parsers import BaseParser +from rest_framework import status +from rest_framework.exceptions import ParseError, APIException +from common.utils import get_logger + +logger = get_logger(__file__) + + +class FileContentOverflowedError(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_code = 'file_content_overflowed' + default_detail = _('The file content overflowed (The maximum length `{}` bytes)') + + +class BaseFileParser(BaseParser): + + FILE_CONTENT_MAX_LENGTH = 1024 * 1024 * 10 + + serializer_cls = None + + def check_content_length(self, meta): + content_length = int(meta.get('CONTENT_LENGTH', meta.get('HTTP_CONTENT_LENGTH', 0))) + if content_length > self.FILE_CONTENT_MAX_LENGTH: + msg = FileContentOverflowedError.default_detail.format(self.FILE_CONTENT_MAX_LENGTH) + logger.error(msg) + raise FileContentOverflowedError(msg) + + @staticmethod + def get_stream_data(stream): + stream_data = stream.read() + stream_data = stream_data.strip(codecs.BOM_UTF8) + return stream_data + + @abc.abstractmethod + def generate_rows(self, stream_data): + raise NotImplemented + + def get_column_titles(self, rows): + return next(rows) + + def convert_to_field_names(self, column_titles): + fields_map = {} + fields = self.serializer_cls().fields + fields_map.update({v.label: k for k, v in fields.items()}) + fields_map.update({k: k for k, _ in fields.items()}) + field_names = [ + fields_map.get(column_title.strip('*'), '') + for column_title in column_titles + ] + return field_names + + @staticmethod + def _replace_chinese_quote(s): + trans_table = str.maketrans({ + '“': '"', + '”': '"', + '‘': '"', + '’': '"', + '\'': '"' + }) + return s.translate(trans_table) + + @classmethod + def process_row(cls, row): + """ + 构建json数据前的行处理 + """ + new_row = [] + for col in row: + # 转换中文引号 + col = cls._replace_chinese_quote(col) + # 列表/字典转换 + if isinstance(col, str) and ( + (col.startswith('[') and col.endswith(']')) + or + (col.startswith("{") and col.endswith("}")) + ): + col = json.loads(col) + new_row.append(col) + return new_row + + @staticmethod + def process_row_data(row_data): + """ + 构建json数据后的行数据处理 + """ + new_row_data = {} + 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(): + new_row_data[k] = v + return new_row_data + + def generate_data(self, fields_name, rows): + data = [] + for row in rows: + # 空行不处理 + if not any(row): + continue + row = self.process_row(row) + row_data = dict(zip(fields_name, row)) + row_data = self.process_row_data(row_data) + data.append(row_data) + return data + + def parse(self, stream, media_type=None, parser_context=None): + parser_context = parser_context or {} + + try: + view = parser_context['view'] + meta = view.request.META + self.serializer_cls = view.get_serializer_class() + except Exception as e: + logger.debug(e, exc_info=True) + raise ParseError('The resource does not support imports!') + + self.check_content_length(meta) + + try: + stream_data = self.get_stream_data(stream) + rows = self.generate_rows(stream_data) + column_titles = self.get_column_titles(rows) + field_names = self.convert_to_field_names(column_titles) + data = self.generate_data(field_names, rows) + return data + except Exception as e: + logger.error(e, exc_info=True) + raise ParseError('Parse error! ({})'.format(self.media_type)) + diff --git a/apps/common/drf/parsers/csv.py b/apps/common/drf/parsers/csv.py index de0d14ea7..0dd11aa4b 100644 --- a/apps/common/drf/parsers/csv.py +++ b/apps/common/drf/parsers/csv.py @@ -1,32 +1,13 @@ # ~*~ coding: utf-8 ~*~ # -import json import chardet -import codecs import unicodecsv -from django.utils.translation import ugettext as _ -from rest_framework.parsers import BaseParser -from rest_framework.exceptions import ParseError, APIException -from rest_framework import status - -from common.utils import get_logger - -logger = get_logger(__file__) +from .base import BaseFileParser -class CsvDataTooBig(APIException): - status_code = status.HTTP_400_BAD_REQUEST - default_code = 'csv_data_too_big' - default_detail = _('The max size of CSV is %d bytes') - - -class JMSCSVParser(BaseParser): - """ - Parses CSV file to serializer data - """ - CSV_UPLOAD_MAX_SIZE = 1024 * 1024 * 10 +class CSVFileParser(BaseFileParser): media_type = 'text/csv' @@ -38,99 +19,10 @@ class JMSCSVParser(BaseParser): for line in stream.splitlines(): yield line - @staticmethod - def _gen_rows(csv_data, charset='utf-8', **kwargs): - csv_reader = unicodecsv.reader(csv_data, encoding=charset, **kwargs) + def generate_rows(self, stream_data): + detect_result = chardet.detect(stream_data) + encoding = detect_result.get("encoding", "utf-8") + lines = self._universal_newlines(stream_data) + csv_reader = unicodecsv.reader(lines, encoding=encoding) for row in csv_reader: - if not any(row): # 空行 - continue yield row - - @staticmethod - def _get_fields_map(serializer_cls): - fields_map = {} - fields = serializer_cls().fields - fields_map.update({v.label: k for k, v in fields.items()}) - fields_map.update({k: k for k, _ in fields.items()}) - return fields_map - - @staticmethod - def _replace_chinese_quot(str_): - trans_table = str.maketrans({ - '“': '"', - '”': '"', - '‘': '"', - '’': '"', - '\'': '"' - }) - return str_.translate(trans_table) - - @classmethod - def _process_row(cls, row): - """ - 构建json数据前的行处理 - """ - _row = [] - - for col in row: - # 列表转换 - if isinstance(col, str) and col.startswith('[') and col.endswith(']'): - col = cls._replace_chinese_quot(col) - col = json.loads(col) - # 字典转换 - if isinstance(col, str) and col.startswith("{") and col.endswith("}"): - col = cls._replace_chinese_quot(col) - col = json.loads(col) - _row.append(col) - return _row - - @staticmethod - def _process_row_data(row_data): - """ - 构建json数据后的行数据处理 - """ - _row_data = {} - 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(): - _row_data[k] = v - return _row_data - - def parse(self, stream, media_type=None, parser_context=None): - parser_context = parser_context or {} - try: - view = parser_context['view'] - meta = view.request.META - serializer_cls = view.get_serializer_class() - except Exception as e: - logger.debug(e, exc_info=True) - raise ParseError('The resource does not support imports!') - - content_length = int(meta.get('CONTENT_LENGTH', meta.get('HTTP_CONTENT_LENGTH', 0))) - if content_length > self.CSV_UPLOAD_MAX_SIZE: - msg = CsvDataTooBig.default_detail % self.CSV_UPLOAD_MAX_SIZE - logger.error(msg) - raise CsvDataTooBig(msg) - - try: - stream_data = stream.read() - stream_data = stream_data.strip(codecs.BOM_UTF8) - detect_result = chardet.detect(stream_data) - encoding = detect_result.get("encoding", "utf-8") - binary = self._universal_newlines(stream_data) - rows = self._gen_rows(binary, charset=encoding) - - header = next(rows) - fields_map = self._get_fields_map(serializer_cls) - header = [fields_map.get(name.strip('*'), '') for name in header] - - data = [] - for row in rows: - row = self._process_row(row) - row_data = dict(zip(header, row)) - row_data = self._process_row_data(row_data) - data.append(row_data) - return data - except Exception as e: - logger.error(e, exc_info=True) - raise ParseError('CSV parse error!') diff --git a/apps/common/drf/parsers/excel.py b/apps/common/drf/parsers/excel.py new file mode 100644 index 000000000..c5007866c --- /dev/null +++ b/apps/common/drf/parsers/excel.py @@ -0,0 +1,14 @@ +import pyexcel +from .base import BaseFileParser + + +class ExcelFileParser(BaseFileParser): + + media_type = 'text/xlsx' + + def generate_rows(self, stream_data): + workbook = pyexcel.get_book(file_type='xlsx', file_content=stream_data) + # 默认获取第一个工作表sheet + sheet = workbook.sheet_by_index(0) + rows = sheet.rows() + return rows diff --git a/apps/common/drf/renders/__init__.py b/apps/common/drf/renders/__init__.py index f99b13586..bbefe8783 100644 --- a/apps/common/drf/renders/__init__.py +++ b/apps/common/drf/renders/__init__.py @@ -1,6 +1,7 @@ from rest_framework import renderers from .csv import * +from .excel import * class PassthroughRenderer(renderers.BaseRenderer): diff --git a/apps/common/drf/renders/base.py b/apps/common/drf/renders/base.py new file mode 100644 index 000000000..deac735cc --- /dev/null +++ b/apps/common/drf/renders/base.py @@ -0,0 +1,132 @@ +import abc +from datetime import datetime +from rest_framework.renderers import BaseRenderer +from rest_framework.utils import encoders, json + +from common.utils import get_logger + +logger = get_logger(__file__) + + +class BaseFileRenderer(BaseRenderer): + # 渲染模版标识, 导入、导出、更新模版: ['import', 'update', 'export'] + template = 'export' + serializer = None + + @staticmethod + def _check_validation_data(data): + detail_key = "detail" + if detail_key in data: + return False + return True + + @staticmethod + def _json_format_response(response_data): + return json.dumps(response_data) + + def set_response_disposition(self, response): + serializer = self.serializer + if response and hasattr(serializer, 'Meta') and hasattr(serializer.Meta, "model"): + model_name = serializer.Meta.model.__name__.lower() + now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + filename = "{}_{}.{}".format(model_name, now, self.format) + disposition = 'attachment; filename="{}"'.format(filename) + response['Content-Disposition'] = disposition + + def get_rendered_fields(self): + fields = self.serializer.fields + if self.template == 'import': + return [v for k, v in fields.items() if not v.read_only and k != "org_id" and k != 'id'] + elif self.template == 'update': + return [v for k, v in fields.items() if not v.read_only and k != "org_id"] + else: + return [v for k, v in fields.items() if not v.write_only and k != "org_id"] + + @staticmethod + def get_column_titles(render_fields): + return [ + '*{}'.format(field.label) if field.required else str(field.label) + for field in render_fields + ] + + def process_data(self, data): + results = data['results'] if 'results' in data else data + + if isinstance(results, dict): + results = [results] + + if self.template == 'import': + results = [results[0]] if results else results + + else: + # 限制数据数量 + results = results[:10000] + # 会将一些 UUID 字段转化为 string + results = json.loads(json.dumps(results, cls=encoders.JSONEncoder)) + return results + + @staticmethod + def generate_rows(data, render_fields): + for item in data: + row = [] + for field in render_fields: + value = item.get(field.field_name) + value = str(value) if value else '' + row.append(value) + yield row + + @abc.abstractmethod + def initial_writer(self): + raise NotImplementedError + + def write_column_titles(self, column_titles): + self.write_row(column_titles) + + def write_rows(self, rows): + for row in rows: + self.write_row(row) + + @abc.abstractmethod + def write_row(self, row): + raise NotImplementedError + + @abc.abstractmethod + def get_rendered_value(self): + raise NotImplementedError + + def render(self, data, accepted_media_type=None, renderer_context=None): + if data is None: + return bytes() + + if not self._check_validation_data(data): + return self._json_format_response(data) + + try: + renderer_context = renderer_context or {} + request = renderer_context['request'] + response = renderer_context['response'] + view = renderer_context['view'] + self.template = request.query_params.get('template', 'export') + self.serializer = view.get_serializer() + self.set_response_disposition(response) + except Exception as e: + logger.debug(e, exc_info=True) + value = 'The resource not support export!'.encode('utf-8') + return value + + try: + rendered_fields = self.get_rendered_fields() + column_titles = self.get_column_titles(rendered_fields) + data = self.process_data(data) + rows = self.generate_rows(data, rendered_fields) + self.initial_writer() + self.write_column_titles(column_titles) + self.write_rows(rows) + value = self.get_rendered_value() + except Exception as e: + logger.debug(e, exc_info=True) + value = 'Render error! ({})'.format(self.media_type).encode('utf-8') + return value + + return value + diff --git a/apps/common/drf/renders/csv.py b/apps/common/drf/renders/csv.py index 435e3d4a6..ba469a21f 100644 --- a/apps/common/drf/renders/csv.py +++ b/apps/common/drf/renders/csv.py @@ -1,83 +1,30 @@ # ~*~ coding: utf-8 ~*~ # -import unicodecsv import codecs -from datetime import datetime - +import unicodecsv from six import BytesIO -from rest_framework.renderers import BaseRenderer -from rest_framework.utils import encoders, json -from common.utils import get_logger - -logger = get_logger(__file__) +from .base import BaseFileRenderer -class JMSCSVRender(BaseRenderer): - +class CSVFileRenderer(BaseFileRenderer): media_type = 'text/csv' format = 'csv' - @staticmethod - def _get_show_fields(fields, template): - if template == 'import': - return [v for k, v in fields.items() if not v.read_only and k != "org_id" and k != 'id'] - elif template == 'update': - return [v for k, v in fields.items() if not v.read_only and k != "org_id"] - else: - return [v for k, v in fields.items() if not v.write_only and k != "org_id"] + writer = None + buffer = None - @staticmethod - def _gen_table(data, fields): - data = data[:10000] - yield ['*{}'.format(f.label) if f.required else f.label for f in fields] + def initial_writer(self): + csv_buffer = BytesIO() + csv_buffer.write(codecs.BOM_UTF8) + csv_writer = unicodecsv.writer(csv_buffer, encoding='utf-8') + self.buffer = csv_buffer + self.writer = csv_writer - for item in data: - row = [item.get(f.field_name) for f in fields] - yield row - - def set_response_disposition(self, serializer, context): - response = context.get('response') - if response and hasattr(serializer, 'Meta') and \ - hasattr(serializer.Meta, "model"): - model_name = serializer.Meta.model.__name__.lower() - now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") - filename = "{}_{}.csv".format(model_name, now) - disposition = 'attachment; filename="{}"'.format(filename) - response['Content-Disposition'] = disposition - - def render(self, data, media_type=None, renderer_context=None): - renderer_context = renderer_context or {} - request = renderer_context['request'] - template = request.query_params.get('template', 'export') - view = renderer_context['view'] - - if isinstance(data, dict): - data = data.get("results", []) - - if template == 'import': - data = [data[0]] if data else data - - data = json.loads(json.dumps(data, cls=encoders.JSONEncoder)) - - try: - serializer = view.get_serializer() - self.set_response_disposition(serializer, renderer_context) - except Exception as e: - logger.debug(e, exc_info=True) - value = 'The resource not support export!'.encode('utf-8') - else: - fields = serializer.fields - show_fields = self._get_show_fields(fields, template) - table = self._gen_table(data, show_fields) - - csv_buffer = BytesIO() - csv_buffer.write(codecs.BOM_UTF8) - csv_writer = unicodecsv.writer(csv_buffer, encoding='utf-8') - for row in table: - csv_writer.writerow(row) - - value = csv_buffer.getvalue() + def write_row(self, row): + self.writer.writerow(row) + def get_rendered_value(self): + value = self.buffer.getvalue() return value diff --git a/apps/common/drf/renders/excel.py b/apps/common/drf/renders/excel.py new file mode 100644 index 000000000..0d1cb8d51 --- /dev/null +++ b/apps/common/drf/renders/excel.py @@ -0,0 +1,28 @@ +from openpyxl import Workbook +from openpyxl.writer.excel import save_virtual_workbook + +from .base import BaseFileRenderer + + +class ExcelFileRenderer(BaseFileRenderer): + media_type = "application/xlsx" + format = "xlsx" + + wb = None + ws = None + row_count = 0 + + def initial_writer(self): + self.wb = Workbook() + self.ws = self.wb.active + + def write_row(self, row): + self.row_count += 1 + column_count = 0 + for cell_value in row: + column_count += 1 + self.ws.cell(row=self.row_count, column=column_count, value=cell_value) + + def get_rendered_value(self): + value = save_virtual_workbook(self.wb) + return value diff --git a/apps/jumpserver/settings/libs.py b/apps/jumpserver/settings/libs.py index e60932464..782d2bc06 100644 --- a/apps/jumpserver/settings/libs.py +++ b/apps/jumpserver/settings/libs.py @@ -12,13 +12,16 @@ REST_FRAMEWORK = { 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.JSONRenderer', # 'rest_framework.renderers.BrowsableAPIRenderer', - 'common.drf.renders.JMSCSVRender', + 'common.drf.renders.CSVFileRenderer', + 'common.drf.renders.ExcelFileRenderer', + ), 'DEFAULT_PARSER_CLASSES': ( 'rest_framework.parsers.JSONParser', 'rest_framework.parsers.FormParser', 'rest_framework.parsers.MultiPartParser', - 'common.drf.parsers.JMSCSVParser', + 'common.drf.parsers.CSVFileParser', + 'common.drf.parsers.ExcelFileParser', 'rest_framework.parsers.FileUploadParser', ), 'DEFAULT_AUTHENTICATION_CLASSES': ( diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 391481090..fa287298e 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -105,3 +105,6 @@ azure-mgmt-compute==4.6.2 azure-mgmt-network==2.7.0 msrestazure==0.6.4 adal==1.2.5 +openpyxl==3.0.5 +pyexcel==0.6.6 +pyexcel-xlsx==0.6.0 From 2a6f68c7ba988358a2f777e60e8e61136a543e99 Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 7 Dec 2020 19:11:32 +0800 Subject: [PATCH 39/57] =?UTF-8?q?revert:=20=E8=BF=98=E5=8E=9F=E5=8E=9F?= =?UTF-8?q?=E6=9D=A5=E7=9A=84jms=EF=BC=8C=E8=87=AA=E5=8A=A8=E8=BF=90?= =?UTF-8?q?=E8=A1=8Cmigraionts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jms | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/jms b/jms index a0b628db1..7e4f2faed 100755 --- a/jms +++ b/jms @@ -91,10 +91,10 @@ def check_migrations(): if code == 1: return - for i in range(3): - print("!!! Warning: Has SQL migrations not perform, 有 SQL 变更没有执行") - print("You should run `./PROC upgrade_db` first, 请先运行 ./PROC upgrade_db, 进行表结构变更") - sys.exit(1) + # for i in range(3): + # print("!!! Warning: Has SQL migrations not perform, 有 SQL 变更没有执行") + # print("You should run `./PROC upgrade_db` first, 请先运行 ./PROC upgrade_db, 进行表结构变更") + # sys.exit(1) def perform_db_migrate(): @@ -115,6 +115,7 @@ def collect_static(): def prepare(): check_database_connection() check_migrations() + upgrade_db() def check_pid(pid): From 042ea5e137213fd713ee43f3214cce2734f7050d Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 8 Dec 2020 10:59:07 +0800 Subject: [PATCH 40/57] =?UTF-8?q?feat:=20=E6=8E=88=E6=9D=83=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E6=A0=91API=E8=BF=94=E5=9B=9Eorg=5Fname=E5=AD=97?= =?UTF-8?q?=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/applications/api/mixin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/applications/api/mixin.py b/apps/applications/api/mixin.py index fe51e1dc7..7833b46ba 100644 --- a/apps/applications/api/mixin.py +++ b/apps/applications/api/mixin.py @@ -88,6 +88,9 @@ class SerializeApplicationToTreeNodeMixin: def _serialize(self, application): method_name = f'_serialize_{application.category}' data = getattr(self, method_name)(application) + data.update({ + 'org_name': application.org_name + }) return data def serialize_applications(self, applications): From dd979f582a7bd2aa317c5425796d6ea7ebe74713 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Tue, 8 Dec 2020 14:26:18 +0800 Subject: [PATCH 41/57] stash (#5178) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Dev (#4791) * fix(xpack): 修复last login太长的问题 (#4786) Co-authored-by: ibuler * perf: 更新密码中也发送邮件 (#4789) Co-authored-by: ibuler * fix(terminal): 修复获取螺旋的异步api * fix(terminal): 修复有的录像存储有问题的导致下载录像的bug * fix(orgs): 修复组织添加用户bug * perf(requirements): 修改jms-storage==0.0.34 (#4797) Co-authored-by: Bai Co-authored-by: fit2bot <68588906+fit2bot@users.noreply.github.com> Co-authored-by: ibuler Co-authored-by: Bai * stash * feat(system): 添加系统app * stash * fix: 修复一些bug Co-authored-by: xinwen Co-authored-by: ibuler Co-authored-by: Bai Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com> --- apps/jumpserver/settings/base.py | 1 + apps/jumpserver/urls.py | 3 +- apps/system/__init__.py | 0 apps/system/admin.py | 3 + apps/system/api.py | 17 +++++ apps/system/apps.py | 5 ++ apps/system/migrations/0001_initial.py | 26 ++++++++ apps/system/migrations/__init__.py | 0 apps/system/models.py | 68 +++++++++++++++++++ apps/system/serializers.py | 12 ++++ apps/system/tests.py | 3 + apps/system/urls.py | 14 ++++ apps/system/views.py | 3 + utils/generate_fake_data/generate.py | 4 +- utils/generate_fake_data/resources/system.py | 69 ++++++++++++++++++++ utils/generate_fake_data/resources/users.py | 2 +- 16 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 apps/system/__init__.py create mode 100644 apps/system/admin.py create mode 100644 apps/system/api.py create mode 100644 apps/system/apps.py create mode 100644 apps/system/migrations/0001_initial.py create mode 100644 apps/system/migrations/__init__.py create mode 100644 apps/system/models.py create mode 100644 apps/system/serializers.py create mode 100644 apps/system/tests.py create mode 100644 apps/system/urls.py create mode 100644 apps/system/views.py create mode 100644 utils/generate_fake_data/resources/system.py diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index 47b3d7a04..db00cb6a2 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -48,6 +48,7 @@ INSTALLED_APPS = [ 'authentication.apps.AuthenticationConfig', # authentication 'applications.apps.ApplicationsConfig', 'tickets.apps.TicketsConfig', + 'system.apps.SystemConfig', 'jms_oidc_rp', 'rest_framework', 'rest_framework_swagger', diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 624212e6f..dc74bcd14 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -23,6 +23,7 @@ api_v1 = [ path('common/', include('common.urls.api_urls', namespace='api-common')), path('applications/', include('applications.urls.api_urls', namespace='api-applications')), path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')), + path('system/', include('system.urls', namespace='api-system')), ] api_v2 = [ @@ -63,7 +64,7 @@ urlpatterns = [ # External apps url path('core/auth/captcha/', include('captcha.urls')), path('core/', include(app_view_patterns)), - path('ui/', views.UIView.as_view()) + path('ui/', views.UIView.as_view()), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ diff --git a/apps/system/__init__.py b/apps/system/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/system/admin.py b/apps/system/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/apps/system/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/system/api.py b/apps/system/api.py new file mode 100644 index 000000000..d1ebb369c --- /dev/null +++ b/apps/system/api.py @@ -0,0 +1,17 @@ +# ~*~ coding: utf-8 ~*~ +from common.permissions import IsOrgAdminOrAppUser +from common.drf.api import JMSBulkModelViewSet +from common.utils import get_logger +from . import serializers +from .models import Stat + +logger = get_logger(__name__) +__all__ = ['StatViewSet'] + + +class StatViewSet(JMSBulkModelViewSet): + queryset = Stat.objects.all() + filter_fields = ('id', 'key', 'value', 'component') + search_fields = filter_fields + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = serializers.StatSerializer diff --git a/apps/system/apps.py b/apps/system/apps.py new file mode 100644 index 000000000..5dc4d64bc --- /dev/null +++ b/apps/system/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SystemConfig(AppConfig): + name = 'system' diff --git a/apps/system/migrations/0001_initial.py b/apps/system/migrations/0001_initial.py new file mode 100644 index 000000000..d4f7ad7a1 --- /dev/null +++ b/apps/system/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 3.1 on 2020-11-25 06:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Stat', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('node', models.CharField(max_length=128)), + ('ip', models.GenericIPAddressField()), + ('component', models.CharField(choices=[('core', 'Core'), ('koko', 'KoKo'), ('guacamole', 'Guacamole'), ('omnidb', 'OmniDB')], max_length=16)), + ('key', models.CharField(db_index=True, max_length=16, verbose_name='Item key')), + ('value', models.FloatField()), + ('datetime', models.DateTimeField()), + ], + ), + ] diff --git a/apps/system/migrations/__init__.py b/apps/system/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/system/models.py b/apps/system/models.py new file mode 100644 index 000000000..924f60521 --- /dev/null +++ b/apps/system/models.py @@ -0,0 +1,68 @@ +import time + +from django.db import models +import psutil +from django.utils import timezone + +from django.utils.translation import ugettext_lazy as _ + + +class Stat(models.Model): + class Components(models.TextChoices): + core = 'core', 'Core' + koko = 'koko', 'KoKo' + guacamole = 'guacamole', 'Guacamole' + omnidb = 'omnidb', 'OmniDB' + + class Keys(models.TextChoices): + cpu_load_1 = 'cpu_load', 'CPU load' + memory_used_percent = 'memory_used_percent', _('Memory used percent') + disk_used_percent = 'disk_used_percent', _('Disk used percent') + session_active = 'session_active', _('Session active') + session_processed = 'session_processed', _('Session processed') + + node = models.CharField(max_length=128) + ip = models.GenericIPAddressField() + component = models.CharField(choices=Components.choices, max_length=16) + key = models.CharField(db_index=True, max_length=16, verbose_name=_('Item key')) + value = models.FloatField() + datetime = models.DateTimeField() + + def __str__(self): + return f'{self.key}:{self.value}' + + @staticmethod + def collect_local_stats(): + memory_percent = psutil.virtual_memory().percent + cpu_load = psutil.getloadavg() + cpu_load_1 = round(cpu_load[0], 2) + cpu_load_5 = round(cpu_load[1], 2) + cpu_load_15 = round(cpu_load[2], 2) + cpu_percent = psutil.cpu_percent() + stats = dict( + memory_percent=memory_percent, + cpu_load_1=cpu_load_1, + cpu_load_5=cpu_load_5, + cpu_load_15=cpu_load_15, + cpu_load=cpu_load_1, + cpu_percent=cpu_percent + ) + return stats + + @classmethod + def keep_collect_local_stats(cls): + data = { + 'node': 'core-01', + 'ip': '192.168.1.1', + 'component': 'core' + } + while True: + stats = cls.collect_local_stats() + data['datetime'] = timezone.now() + items = [] + for k, v in stats.items(): + data['key'] = k + data['value'] = v + items.append(cls(**data)) + cls.objects.bulk_create(items, ignore_conflicts=True) + time.sleep(60) diff --git a/apps/system/serializers.py b/apps/system/serializers.py new file mode 100644 index 000000000..e8ada1d15 --- /dev/null +++ b/apps/system/serializers.py @@ -0,0 +1,12 @@ +from common.drf.serializers import BulkModelSerializer + +from .models import Stat + + +class StatSerializer(BulkModelSerializer): + class Meta: + model = Stat + fields = ( + 'id', 'node', 'ip', 'component', + 'key', 'value', 'datetime', + ) diff --git a/apps/system/tests.py b/apps/system/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/apps/system/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/system/urls.py b/apps/system/urls.py new file mode 100644 index 000000000..dee2db457 --- /dev/null +++ b/apps/system/urls.py @@ -0,0 +1,14 @@ +# coding:utf-8 +from rest_framework_bulk.routes import BulkRouter + +from . import api + +app_name = 'system' + +router = BulkRouter() +router.register(r'stats', api.StatViewSet, 'stat') + +urlpatterns = [ +] + +urlpatterns += router.urls diff --git a/apps/system/views.py b/apps/system/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/apps/system/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/utils/generate_fake_data/generate.py b/utils/generate_fake_data/generate.py index f504c8e55..c87a8dbe4 100644 --- a/utils/generate_fake_data/generate.py +++ b/utils/generate_fake_data/generate.py @@ -15,6 +15,7 @@ django.setup() from resources.assets import AssetsGenerator, NodesGenerator, SystemUsersGenerator, AdminUsersGenerator from resources.users import UserGroupGenerator, UserGenerator from resources.perms import AssetPermissionGenerator +from resources.system import StatGenerator resource_generator_mapper = { @@ -24,7 +25,8 @@ resource_generator_mapper = { 'admin_user': AdminUsersGenerator, 'user': UserGenerator, 'user_group': UserGroupGenerator, - 'asset_permission': AssetPermissionGenerator + 'asset_permission': AssetPermissionGenerator, + 'stat': StatGenerator } diff --git a/utils/generate_fake_data/resources/system.py b/utils/generate_fake_data/resources/system.py new file mode 100644 index 000000000..69a12d144 --- /dev/null +++ b/utils/generate_fake_data/resources/system.py @@ -0,0 +1,69 @@ +import random + +from .base import FakeDataGenerator +from system.models import * + + +class StatGenerator(FakeDataGenerator): + resource = 'stat' + + nodes = [ + { + 'node': 'guacamole-01', + 'ip': '192.168.1.1', + 'component': 'guacamole' + }, + { + 'node': 'koko-01', + 'ip': '192.168.1.2', + 'component': 'koko' + }, + { + 'node': 'omnidb-01', + 'ip': '192.168.1.3', + 'component': 'omnidb' + }, + { + 'node': 'core-01', + 'ip': '192.168.1.4', + 'component': 'core' + } + ] + items_value_range = { + 'cpu_load': (0, 3.0), + 'memory_used_percent': (20, 10.0), + 'disk_used_percent': (30, 80.0), + 'thread': (100, 100), + 'goroutine': (200, 500), + 'replay_upload_health': (0, [0, 1]), + 'command_upload_health': (0, [0, 1]), + 'session_active': (100, 50), + 'session_processed': (400, 400), + 'session_failed': (50, 100), + 'session_succeeded': (500, 300) + } + + def do_generate(self, batch, batch_size): + datetime = timezone.now() + for i in batch: + datetime = datetime - timezone.timedelta(minutes=1) + items = [] + for node in self.nodes: + for key, values in self.items_value_range.items(): + base, r = values + if isinstance(r, int): + value = int(random.random() * r) + elif isinstance(r, float): + value = round(random.random() * r, 2) + elif isinstance(r, list): + value = random.choice(r) + else: + continue + value += base + node.update({ + 'key': key, + 'value': value, + 'datetime': datetime + }) + items.append(Stat(**node)) + Stat.objects.bulk_create(items, ignore_conflicts=True) \ No newline at end of file diff --git a/utils/generate_fake_data/resources/users.py b/utils/generate_fake_data/resources/users.py index 05332e4e2..0d8cbab9a 100644 --- a/utils/generate_fake_data/resources/users.py +++ b/utils/generate_fake_data/resources/users.py @@ -47,7 +47,7 @@ class UserGenerator(FakeDataGenerator): def do_generate(self, batch, batch_size): users = [] for i in batch: - username = forgery_py.internet.user_name(True) + username = forgery_py.internet.user_name(True) + '-' + str(i) email = forgery_py.internet.email_address() u = User( username=username, From 4b67d6925e28e14f4dc7d5d2ea33c512c951af3a Mon Sep 17 00:00:00 2001 From: xinwen Date: Tue, 8 Dec 2020 17:29:22 +0800 Subject: [PATCH 42/57] =?UTF-8?q?feat(asset):=20api=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=8E=A8=E9=80=81=E7=B3=BB=E7=BB=9F=E7=94=A8=E6=88=B7=E5=88=B0?= =?UTF-8?q?=E5=A4=9A=E4=B8=AA=E8=B5=84=E4=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/system_user.py | 24 +++++++++++++++--------- apps/assets/serializers/system_user.py | 4 ++++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index 59be0fa68..70bbe376e 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -3,7 +3,8 @@ from django.shortcuts import get_object_or_404 from rest_framework.response import Response from common.utils import get_logger -from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsAppUser +from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser +from common.drf.filters import CustomFilter from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics from orgs.utils import tmp_to_org @@ -12,7 +13,7 @@ from .. import serializers from ..serializers import SystemUserWithAuthInfoSerializer from ..tasks import ( push_system_user_to_assets_manual, test_system_user_connectivity_manual, - push_system_user_a_asset_manual, + push_system_user_to_assets ) @@ -82,18 +83,18 @@ class SystemUserTaskApi(generics.CreateAPIView): permission_classes = (IsOrgAdmin,) serializer_class = serializers.SystemUserTaskSerializer - def do_push(self, system_user, asset=None): - if asset is None: + def do_push(self, system_user, assets_id=None): + if assets_id is None: task = push_system_user_to_assets_manual.delay(system_user) else: username = self.request.query_params.get('username') - task = push_system_user_a_asset_manual.delay( - system_user, asset, username=username + task = push_system_user_to_assets.delay( + system_user.id, assets_id, username=username ) return task @staticmethod - def do_test(system_user, asset=None): + def do_test(system_user): task = test_system_user_connectivity_manual.delay(system_user) return task @@ -104,11 +105,16 @@ class SystemUserTaskApi(generics.CreateAPIView): def perform_create(self, serializer): action = serializer.validated_data["action"] asset = serializer.validated_data.get('asset') + assets = serializer.validated_data.get('assets') or [] + system_user = self.get_object() if action == 'push': - task = self.do_push(system_user, asset) + assets = [asset] if asset else assets + assets_id = [asset.id for asset in assets] + assets_id = assets_id if assets_id else None + task = self.do_push(system_user, assets_id) else: - task = self.do_test(system_user, asset) + task = self.do_test(system_user) data = getattr(serializer, '_data', {}) data["task"] = task.id setattr(serializer, '_data', data) diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index a4f6d933c..fb8d4df97 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -257,4 +257,8 @@ class SystemUserTaskSerializer(serializers.Serializer): asset = serializers.PrimaryKeyRelatedField( queryset=Asset.objects, allow_null=True, required=False, write_only=True ) + assets = serializers.PrimaryKeyRelatedField( + queryset=Asset.objects, allow_null=True, required=False, write_only=True, + many=True + ) task = serializers.CharField(read_only=True) From 2ccc5beedae58952bffc071e8063fb35e531cf9c Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Tue, 8 Dec 2020 20:22:48 +0800 Subject: [PATCH 43/57] =?UTF-8?q?perf(Dockerfile):=20=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E4=BD=BF=E7=94=A8zh=5FCN.UTF-8,=20en=5FUS.UTF-8=20=E5=BA=94?= =?UTF-8?q?=E8=AF=A5=E4=B9=9F=E6=98=AF=E5=8F=AF=E4=BB=A5=E7=9A=84=20(#5190?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(Dockerfile): 不再使用zh_CN.UTF-8, en_US.UTF-8 应该也是可以的 * fix: 还原回原来的LANG设置 * perf: 合并层数 * feat: 修改Dockerfile Co-authored-by: ibuler Co-authored-by: Bai --- Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index f88dbe4b5..5ed72c554 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,8 +21,10 @@ WORKDIR /opt/jumpserver COPY ./requirements/deb_buster_requirements.txt ./requirements/deb_buster_requirements.txt RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \ && sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \ - && apt update -RUN grep -v '^#' ./requirements/deb_buster_requirements.txt | xargs apt -y install + && apt update \ + && grep -v '^#' ./requirements/deb_buster_requirements.txt | xargs apt -y install \ + && localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \ + && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime COPY ./requirements/requirements.txt ./requirements/requirements.txt RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \ @@ -39,7 +41,6 @@ VOLUME /opt/jumpserver/data VOLUME /opt/jumpserver/logs ENV LANG=zh_CN.UTF-8 -ENV LC_ALL=zh_CN.UTF-8 EXPOSE 8070 EXPOSE 8080 From 4c469afa951f42c7e33ebcac751ee6057e02ccd9 Mon Sep 17 00:00:00 2001 From: "Jiangjie.Bai" <32935519+BaiJiangJie@users.noreply.github.com> Date: Tue, 8 Dec 2020 20:32:48 +0800 Subject: [PATCH 44/57] =?UTF-8?q?feat:=20=E5=8F=96=E6=B6=88=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E9=85=8D=E7=BD=AE=E7=9B=B8=E5=85=B3=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E5=8F=AA=E8=AF=BB=E6=A8=A1=E5=BC=8F=20(#5182)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 取消资产配置相关字段只读模式 * feat: 取消资产配置相关字段只读模式 --- apps/assets/serializers/asset.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index 37de2e7fd..ce8b54ca9 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -98,9 +98,6 @@ class AssetSerializer(BulkOrgResourceModelSerializer): fields_as = list(annotates_fields.keys()) fields = fields_small + fields_fk + fields_m2m + fields_as read_only_fields = [ - 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', - 'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info', - 'os', 'os_version', 'os_arch', 'hostname_raw', 'created_by', 'date_created', ] + fields_as From 5533114db51243af4f33d91d5c3457f689d6e481 Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 8 Dec 2020 15:33:24 +0800 Subject: [PATCH 45/57] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E5=BA=94=E7=94=A8=E6=A0=91=E6=8C=89=E7=BB=84=E7=BB=87?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E8=BF=9B=E8=A1=8C=E5=8C=BA=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/applications/api/mixin.py | 37 ++++++++++++++++++- apps/orgs/mixins/models.py | 6 +-- apps/orgs/utils.py | 7 +++- .../user_permission_applications.py | 2 +- .../user_permission_nodes_with_assets.py | 12 ++++-- apps/perms/urls/asset_permission.py | 2 +- 6 files changed, 54 insertions(+), 12 deletions(-) diff --git a/apps/applications/api/mixin.py b/apps/applications/api/mixin.py index 7833b46ba..91c7d7b3f 100644 --- a/apps/applications/api/mixin.py +++ b/apps/applications/api/mixin.py @@ -1,4 +1,5 @@ from common.exceptions import JMSException +from orgs.models import Organization from .. import models @@ -85,14 +86,46 @@ class SerializeApplicationToTreeNodeMixin: 'meta': {'type': 'k8s_app'} } - def _serialize(self, application): + def _serialize_application(self, application): method_name = f'_serialize_{application.category}' data = getattr(self, method_name)(application) data.update({ + 'pId': application.org.id, 'org_name': application.org_name }) return data def serialize_applications(self, applications): - data = [self._serialize(application) for application in applications] + data = [self._serialize_application(application) for application in applications] + return data + + @staticmethod + def _serialize_organization(org): + return { + 'id': org.id, + 'name': org.name, + 'title': org.name, + 'pId': '', + 'open': True, + 'isParent': True, + 'meta': { + 'type': 'node' + } + } + + def serialize_organizations(self, organizations): + data = [self._serialize_organization(org) for org in organizations] + return data + + @staticmethod + def filter_organizations(applications): + organizations_id = set(applications.values_list('org_id', flat=True)) + organizations = [Organization.get_instance(org_id) for org_id in organizations_id] + return organizations + + def serialize_applications_with_org(self, applications): + organizations = self.filter_organizations(applications) + data_organizations = self.serialize_organizations(organizations) + data_applications = self.serialize_applications(applications) + data = data_organizations + data_applications return data diff --git a/apps/orgs/mixins/models.py b/apps/orgs/mixins/models.py index af3a8a30f..f2f6ae49e 100644 --- a/apps/orgs/mixins/models.py +++ b/apps/orgs/mixins/models.py @@ -8,7 +8,7 @@ from django.core.exceptions import ValidationError from common.utils import get_logger from ..utils import ( set_current_org, get_current_org, current_org, - filter_org_queryset, get_org_name_by_id + filter_org_queryset, get_org_by_id, get_org_name_by_id ) from ..models import Organization @@ -70,9 +70,7 @@ class OrgModelMixin(models.Model): @property def org(self): - from orgs.models import Organization - org = Organization.get_instance(self.org_id) - return org + return get_org_by_id(self.org_id) @property def org_name(self): diff --git a/apps/orgs/utils.py b/apps/orgs/utils.py index 356fac9bd..c10a5dacc 100644 --- a/apps/orgs/utils.py +++ b/apps/orgs/utils.py @@ -90,10 +90,15 @@ def get_org_mapper(): return org_mapper -def get_org_name_by_id(org_id): +def get_org_by_id(org_id): org_id = str(org_id) org_mapper = get_org_mapper() org = org_mapper.get(org_id) + return org + + +def get_org_name_by_id(org_id): + org = get_org_by_id(org_id) if org: org_name = org.name else: 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 c74adb3d1..848e22304 100644 --- a/apps/perms/api/application/user_permission/user_permission_applications.py +++ b/apps/perms/api/application/user_permission/user_permission_applications.py @@ -48,7 +48,7 @@ class ApplicationsAsTreeMixin(SerializeApplicationToTreeNodeMixin): def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) - data = self.serialize_applications(queryset) + data = self.serialize_applications_with_org(queryset) return Response(data=data) diff --git a/apps/perms/api/asset/user_permission/user_permission_nodes_with_assets.py b/apps/perms/api/asset/user_permission/user_permission_nodes_with_assets.py index 4f9de071d..44d8f22b3 100644 --- a/apps/perms/api/asset/user_permission/user_permission_nodes_with_assets.py +++ b/apps/perms/api/asset/user_permission/user_permission_nodes_with_assets.py @@ -139,11 +139,13 @@ class MyGrantedNodesWithAssetsAsTreeApi(SerializeToTreeNodeMixin, ListAPIView): return Response(data=data) -class UserGrantedNodeChildrenWithAssetsAsTreeForAdminApi(ForAdminMixin, UserNodeGrantStatusDispatchMixin, - SerializeToTreeNodeMixin, ListAPIView): +class GrantedNodeChildrenWithAssetsAsTreeApiMixin(UserNodeGrantStatusDispatchMixin, + SerializeToTreeNodeMixin, + ListAPIView): """ 带资产的授权树 """ + user: None def get_data_on_node_direct_granted(self, key): nodes = Node.objects.filter(parent_key=key) @@ -203,5 +205,9 @@ class UserGrantedNodeChildrenWithAssetsAsTreeForAdminApi(ForAdminMixin, UserNode return Response(data=[*tree_nodes, *tree_assets]) -class MyGrantedNodeChildrenWithAssetsAsTreeApi(ForUserMixin, UserGrantedNodeChildrenWithAssetsAsTreeForAdminApi): +class UserGrantedNodeChildrenWithAssetsAsTreeApi(ForAdminMixin, GrantedNodeChildrenWithAssetsAsTreeApiMixin): + pass + + +class MyGrantedNodeChildrenWithAssetsAsTreeApi(ForUserMixin, GrantedNodeChildrenWithAssetsAsTreeApiMixin): pass diff --git a/apps/perms/urls/asset_permission.py b/apps/perms/urls/asset_permission.py index f7e10e0fc..7b251dea7 100644 --- a/apps/perms/urls/asset_permission.py +++ b/apps/perms/urls/asset_permission.py @@ -56,7 +56,7 @@ user_permission_urlpatterns = [ path('nodes-with-assets/tree/', api.MyGrantedNodesWithAssetsAsTreeApi.as_view(), name='my-nodes-with-assets-as-tree'), # 主要用于 luna 页面,带资产的节点树 - path('/nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeForAdminApi.as_view(), name='user-nodes-children-with-assets-as-tree'), + path('/nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='user-nodes-children-with-assets-as-tree'), path('nodes/children-with-assets/tree/', api.MyGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='my-nodes-children-with-assets-as-tree'), # 查询授权树上某个节点的所有资产 From 4c3a6552399cca20040fc7fecb764516daa403da Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 8 Dec 2020 20:51:02 +0800 Subject: [PATCH 46/57] =?UTF-8?q?perf:=20=E7=94=A8=E6=88=B7=E5=BA=8F?= =?UTF-8?q?=E5=88=97=E7=B1=BB=E7=A6=81=E6=AD=A2=E4=BF=AE=E6=94=B9source?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/users/serializers/user.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index e59467173..1e50c1107 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -68,6 +68,10 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): 'can_update', 'can_delete', 'login_blocked', 'org_roles' ] + read_only_fields = [ + 'date_joined', 'last_login', 'created_by', 'is_first_login', 'source' + ] + extra_kwargs = { 'password': {'write_only': True, 'required': False, 'allow_null': True, 'allow_blank': True}, 'public_key': {'write_only': True}, From b189e363cc7fbe8a42575910e8d20576c64f9e2b Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 9 Dec 2020 11:10:28 +0800 Subject: [PATCH 47/57] =?UTF-8?q?revert(system):=20=E6=9A=82=E6=97=B6?= =?UTF-8?q?=E5=8E=BB=E6=8E=89system=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/settings/base.py | 1 - apps/jumpserver/urls.py | 1 - apps/system/__init__.py | 0 apps/system/admin.py | 3 -- apps/system/api.py | 17 ------- apps/system/apps.py | 5 -- apps/system/migrations/0001_initial.py | 26 ---------- apps/system/migrations/__init__.py | 0 apps/system/models.py | 68 -------------------------- apps/system/serializers.py | 12 ----- apps/system/tests.py | 3 -- apps/system/urls.py | 14 ------ apps/system/views.py | 3 -- 13 files changed, 153 deletions(-) delete mode 100644 apps/system/__init__.py delete mode 100644 apps/system/admin.py delete mode 100644 apps/system/api.py delete mode 100644 apps/system/apps.py delete mode 100644 apps/system/migrations/0001_initial.py delete mode 100644 apps/system/migrations/__init__.py delete mode 100644 apps/system/models.py delete mode 100644 apps/system/serializers.py delete mode 100644 apps/system/tests.py delete mode 100644 apps/system/urls.py delete mode 100644 apps/system/views.py diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index db00cb6a2..47b3d7a04 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -48,7 +48,6 @@ INSTALLED_APPS = [ 'authentication.apps.AuthenticationConfig', # authentication 'applications.apps.ApplicationsConfig', 'tickets.apps.TicketsConfig', - 'system.apps.SystemConfig', 'jms_oidc_rp', 'rest_framework', 'rest_framework_swagger', diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index dc74bcd14..c6d85b48b 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -23,7 +23,6 @@ api_v1 = [ path('common/', include('common.urls.api_urls', namespace='api-common')), path('applications/', include('applications.urls.api_urls', namespace='api-applications')), path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')), - path('system/', include('system.urls', namespace='api-system')), ] api_v2 = [ diff --git a/apps/system/__init__.py b/apps/system/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/system/admin.py b/apps/system/admin.py deleted file mode 100644 index 8c38f3f3d..000000000 --- a/apps/system/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/apps/system/api.py b/apps/system/api.py deleted file mode 100644 index d1ebb369c..000000000 --- a/apps/system/api.py +++ /dev/null @@ -1,17 +0,0 @@ -# ~*~ coding: utf-8 ~*~ -from common.permissions import IsOrgAdminOrAppUser -from common.drf.api import JMSBulkModelViewSet -from common.utils import get_logger -from . import serializers -from .models import Stat - -logger = get_logger(__name__) -__all__ = ['StatViewSet'] - - -class StatViewSet(JMSBulkModelViewSet): - queryset = Stat.objects.all() - filter_fields = ('id', 'key', 'value', 'component') - search_fields = filter_fields - permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = serializers.StatSerializer diff --git a/apps/system/apps.py b/apps/system/apps.py deleted file mode 100644 index 5dc4d64bc..000000000 --- a/apps/system/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class SystemConfig(AppConfig): - name = 'system' diff --git a/apps/system/migrations/0001_initial.py b/apps/system/migrations/0001_initial.py deleted file mode 100644 index d4f7ad7a1..000000000 --- a/apps/system/migrations/0001_initial.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 3.1 on 2020-11-25 06:31 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Stat', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('node', models.CharField(max_length=128)), - ('ip', models.GenericIPAddressField()), - ('component', models.CharField(choices=[('core', 'Core'), ('koko', 'KoKo'), ('guacamole', 'Guacamole'), ('omnidb', 'OmniDB')], max_length=16)), - ('key', models.CharField(db_index=True, max_length=16, verbose_name='Item key')), - ('value', models.FloatField()), - ('datetime', models.DateTimeField()), - ], - ), - ] diff --git a/apps/system/migrations/__init__.py b/apps/system/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/system/models.py b/apps/system/models.py deleted file mode 100644 index 924f60521..000000000 --- a/apps/system/models.py +++ /dev/null @@ -1,68 +0,0 @@ -import time - -from django.db import models -import psutil -from django.utils import timezone - -from django.utils.translation import ugettext_lazy as _ - - -class Stat(models.Model): - class Components(models.TextChoices): - core = 'core', 'Core' - koko = 'koko', 'KoKo' - guacamole = 'guacamole', 'Guacamole' - omnidb = 'omnidb', 'OmniDB' - - class Keys(models.TextChoices): - cpu_load_1 = 'cpu_load', 'CPU load' - memory_used_percent = 'memory_used_percent', _('Memory used percent') - disk_used_percent = 'disk_used_percent', _('Disk used percent') - session_active = 'session_active', _('Session active') - session_processed = 'session_processed', _('Session processed') - - node = models.CharField(max_length=128) - ip = models.GenericIPAddressField() - component = models.CharField(choices=Components.choices, max_length=16) - key = models.CharField(db_index=True, max_length=16, verbose_name=_('Item key')) - value = models.FloatField() - datetime = models.DateTimeField() - - def __str__(self): - return f'{self.key}:{self.value}' - - @staticmethod - def collect_local_stats(): - memory_percent = psutil.virtual_memory().percent - cpu_load = psutil.getloadavg() - cpu_load_1 = round(cpu_load[0], 2) - cpu_load_5 = round(cpu_load[1], 2) - cpu_load_15 = round(cpu_load[2], 2) - cpu_percent = psutil.cpu_percent() - stats = dict( - memory_percent=memory_percent, - cpu_load_1=cpu_load_1, - cpu_load_5=cpu_load_5, - cpu_load_15=cpu_load_15, - cpu_load=cpu_load_1, - cpu_percent=cpu_percent - ) - return stats - - @classmethod - def keep_collect_local_stats(cls): - data = { - 'node': 'core-01', - 'ip': '192.168.1.1', - 'component': 'core' - } - while True: - stats = cls.collect_local_stats() - data['datetime'] = timezone.now() - items = [] - for k, v in stats.items(): - data['key'] = k - data['value'] = v - items.append(cls(**data)) - cls.objects.bulk_create(items, ignore_conflicts=True) - time.sleep(60) diff --git a/apps/system/serializers.py b/apps/system/serializers.py deleted file mode 100644 index e8ada1d15..000000000 --- a/apps/system/serializers.py +++ /dev/null @@ -1,12 +0,0 @@ -from common.drf.serializers import BulkModelSerializer - -from .models import Stat - - -class StatSerializer(BulkModelSerializer): - class Meta: - model = Stat - fields = ( - 'id', 'node', 'ip', 'component', - 'key', 'value', 'datetime', - ) diff --git a/apps/system/tests.py b/apps/system/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/apps/system/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/apps/system/urls.py b/apps/system/urls.py deleted file mode 100644 index dee2db457..000000000 --- a/apps/system/urls.py +++ /dev/null @@ -1,14 +0,0 @@ -# coding:utf-8 -from rest_framework_bulk.routes import BulkRouter - -from . import api - -app_name = 'system' - -router = BulkRouter() -router.register(r'stats', api.StatViewSet, 'stat') - -urlpatterns = [ -] - -urlpatterns += router.urls diff --git a/apps/system/views.py b/apps/system/views.py deleted file mode 100644 index 91ea44a21..000000000 --- a/apps/system/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. From 32dbab2e344075bbcb2568119fb32c3a518b028a Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Wed, 9 Dec 2020 13:44:07 +0800 Subject: [PATCH 48/57] =?UTF-8?q?perf:=20=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E5=BA=94=E7=94=A8database=E5=AD=97=E6=AE=B5=E6=B7=BB=E5=8A=A0a?= =?UTF-8?q?llow=5Fnull=3DTrue=20(#5196)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: 数据库应用database字段修改为required * perf: 数据库应用database字段添加allow_null=True Co-authored-by: Bai --- apps/applications/serializers/database_app.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/applications/serializers/database_app.py b/apps/applications/serializers/database_app.py index ac65764cd..8b983ed87 100644 --- a/apps/applications/serializers/database_app.py +++ b/apps/applications/serializers/database_app.py @@ -12,9 +12,8 @@ from .. import models class DBAttrsSerializer(serializers.Serializer): host = serializers.CharField(max_length=128, label=_('Host')) port = serializers.IntegerField(label=_('Port')) - database = serializers.CharField( - max_length=128, required=False, allow_blank=True, allow_null=True, label=_('Database') - ) + # 添加allow_null=True,兼容之前数据库中database字段为None的情况 + database = serializers.CharField(max_length=128, required=True, allow_null=True, label=_('Database')) class MySQLAttrsSerializer(DBAttrsSerializer): From 80b03e73f66d63a70a04a5c9d2f5e402d5d60efb Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 9 Dec 2020 16:27:04 +0800 Subject: [PATCH 49/57] =?UTF-8?q?feat(celery):=20=E6=B7=BB=E5=8A=A0celery?= =?UTF-8?q?=E7=9A=84health=20check=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/ops/celery/utils.py | 26 ++++++++++++++++++++ apps/ops/management/commands/check_celery.py | 20 +++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 apps/ops/management/commands/check_celery.py diff --git a/apps/ops/celery/utils.py b/apps/ops/celery/utils.py index 0c758b70e..ff5aeb1d4 100644 --- a/apps/ops/celery/utils.py +++ b/apps/ops/celery/utils.py @@ -3,6 +3,8 @@ import json import os +import redis_lock +import redis from django.conf import settings from django.utils.timezone import get_current_timezone from django.db.utils import ProgrammingError, OperationalError @@ -105,3 +107,27 @@ def get_celery_task_log_path(task_id): path = os.path.join(settings.CELERY_LOG_DIR, rel_path) os.makedirs(os.path.dirname(path), exist_ok=True) return path + + +def get_celery_status(): + from . import app + i = app.control.inspect() + ping_data = i.ping() or {} + active_nodes = [k for k, v in ping_data.items() if v.get('ok') == 'pong'] + active_queue_worker = set([n.split('@')[0] for n in active_nodes if n]) + if len(active_queue_worker) < 5: + print("Not all celery worker worked") + return False + else: + return True + + +def get_beat_status(): + CONFIG = settings.CONFIG + r = redis.Redis(host=CONFIG.REDIS_HOST, port=CONFIG.REDIS_PORT, password=CONFIG.REDIS_PASSWORD) + lock = redis_lock.Lock(r, name="beat-distribute-start-lock") + try: + locked = lock.locked() + return locked + except redis.ConnectionError: + return False diff --git a/apps/ops/management/commands/check_celery.py b/apps/ops/management/commands/check_celery.py new file mode 100644 index 000000000..41985129d --- /dev/null +++ b/apps/ops/management/commands/check_celery.py @@ -0,0 +1,20 @@ +from django.core.management.base import BaseCommand, CommandError + + +class Command(BaseCommand): + help = 'Ops manage commands' + + def add_arguments(self, parser): + parser.add_argument('check_celery', nargs='?', help='Check celery health') + + def handle(self, *args, **options): + from ops.celery.utils import get_celery_status, get_beat_status + + ok = get_celery_status() + if not ok: + raise CommandError('Celery worker unhealthy') + + ok = get_beat_status() + if not ok: + raise CommandError('Beat unhealthy') + From 7c7de96158ad2fbfe8ecb7e097acd7d0a27f7b30 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Wed, 9 Dec 2020 18:43:13 +0800 Subject: [PATCH 50/57] =?UTF-8?q?feat(login):=20=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E8=A6=81=E4=BD=93=E7=8E=B0=E7=94=A8=E5=93=AA?= =?UTF-8?q?=E4=B8=AAbackend=E7=99=BB=E5=BD=95=E7=9A=84=20#4472=20(#5199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: xinwen --- .../migrations/0011_userloginlog_backend.py | 18 +++++++++++++ apps/audits/models.py | 1 + apps/audits/serializers.py | 3 ++- apps/audits/signals_handler.py | 24 ++++++++++++++++- apps/authentication/backends/radius.py | 2 +- apps/authentication/models.py | 2 -- apps/settings/utils/ldap.py | 2 +- apps/users/forms/user.py | 2 +- apps/users/models/user.py | 26 +++++++------------ apps/users/signals_handler.py | 8 +++--- apps/users/tasks.py | 2 +- apps/users/utils.py | 11 ++++---- 12 files changed, 67 insertions(+), 34 deletions(-) create mode 100644 apps/audits/migrations/0011_userloginlog_backend.py diff --git a/apps/audits/migrations/0011_userloginlog_backend.py b/apps/audits/migrations/0011_userloginlog_backend.py new file mode 100644 index 000000000..5a708b198 --- /dev/null +++ b/apps/audits/migrations/0011_userloginlog_backend.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-12-09 03:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('audits', '0010_auto_20200811_1122'), + ] + + operations = [ + migrations.AddField( + model_name='userloginlog', + name='backend', + field=models.CharField(default='', max_length=32, verbose_name='Login backend'), + ), + ] diff --git a/apps/audits/models.py b/apps/audits/models.py index c959bc35c..2b28943bb 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -105,6 +105,7 @@ class UserLoginLog(models.Model): reason = models.CharField(default='', max_length=128, blank=True, verbose_name=_('Reason')) status = models.BooleanField(max_length=2, default=True, choices=STATUS_CHOICE, verbose_name=_('Status')) datetime = models.DateTimeField(default=timezone.now, verbose_name=_('Date login')) + backend = models.CharField(max_length=32, default='', verbose_name=_('Login backend')) @classmethod def get_login_logs(cls, date_from=None, date_to=None, user=None, keyword=None): diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index 0815226a4..74c8f598c 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -31,7 +31,8 @@ class UserLoginLogSerializer(serializers.ModelSerializer): model = models.UserLoginLog fields = ( 'id', 'username', 'type', 'type_display', 'ip', 'city', 'user_agent', - 'mfa', 'reason', 'status', 'status_display', 'datetime', 'mfa_display' + 'mfa', 'reason', 'status', 'status_display', 'datetime', 'mfa_display', + 'backend' ) extra_kwargs = { "user_agent": {'label': _('User agent')} diff --git a/apps/audits/signals_handler.py b/apps/audits/signals_handler.py index b95f6fbdf..e25bd9be9 100644 --- a/apps/audits/signals_handler.py +++ b/apps/audits/signals_handler.py @@ -5,6 +5,8 @@ from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from django.db import transaction from django.utils import timezone +from django.contrib.auth import BACKEND_SESSION_KEY +from django.utils.translation import ugettext_lazy as _ from rest_framework.renderers import JSONRenderer from rest_framework.request import Request @@ -32,6 +34,19 @@ MODELS_NEED_RECORD = ( ) +LOGIN_BACKEND = { + 'PublicKeyAuthBackend': _('SSH Key'), + 'RadiusBackend': User.Source.radius.label, + 'RadiusRealmBackend': User.Source.radius.label, + 'LDAPAuthorizationBackend': User.Source.ldap.label, + 'ModelBackend': _('Password'), + 'SSOAuthentication': _('SSO'), + 'CASBackend': User.Source.cas.label, + 'OIDCAuthCodeBackend': User.Source.openid.label, + 'OIDCAuthPasswordBackend': User.Source.openid.label, +} + + def create_operate_log(action, sender, resource): user = current_request.user if current_request else None if not user or not user.is_authenticated: @@ -109,6 +124,12 @@ def on_audits_log_create(sender, instance=None, **kwargs): sys_logger.info(msg) +def get_login_backend(request): + backend = request.session.get(BACKEND_SESSION_KEY, '') + backend = backend.rsplit('.', maxsplit=1)[-1] + return LOGIN_BACKEND.get(backend, '') + + def generate_data(username, request): user_agent = request.META.get('HTTP_USER_AGENT', '') login_ip = get_request_ip(request) or '0.0.0.0' @@ -122,7 +143,8 @@ def generate_data(username, request): 'ip': login_ip, 'type': login_type, 'user_agent': user_agent, - 'datetime': timezone.now() + 'datetime': timezone.now(), + 'backend': get_login_backend(request) } return data diff --git a/apps/authentication/backends/radius.py b/apps/authentication/backends/radius.py index 4301e17bf..6798e72f2 100644 --- a/apps/authentication/backends/radius.py +++ b/apps/authentication/backends/radius.py @@ -23,7 +23,7 @@ class CreateUserMixin: email_suffix = settings.EMAIL_SUFFIX email = '{}@{}'.format(username, email_suffix) user = User(username=username, name=username, email=email) - user.source = user.SOURCE_RADIUS + user.source = user.Source.radius.value user.save() return user diff --git a/apps/authentication/models.py b/apps/authentication/models.py index 592a43674..a205a5190 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -1,11 +1,9 @@ import uuid -from functools import partial from django.utils import timezone from django.utils.translation import ugettext_lazy as _, ugettext as __ from rest_framework.authtoken.models import Token from django.conf import settings -from django.utils.crypto import get_random_string from common.db import models from common.mixins.models import CommonModelMixin diff --git a/apps/settings/utils/ldap.py b/apps/settings/utils/ldap.py index 3193a0a73..5ca455380 100644 --- a/apps/settings/utils/ldap.py +++ b/apps/settings/utils/ldap.py @@ -333,7 +333,7 @@ class LDAPImportUtil(object): def update_or_create(self, user): user['email'] = self.get_user_email(user) if user['username'] not in ['admin']: - user['source'] = User.SOURCE_LDAP + user['source'] = User.Source.ldap.value obj, created = User.objects.update_or_create( username=user['username'], defaults=user ) diff --git a/apps/users/forms/user.py b/apps/users/forms/user.py index f3852c0dd..299862b5e 100644 --- a/apps/users/forms/user.py +++ b/apps/users/forms/user.py @@ -28,7 +28,7 @@ class UserCreateUpdateFormMixin(OrgModelForm): ) source = forms.ChoiceField( choices=get_source_choices, required=True, - initial=User.SOURCE_LOCAL, label=_("Source") + initial=User.Source.local.value, label=_("Source") ) public_key = forms.CharField( label=_('ssh public key'), max_length=5000, required=False, diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 56c979738..2dcbd452d 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -7,10 +7,10 @@ import string import random from django.conf import settings -from django.contrib.auth.hashers import make_password from django.contrib.auth.models import AbstractUser from django.core.cache import cache from django.db import models +from django.db.models import TextChoices from django.utils.translation import ugettext_lazy as _ from django.utils import timezone @@ -481,18 +481,12 @@ class MFAMixin: class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): - SOURCE_LOCAL = 'local' - SOURCE_LDAP = 'ldap' - SOURCE_OPENID = 'openid' - SOURCE_RADIUS = 'radius' - SOURCE_CAS = 'cas' - SOURCE_CHOICES = ( - (SOURCE_LOCAL, _('Local')), - (SOURCE_LDAP, 'LDAP/AD'), - (SOURCE_OPENID, 'OpenID'), - (SOURCE_RADIUS, 'Radius'), - (SOURCE_CAS, 'CAS'), - ) + class Source(TextChoices): + local = 'local', _('Local') + ldap = 'ldap', 'LDAP/AD' + openid = 'openid', 'OpenID' + radius = 'radius', 'Radius' + cas = 'cas', 'CAS' id = models.UUIDField(default=uuid.uuid4, primary_key=True) username = models.CharField( @@ -542,7 +536,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): max_length=30, default='', blank=True, verbose_name=_('Created by') ) source = models.CharField( - max_length=30, default=SOURCE_LOCAL, choices=SOURCE_CHOICES, + max_length=30, default=Source.local.value, choices=Source.choices, verbose_name=_('Source') ) date_password_last_updated = models.DateTimeField( @@ -593,7 +587,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): @property def is_local(self): - return self.source == self.SOURCE_LOCAL + return self.source == self.Source.local.value def set_unprovide_attr_if_need(self): if not self.name: @@ -663,6 +657,6 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): user.groups.add(UserGroup.initial()) def can_send_created_mail(self): - if self.email and self.source == self.SOURCE_LOCAL: + if self.email and self.source == self.Source.local.value: return True return False diff --git a/apps/users/signals_handler.py b/apps/users/signals_handler.py index a25d4ea20..09320a2e1 100644 --- a/apps/users/signals_handler.py +++ b/apps/users/signals_handler.py @@ -28,7 +28,7 @@ def on_user_create(sender, user=None, **kwargs): @receiver(cas_user_authenticated) def on_cas_user_authenticated(sender, user, created, **kwargs): if created: - user.source = user.SOURCE_CAS + user.source = user.Source.cas.value user.save() @@ -37,7 +37,7 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs): if user and user.username not in ['admin']: exists = User.objects.filter(username=user.username).exists() if not exists: - user.source = user.SOURCE_LDAP + user.source = user.Source.ldap.value user.save() @@ -46,9 +46,9 @@ def on_openid_create_or_update_user(sender, request, user, created, name, userna if created: logger.debug( "Receive OpenID user created signal: {}, " - "Set user source is: {}".format(user, User.SOURCE_OPENID) + "Set user source is: {}".format(user, User.Source.openid.value) ) - user.source = User.SOURCE_OPENID + user.source = User.Source.openid.value user.save() elif not created and settings.AUTH_OPENID_ALWAYS_UPDATE_USER: logger.debug( diff --git a/apps/users/tasks.py b/apps/users/tasks.py index f575a3afc..fc938499b 100644 --- a/apps/users/tasks.py +++ b/apps/users/tasks.py @@ -22,7 +22,7 @@ logger = get_logger(__file__) @shared_task def check_password_expired(): - users = User.objects.filter(source=User.SOURCE_LOCAL).exclude(role=User.ROLE.APP) + users = User.objects.filter(source=User.Source.local.value).exclude(role=User.ROLE.APP) for user in users: if not user.is_valid: continue diff --git a/apps/users/utils.py b/apps/users/utils.py index be33f25ba..94669c03a 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -362,18 +362,17 @@ def get_current_org_members(exclude=()): def get_source_choices(): from .models import User - choices_all = dict(User.SOURCE_CHOICES) choices = [ - (User.SOURCE_LOCAL, choices_all[User.SOURCE_LOCAL]), + (User.Source.local.value, User.Source.local.label), ] if settings.AUTH_LDAP: - choices.append((User.SOURCE_LDAP, choices_all[User.SOURCE_LDAP])) + choices.append((User.Source.ldap.value, User.Source.ldap.label)) if settings.AUTH_OPENID: - choices.append((User.SOURCE_OPENID, choices_all[User.SOURCE_OPENID])) + choices.append((User.Source.openid.value, User.Source.openid.label)) if settings.AUTH_RADIUS: - choices.append((User.SOURCE_RADIUS, choices_all[User.SOURCE_RADIUS])) + choices.append((User.Source.radius.value, User.Source.radius.label)) if settings.AUTH_CAS: - choices.append((User.SOURCE_CAS, choices_all[User.SOURCE_CAS])) + choices.append((User.Source.cas.value, User.Source.cas.label)) return choices From 79a371eb6ca1b0c28d8685320af7ed6866dcc527 Mon Sep 17 00:00:00 2001 From: xinwen Date: Wed, 9 Dec 2020 18:17:10 +0800 Subject: [PATCH 51/57] =?UTF-8?q?perf(auth):=20=E5=AF=86=E7=A0=81=E8=BF=87?= =?UTF-8?q?=E6=9C=9F=E5=90=8E=EF=BC=8C=E8=B5=B0=E9=87=8D=E7=BD=AE=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E6=B5=81=E7=A8=8B=20#530?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/errors.py | 11 +- apps/authentication/mixins.py | 41 ++-- apps/authentication/urls/view_urls.py | 1 + apps/authentication/views/login.py | 19 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 62514 -> 62680 bytes apps/locale/zh/LC_MESSAGES/django.po | 274 ++++++++++++++------------ 6 files changed, 202 insertions(+), 144 deletions(-) diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index 26363363e..8cea830e1 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -218,5 +218,14 @@ class PasswdTooSimple(JMSException): default_detail = _('Your password is too simple, please change it for security') def __init__(self, url, *args, **kwargs): - super(PasswdTooSimple, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) + self.url = url + + +class PasswordRequireResetError(JMSException): + default_code = 'passwd_has_expired' + default_detail = _('Your password has expired, please reset before logging in') + + def __init__(self, url, *args, **kwargs): + super().__init__(*args, **kwargs) self.url = url diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 1d9335743..87b989283 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -110,9 +110,8 @@ class AuthMixin: raise CredentialError(error=errors.reason_user_inactive) elif not user.is_active: raise CredentialError(error=errors.reason_user_inactive) - elif user.password_has_expired: - raise CredentialError(error=errors.reason_password_expired) + self._check_password_require_reset_or_not(user) self._check_passwd_is_too_simple(user, password) clean_failed_count(username, ip) @@ -123,20 +122,34 @@ class AuthMixin: return user @classmethod - def _check_passwd_is_too_simple(cls, user, password): + def generate_reset_password_url_with_flash_msg(cls, user: User, flash_view_name): + reset_passwd_url = reverse('authentication:reset-password') + query_str = urlencode({ + 'token': user.generate_reset_token() + }) + reset_passwd_url = f'{reset_passwd_url}?{query_str}' + + flash_page_url = reverse(flash_view_name) + query_str = urlencode({ + 'redirect_url': reset_passwd_url + }) + return f'{flash_page_url}?{query_str}' + + @classmethod + def _check_passwd_is_too_simple(cls, user: User, password): if user.is_superuser and password == 'admin': - reset_passwd_url = reverse('authentication:reset-password') - query_str = urlencode({ - 'token': user.generate_reset_token() - }) - reset_passwd_url = f'{reset_passwd_url}?{query_str}' + url = cls.generate_reset_password_url_with_flash_msg( + user, 'authentication:passwd-too-simple-flash-msg' + ) + raise errors.PasswdTooSimple(url) - flash_page_url = reverse('authentication:passwd-too-simple-flash-msg') - query_str = urlencode({ - 'redirect_url': reset_passwd_url - }) - - raise errors.PasswdTooSimple(f'{flash_page_url}?{query_str}') + @classmethod + def _check_password_require_reset_or_not(cls, user: User): + if user.password_has_expired: + url = cls.generate_reset_password_url_with_flash_msg( + user, 'authentication:passwd-has-expired-flash-msg' + ) + raise errors.PasswordRequireResetError(url) def check_user_auth_if_need(self, decrypt_passwd=False): request = self.request diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index 467e32d0d..a95342fa6 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -22,6 +22,7 @@ urlpatterns = [ name='forgot-password-sendmail-success'), path('password/reset/', users_view.UserResetPasswordView.as_view(), name='reset-password'), path('password/too-simple-flash-msg/', views.FlashPasswdTooSimpleMsgView.as_view(), name='passwd-too-simple-flash-msg'), + path('password/has-expired-msg/', views.FlashPasswdHasExpiredMsgView.as_view(), name='passwd-has-expired-flash-msg'), path('password/reset/success/', users_view.UserResetPasswordSuccessView.as_view(), name='reset-password-success'), path('password/verify/', users_view.UserVerifyPasswordView.as_view(), name='user-verify-password'), diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 4210315c5..dcff66905 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -32,7 +32,7 @@ from ..forms import get_user_login_form_cls __all__ = [ 'UserLoginView', 'UserLogoutView', 'UserLoginGuardView', 'UserLoginWaitConfirmView', - 'FlashPasswdTooSimpleMsgView', + 'FlashPasswdTooSimpleMsgView', 'FlashPasswdHasExpiredMsgView' ] @@ -96,7 +96,7 @@ class UserLoginView(mixins.AuthMixin, FormView): new_form._errors = form.errors context = self.get_context_data(form=new_form) return self.render_to_response(context) - except errors.PasswdTooSimple as e: + except (errors.PasswdTooSimple, errors.PasswordRequireResetError) as e: return redirect(e.url) self.clear_rsa_key() return self.redirect_to_guard_view() @@ -250,3 +250,18 @@ class FlashPasswdTooSimpleMsgView(TemplateView): 'auto_redirect': True, } return self.render_to_response(context) + + +@method_decorator(never_cache, name='dispatch') +class FlashPasswdHasExpiredMsgView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + context = { + 'title': _('Please change your password'), + 'messages': _('Your password has expired, please reset before logging in'), + 'interval': 5, + 'redirect_url': request.GET.get('redirect_url'), + 'auto_redirect': True, + } + return self.render_to_response(context) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 801d192a25d63277c13e07a5f7992a9312c557b3..211388dd871be071493b0820f3e599a00cd8b372 100644 GIT binary patch delta 18370 zcmZA91$dTa`^WKnBS(xFwT;+_!C-X5h|%3hH)E6_CC!6$3OKsE1Syq9iHXt(2);;& zG)VZOpdkLgzg;Kq;qkwZUm=^Q!~%I%2M7uDyQiY}`C5Bka;%7H zu^OhvCYS}=V>k{n=V315-589Qk^AwUV{!Zg3u6K9CY<%X_ozgY7=}4w7uVY9%&r64YVk!)1&mChXGu$kK+Hge-!rGV#+hGdU_dcMafy1oBMARKG zQU_dV@kZ1MY(p)45VPWG)c89XfWIM6(|dy{(d*#m3BdG(8BiO@jXv$XEEQP|HE~1K zLLD#y`=Czf6YP%%uq;M(bp5)c795BL@I%axD=--zN6mK{b)r`>1E%W4`D=&SJGq_a zLmgES)J`j)o^gHD9d^Zx*dKK=<1iOaLmlmA)Ix_)&-@f>1LsleT( zJDCHVK5x7w=AoY92Gld&k2SB3) z{@YQ}oz24nxD<5~38*7Hjhgrx>Q3&Wj{GGCW5!sw(J0hLtD)ZV2B>*PVp1HBI*}=; zaVs&9^}T&m^m+UawcvG3hEGr%dx3gHuTk$rKo2)@7Sx^QM2(BIe0j`G9D};k?ih$4 zqE2`ss{bPNsj`WR?r<+^$45|)-~?)dYnFeC+UQHvglXd3iG-p~B+BC2n2oqK>Jbh= zz3r1RCoabncrcFh*XJ;SgzoITHC#9EqK@>5wZFn};$%JDryxHjCvJzK*cH`(3~Jsf zs2iAzI;r)j{yR`NzNaVWucQ5zga-a#-o!BChnNym_i}fh2{myf>L^R2?yN5A8FxnA zNGxjHK#NDB)|rADKOeP$)jldZ%AKek9!4GIDU0u60P%0AiQk|m4(RRfEF-2M&WoBb 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 P+JEQZyj@qGxAp%&BY+-P5)n{(5$Pp}^r{F5 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 NQGZ1z`TlAV@PAe=N3;L{ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index cefa4bf2a..7ddf14999 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-11-17 17:24+0800\n" +"POT-Creation-Date: 2020-12-09 18:14+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -17,22 +17,22 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: applications/const.py:53 applications/models/application.py:34 +#: applications/const.py:53 applications/models/application.py:33 msgid "Custom" msgstr "自定义" -#: applications/models/application.py:61 applications/models/database_app.py:29 +#: applications/models/application.py:60 applications/models/database_app.py:29 #: applications/serializers/database_app.py:16 #: applications/serializers/remote_app.py:69 #: users/templates/users/user_granted_database_app.html:37 msgid "Database" msgstr "数据库" -#: applications/models/application.py:62 +#: applications/models/application.py:61 msgid "Remote app" msgstr "远程应用" -#: applications/models/application.py:122 +#: 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 #: assets/models/base.py:234 assets/models/cluster.py:18 @@ -41,7 +41,7 @@ msgstr "远程应用" #: orgs/models.py:23 perms/models/base.py:48 settings/models.py:27 #: terminal/models.py:28 terminal/models.py:372 terminal/models.py:404 #: terminal/models.py:441 users/forms/profile.py:20 users/models/group.py:15 -#: users/models/user.py:505 users/templates/users/_select_user_modal.html:13 +#: users/models/user.py:501 users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 #: users/templates/users/user_database_app_permission.html:36 @@ -58,12 +58,12 @@ msgstr "远程应用" msgid "Name" msgstr "名称" -#: applications/models/application.py:123 assets/models/asset.py:198 +#: 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:124 +#: applications/models/application.py:123 #: applications/serializers/application.py:16 assets/models/label.py:21 #: perms/models/application_permission.py:19 #: perms/serializers/application/permission.py:16 @@ -71,7 +71,7 @@ msgstr "网域" msgid "Category" msgstr "分类" -#: applications/models/application.py:125 +#: 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 @@ -84,7 +84,7 @@ msgstr "类型" # msgid "Date created" # msgstr "创建日期" -#: applications/models/application.py:128 +#: 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 #: assets/models/asset.py:230 assets/models/base.py:239 @@ -94,15 +94,15 @@ msgstr "类型" #: 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.py:38 #: terminal/models.py:411 terminal/models.py:448 tickets/models/ticket.py:43 -#: users/models/group.py:16 users/models/user.py:538 +#: users/models/group.py:16 users/models/user.py:534 #: 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 #: users/templates/users/user_group_list.html:16 #: users/templates/users/user_profile.html:138 -#: xpack/plugins/change_auth_plan/models.py:77 xpack/plugins/cloud/models.py:54 -#: xpack/plugins/cloud/models.py:149 xpack/plugins/gathered_user/models.py:26 +#: xpack/plugins/change_auth_plan/models.py:77 xpack/plugins/cloud/models.py:55 +#: xpack/plugins/cloud/models.py:150 xpack/plugins/gathered_user/models.py:26 msgid "Comment" msgstr "备注" @@ -114,9 +114,9 @@ msgstr "主机" #: applications/models/database_app.py:27 #: applications/serializers/database_app.py:14 -#: applications/serializers/database_app.py:21 -#: applications/serializers/database_app.py:25 -#: applications/serializers/database_app.py:29 +#: 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" @@ -159,7 +159,7 @@ msgstr "Kubernetes应用" #: users/templates/users/user_asset_permission.html:70 #: users/templates/users/user_granted_remote_app.html:36 #: xpack/plugins/change_auth_plan/models.py:282 -#: xpack/plugins/cloud/models.py:278 +#: xpack/plugins/cloud/models.py:279 msgid "Asset" msgstr "资产" @@ -182,10 +182,10 @@ msgstr "参数" #: 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:546 users/serializers/group.py:35 +#: users/models/user.py:542 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:57 -#: xpack/plugins/cloud/models.py:155 xpack/plugins/gathered_user/models.py:30 +#: 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 "创建者" @@ -198,7 +198,7 @@ msgstr "创建者" #: 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:60 xpack/plugins/cloud/models.py:158 +#: xpack/plugins/cloud/models.py:61 xpack/plugins/cloud/models.py:159 msgid "Date created" msgstr "创建日期" @@ -211,7 +211,7 @@ msgstr "创建日期" msgid "RemoteApp" msgstr "远程应用" -#: applications/serializers/database_app.py:50 +#: 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" @@ -237,7 +237,7 @@ msgstr "目标URL" #: 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:503 +#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:499 #: users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:53 #: users/templates/users/user_list.html:15 @@ -300,6 +300,10 @@ msgid "You can't update the root node name" msgstr "不能修改根节点名称" #: assets/api/node.py:65 +msgid "You can't delete the root node ({})" +msgstr "不能删除根节点 ({})" + +#: assets/api/node.py:68 msgid "Deletion failed and the node contains children or assets" msgstr "删除失败,节点包含子节点或资产" @@ -334,7 +338,7 @@ 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:63 +#: tickets/api/request_asset_perm.py:67 #: tickets/serializers/request_asset_perm.py:25 #: users/templates/users/_granted_assets.html:25 #: users/templates/users/user_asset_permission.html:157 @@ -366,7 +370,7 @@ 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:142 xpack/plugins/cloud/serializers.py:84 +#: xpack/plugins/cloud/models.py:143 xpack/plugins/cloud/serializers.py:115 msgid "Admin user" msgstr "管理用户" @@ -480,7 +484,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:524 +#: assets/models/cluster.py:22 users/models/user.py:520 #: users/templates/users/user_detail.html:62 msgid "Phone" msgstr "手机" @@ -506,7 +510,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:665 +#: users/models/user.py:661 msgid "System" msgstr "系统" @@ -617,7 +621,7 @@ msgstr "默认资产组" #: tickets/models/ticket.py:30 tickets/models/ticket.py:136 #: tickets/serializers/request_asset_perm.py:66 #: tickets/serializers/ticket.py:31 users/forms/group.py:15 -#: users/models/user.py:159 users/models/user.py:653 +#: users/models/user.py:159 users/models/user.py:649 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -631,7 +635,7 @@ msgstr "默认资产组" msgid "User" msgstr "用户" -#: assets/models/label.py:19 assets/models/node.py:398 settings/models.py:28 +#: assets/models/label.py:19 assets/models/node.py:401 settings/models.py:28 msgid "Value" msgstr "值" @@ -643,24 +647,24 @@ msgstr "新节点" msgid "empty" msgstr "空" -#: assets/models/node.py:397 perms/models/asset_permission.py:144 +#: assets/models/node.py:400 perms/models/asset_permission.py:144 msgid "Key" msgstr "键" -#: assets/models/node.py:399 +#: assets/models/node.py:402 msgid "Full value" msgstr "全称" -#: assets/models/node.py:402 perms/models/asset_permission.py:148 +#: assets/models/node.py:405 perms/models/asset_permission.py:148 msgid "Parent key" msgstr "ssh私钥" -#: assets/models/node.py:411 assets/serializers/system_user.py:190 +#: assets/models/node.py:414 assets/serializers/system_user.py:190 #: perms/forms/asset_permission.py:92 perms/forms/asset_permission.py:99 #: 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:138 xpack/plugins/cloud/serializers.py:85 +#: xpack/plugins/cloud/models.py:139 xpack/plugins/cloud/serializers.py:116 msgid "Node" msgstr "节点" @@ -732,7 +736,7 @@ msgstr "用户组" #: perms/models/remote_app_permission.py:16 templates/_nav.html:45 #: terminal/backends/command/models.py:20 #: terminal/backends/command/serializers.py:14 terminal/models.py:194 -#: tickets/api/request_asset_perm.py:64 +#: tickets/api/request_asset_perm.py:68 #: tickets/serializers/request_asset_perm.py:27 #: users/templates/users/_granted_assets.html:27 #: users/templates/users/user_asset_permission.html:42 @@ -782,15 +786,15 @@ msgstr "管理用户名称" msgid "Nodes name" msgstr "节点名称" -#: assets/serializers/asset.py:110 +#: assets/serializers/asset.py:107 msgid "Hardware info" msgstr "硬件信息" -#: assets/serializers/asset.py:111 orgs/mixins/serializers.py:26 +#: assets/serializers/asset.py:108 orgs/mixins/serializers.py:26 msgid "Org name" msgstr "组织名称" -#: assets/serializers/asset.py:165 assets/serializers/asset.py:196 +#: assets/serializers/asset.py:162 assets/serializers/asset.py:193 msgid "Connectivity" msgstr "连接" @@ -805,14 +809,14 @@ msgid "Backend" msgstr "后端" #: assets/serializers/asset_user.py:75 users/forms/profile.py:148 -#: users/models/user.py:535 users/templates/users/user_password_update.html:48 +#: users/models/user.py:531 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:532 +#: assets/serializers/asset_user.py:79 users/models/user.py:528 msgid "Private key" msgstr "ssh私钥" @@ -937,20 +941,20 @@ msgstr "收集资产上的用户" msgid "System user is dynamic: {}" msgstr "系统用户是动态的: {}" -#: assets/tasks/push_system_user.py:215 +#: assets/tasks/push_system_user.py:224 msgid "Start push system user for platform: [{}]" msgstr "推送系统用户到平台: [{}]" -#: assets/tasks/push_system_user.py:216 +#: assets/tasks/push_system_user.py:225 #: assets/tasks/system_user_connectivity.py:81 msgid "Hosts count: {}" msgstr "主机数量: {}" -#: assets/tasks/push_system_user.py:235 assets/tasks/push_system_user.py:253 +#: assets/tasks/push_system_user.py:264 assets/tasks/push_system_user.py:290 msgid "Push system users to assets: {}" msgstr "推送系统用户到入资产: {}" -#: assets/tasks/push_system_user.py:243 +#: assets/tasks/push_system_user.py:276 msgid "Push system users to asset: {}({}) => {}" msgstr "推送系统用户到入资产: {}({}) => {}" @@ -1105,7 +1109,7 @@ msgstr "启用" msgid "-" msgstr "" -#: audits/models.py:96 xpack/plugins/cloud/models.py:213 +#: audits/models.py:96 xpack/plugins/cloud/models.py:214 msgid "Failed" msgstr "失败" @@ -1128,20 +1132,20 @@ 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:527 -#: users/serializers/user.py:228 users/templates/users/user_detail.html:77 +#: users/forms/profile.py:52 users/models/user.py:523 +#: users/serializers/user.py:232 users/templates/users/user_detail.html:77 #: users/templates/users/user_profile.html:87 msgid "MFA" msgstr "多因子认证" #: audits/models.py:105 xpack/plugins/change_auth_plan/models.py:303 -#: xpack/plugins/cloud/models.py:226 +#: xpack/plugins/cloud/models.py:227 msgid "Reason" msgstr "原因" #: audits/models.py:106 tickets/serializers/request_asset_perm.py:64 -#: tickets/serializers/ticket.py:29 xpack/plugins/cloud/models.py:223 -#: xpack/plugins/cloud/models.py:281 +#: tickets/serializers/ticket.py:29 xpack/plugins/cloud/models.py:224 +#: xpack/plugins/cloud/models.py:282 msgid "Status" msgstr "状态" @@ -1167,7 +1171,7 @@ msgid "Is success" msgstr "是否成功" #: audits/serializers.py:76 ops/models/command.py:24 -#: xpack/plugins/cloud/models.py:221 +#: xpack/plugins/cloud/models.py:222 msgid "Result" msgstr "结果" @@ -1327,6 +1331,10 @@ msgstr "SSO 认证关闭了" msgid "Your password is too simple, please change it for security" msgstr "你的密码过于简单,为了安全,请修改" +#: authentication/errors.py:227 authentication/views/login.py:262 +msgid "Your password has expired, please reset before logging in" +msgstr "您的密码已过期,请先修改再登录" + #: authentication/forms.py:26 authentication/forms.py:34 #: authentication/templates/authentication/login.html:39 #: authentication/templates/authentication/xpack_login.html:119 @@ -1389,7 +1397,7 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:425 users/serializers/user.py:225 +#: users/models/user.py:421 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 @@ -1398,7 +1406,7 @@ msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:426 users/serializers/user.py:226 +#: users/models/user.py:422 users/serializers/user.py:230 #: users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:170 msgid "Enable" @@ -1538,7 +1546,7 @@ msgstr "退出登录成功" msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/login.py:246 +#: authentication/views/login.py:246 authentication/views/login.py:261 msgid "Please change your password" msgstr "请修改密码" @@ -1556,10 +1564,9 @@ msgstr "%(name)s 更新成功" msgid "Updated by" msgstr "更新人" -#: common/drf/parsers/csv.py:22 -#, python-format -msgid "The max size of CSV is %d bytes" -msgstr "CSV 文件最大为 %d 字节" +#: common/drf/parsers/base.py:16 +msgid "The file content overflowed (The maximum length `{}` bytes)" +msgstr "" #: common/exceptions.py:15 #, python-format @@ -1838,7 +1845,7 @@ 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:80 orgs/serializers.py:91 +#: orgs/models.py:395 orgs/serializers.py:79 msgid "Organization" msgstr "组织" @@ -1850,7 +1857,7 @@ msgstr "组织管理员" msgid "Organization auditor" msgstr "组织审计员" -#: orgs/models.py:397 users/forms/user.py:27 users/models/user.py:515 +#: orgs/models.py:397 users/forms/user.py:27 users/models/user.py:511 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:73 #: users/templates/users/user_list.html:16 @@ -1883,7 +1890,7 @@ msgstr "提示:RDP 协议不支持单独控制上传或下载文件" #: 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:511 users/templates/users/_select_user_modal.html:16 +#: users/models/user.py:507 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 @@ -1957,7 +1964,7 @@ msgid "Asset permission" msgstr "资产授权" #: perms/models/base.py:53 tickets/serializers/request_asset_perm.py:31 -#: users/models/user.py:543 users/templates/users/user_detail.html:93 +#: users/models/user.py:539 users/templates/users/user_detail.html:93 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" @@ -1982,7 +1989,7 @@ msgid "" "permission type. ({})" msgstr "应用列表中包含与授权类型不同的应用。({})" -#: perms/serializers/asset/permission.py:58 users/serializers/user.py:76 +#: perms/serializers/asset/permission.py:58 users/serializers/user.py:80 msgid "Is expired" msgstr "是否过期" @@ -1991,7 +1998,7 @@ msgstr "是否过期" #: 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:75 +#: perms/serializers/remote_app_permission.py:36 users/serializers/user.py:79 msgid "Is valid" msgstr "账户是否有效" @@ -2040,7 +2047,7 @@ msgstr "远程应用数量" msgid "Favorite" msgstr "收藏夹" -#: perms/utils/asset/user_permission.py:526 +#: perms/utils/asset/user_permission.py:522 msgid "Please wait while your data is being initialized" msgstr "数据正在初始化,请稍等" @@ -2849,46 +2856,46 @@ msgstr "" msgid "Ticket has %s" msgstr "工单已%s" -#: tickets/api/request_asset_perm.py:62 +#: tickets/api/request_asset_perm.py:66 #: tickets/serializers/request_asset_perm.py:23 msgid "IP group" msgstr "IP组" -#: tickets/api/request_asset_perm.py:65 +#: tickets/api/request_asset_perm.py:69 #: tickets/serializers/request_asset_perm.py:35 msgid "Confirmed assets" msgstr "确认的资产" -#: tickets/api/request_asset_perm.py:66 +#: tickets/api/request_asset_perm.py:70 msgid "Confirmed system users" msgstr "确认的系统用户" -#: tickets/api/request_asset_perm.py:87 +#: tickets/api/request_asset_perm.py:91 msgid "Confirm assets first" msgstr "请先确认资产" -#: tickets/api/request_asset_perm.py:90 +#: tickets/api/request_asset_perm.py:94 msgid "Confirmed assets changed" msgstr "确认的资产变更了" -#: tickets/api/request_asset_perm.py:94 +#: tickets/api/request_asset_perm.py:98 msgid "Confirm system-users first" msgstr "请先确认系统用户" -#: tickets/api/request_asset_perm.py:98 +#: tickets/api/request_asset_perm.py:102 msgid "Confirmed system-users changed" msgstr "确认的系统用户变更了" -#: tickets/api/request_asset_perm.py:104 tickets/api/request_asset_perm.py:111 -#: xpack/plugins/cloud/models.py:214 +#: 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:118 +#: tickets/api/request_asset_perm.py:122 msgid "From request ticket: {} {}" msgstr "来自工单申请: {} {}" -#: tickets/api/request_asset_perm.py:120 +#: tickets/api/request_asset_perm.py:124 msgid "{} request assets, approved by {}" msgstr "{} 申请资产,通过人 {}" @@ -3063,7 +3070,7 @@ msgstr "" " \n" " " -#: users/api/user.py:190 +#: users/api/user.py:199 msgid "Could not reset self otp, use profile reset instead" msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置" @@ -3109,7 +3116,7 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:89 users/models/user.py:507 +#: users/forms/profile.py:89 users/models/user.py:503 #: users/templates/users/user_detail.html:57 #: users/templates/users/user_profile.html:59 msgid "Email" @@ -3145,12 +3152,12 @@ msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" #: users/forms/profile.py:137 users/forms/user.py:90 -#: users/serializers/user.py:188 users/serializers/user.py:270 -#: users/serializers/user.py:328 +#: users/serializers/user.py:192 users/serializers/user.py:274 +#: users/serializers/user.py:332 msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/user.py:31 users/models/user.py:550 +#: users/forms/user.py:31 users/models/user.py:546 #: users/templates/users/user_detail.html:89 #: users/templates/users/user_list.html:18 #: users/templates/users/user_profile.html:102 @@ -3192,31 +3199,31 @@ msgstr "系统管理员" msgid "System auditor" msgstr "系统审计员" -#: users/models/user.py:427 users/templates/users/user_profile.html:90 +#: users/models/user.py:423 users/templates/users/user_profile.html:90 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:494 +#: users/models/user.py:490 msgid "Local" msgstr "数据库" -#: users/models/user.py:518 +#: users/models/user.py:514 msgid "Avatar" msgstr "头像" -#: users/models/user.py:521 users/templates/users/user_detail.html:68 +#: users/models/user.py:517 users/templates/users/user_detail.html:68 msgid "Wechat" msgstr "微信" -#: users/models/user.py:554 +#: users/models/user.py:550 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:661 +#: users/models/user.py:657 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:664 +#: users/models/user.py:660 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -3236,55 +3243,55 @@ msgstr "是否可更新" msgid "Can delete" msgstr "是否可删除" -#: users/serializers/user.py:49 users/serializers/user.py:81 +#: users/serializers/user.py:49 users/serializers/user.py:85 msgid "Organization role name" msgstr "组织角色名称" -#: users/serializers/user.py:74 users/serializers/user.py:241 +#: users/serializers/user.py:78 users/serializers/user.py:245 msgid "Is first login" msgstr "首次登录" -#: users/serializers/user.py:77 +#: users/serializers/user.py:81 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:79 +#: users/serializers/user.py:83 msgid "Groups name" msgstr "用户组名" -#: users/serializers/user.py:80 +#: users/serializers/user.py:84 msgid "Source name" msgstr "用户来源名" -#: users/serializers/user.py:82 +#: users/serializers/user.py:86 msgid "Super role name" msgstr "超级角色名称" -#: users/serializers/user.py:83 +#: users/serializers/user.py:87 msgid "Total role name" msgstr "汇总角色名称" -#: users/serializers/user.py:84 +#: users/serializers/user.py:88 msgid "MFA enabled" msgstr "是否开启多因子认证" -#: users/serializers/user.py:85 +#: users/serializers/user.py:89 msgid "MFA force enabled" msgstr "强制启用多因子认证" -#: users/serializers/user.py:108 +#: users/serializers/user.py:112 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/user.py:120 users/serializers/user.py:294 +#: users/serializers/user.py:124 users/serializers/user.py:298 msgid "Password does not match security rules" msgstr "密码不满足安全规则" -#: users/serializers/user.py:286 +#: users/serializers/user.py:290 msgid "The old password is incorrect" msgstr "旧密码错误" -#: users/serializers/user.py:300 +#: users/serializers/user.py:304 msgid "The newly set password is inconsistent" msgstr "两次密码不一致" @@ -3298,7 +3305,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:124 xpack/plugins/cloud/serializers.py:83 +#: xpack/plugins/cloud/models.py:125 xpack/plugins/cloud/serializers.py:114 msgid "Account" msgstr "账户" @@ -3462,7 +3469,7 @@ msgstr "很强" #: users/templates/users/user_database_app_permission.html:41 #: users/templates/users/user_list.html:19 #: users/templates/users/user_remote_app_permission.html:41 -#: xpack/plugins/cloud/models.py:51 +#: xpack/plugins/cloud/models.py:52 msgid "Validity" msgstr "有效" @@ -4240,75 +4247,75 @@ msgstr "" msgid "Access key secret" msgstr "" -#: xpack/plugins/cloud/models.py:65 +#: xpack/plugins/cloud/models.py:66 msgid "Cloud account" msgstr "云账号" -#: xpack/plugins/cloud/models.py:120 +#: xpack/plugins/cloud/models.py:121 msgid "Instance name" msgstr "实例名称" -#: xpack/plugins/cloud/models.py:121 +#: xpack/plugins/cloud/models.py:122 msgid "Instance name and Partial IP" msgstr "实例名称和部分IP" -#: xpack/plugins/cloud/models.py:127 xpack/plugins/cloud/serializers.py:59 +#: xpack/plugins/cloud/models.py:128 xpack/plugins/cloud/serializers.py:90 msgid "Regions" msgstr "地域" -#: xpack/plugins/cloud/models.py:130 +#: xpack/plugins/cloud/models.py:131 msgid "Instances" msgstr "实例" -#: xpack/plugins/cloud/models.py:134 +#: xpack/plugins/cloud/models.py:135 msgid "Hostname strategy" msgstr "主机名策略" -#: xpack/plugins/cloud/models.py:146 xpack/plugins/cloud/serializers.py:87 +#: xpack/plugins/cloud/models.py:147 xpack/plugins/cloud/serializers.py:118 msgid "Always update" msgstr "总是更新" -#: xpack/plugins/cloud/models.py:152 +#: xpack/plugins/cloud/models.py:153 msgid "Date last sync" msgstr "最后同步日期" -#: xpack/plugins/cloud/models.py:163 xpack/plugins/cloud/models.py:219 +#: xpack/plugins/cloud/models.py:164 xpack/plugins/cloud/models.py:220 msgid "Sync instance task" msgstr "同步实例任务" -#: xpack/plugins/cloud/models.py:229 xpack/plugins/cloud/models.py:284 +#: xpack/plugins/cloud/models.py:230 xpack/plugins/cloud/models.py:285 msgid "Date sync" msgstr "同步日期" -#: xpack/plugins/cloud/models.py:257 +#: xpack/plugins/cloud/models.py:258 msgid "Unsync" msgstr "未同步" -#: xpack/plugins/cloud/models.py:258 +#: xpack/plugins/cloud/models.py:259 msgid "New Sync" msgstr "新同步" -#: xpack/plugins/cloud/models.py:259 +#: xpack/plugins/cloud/models.py:260 msgid "Synced" msgstr "已同步" -#: xpack/plugins/cloud/models.py:260 +#: xpack/plugins/cloud/models.py:261 msgid "Released" msgstr "已释放" -#: xpack/plugins/cloud/models.py:265 +#: xpack/plugins/cloud/models.py:266 msgid "Sync task" msgstr "同步任务" -#: xpack/plugins/cloud/models.py:269 +#: xpack/plugins/cloud/models.py:270 msgid "Sync instance task history" msgstr "同步实例任务历史" -#: xpack/plugins/cloud/models.py:272 +#: xpack/plugins/cloud/models.py:273 msgid "Instance" msgstr "实例" -#: xpack/plugins/cloud/models.py:275 +#: xpack/plugins/cloud/models.py:276 msgid "Region" msgstr "地域" @@ -4324,6 +4331,10 @@ msgstr "AWS (国际)" msgid "AWS (China)" msgstr "AWS (中国)" +#: xpack/plugins/cloud/providers/azure_.py:18 +msgid "Azure (China)" +msgstr "Azure (中国)" + #: xpack/plugins/cloud/providers/huaweicloud.py:20 msgid "Huawei Cloud" msgstr "华为云" @@ -4384,15 +4395,23 @@ msgstr "拉美-圣地亚哥" msgid "Tencent Cloud" msgstr "腾讯云" -#: xpack/plugins/cloud/serializers.py:57 +#: xpack/plugins/cloud/serializers.py:26 +msgid "Tenant ID" +msgstr "" + +#: xpack/plugins/cloud/serializers.py:30 +msgid "Subscription ID" +msgstr "" + +#: xpack/plugins/cloud/serializers.py:88 msgid "History count" msgstr "执行次数" -#: xpack/plugins/cloud/serializers.py:58 +#: xpack/plugins/cloud/serializers.py:89 msgid "Instance count" msgstr "实例个数" -#: xpack/plugins/cloud/serializers.py:86 +#: xpack/plugins/cloud/serializers.py:117 #: xpack/plugins/gathered_user/serializers.py:20 msgid "Periodic display" msgstr "定时执行" @@ -4485,14 +4504,15 @@ msgstr "旗舰版" 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 "Azure (China)" -#~ msgstr "Azure (中国)" - #~ msgid "MFA level" #~ msgstr "多因子认证级别" From 5863e3e0083b25152a60af6ff813fc67ce58734a Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Thu, 10 Dec 2020 17:12:39 +0800 Subject: [PATCH 52/57] =?UTF-8?q?perf(asset):=20=E8=B5=84=E4=BA=A7?= =?UTF-8?q?=E6=A0=91=EF=BC=8C=E5=8F=B3=E5=87=BB=E5=A2=9E=E5=8A=A0=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E8=8A=82=E7=82=B9=E6=95=B0=E9=87=8F=E7=9A=84=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=EF=BC=8C=E5=8F=AF=E4=BB=A5=E8=AE=A9=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E5=8E=BB=E8=AE=A1=E7=AE=97=20#527=20(#5207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: xinwen --- apps/assets/api/node.py | 8 ++ apps/assets/tasks/nodes_amount.py | 12 ++- apps/assets/utils.py | 2 + apps/common/utils/inspect.py | 10 +++ apps/common/utils/lock.py | 55 ++++++++++++++ apps/locale/zh/LC_MESSAGES/django.mo | Bin 62680 -> 62898 bytes apps/locale/zh/LC_MESSAGES/django.po | 106 ++++++++++++++++----------- 7 files changed, 146 insertions(+), 47 deletions(-) create mode 100644 apps/common/utils/inspect.py create mode 100644 apps/common/utils/lock.py diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 096c65939..b1e01c9c1 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -5,11 +5,13 @@ from collections import namedtuple, defaultdict from rest_framework import status from rest_framework.serializers import ValidationError from rest_framework.response import Response +from rest_framework.decorators import action from django.utils.translation import ugettext_lazy as _ from django.shortcuts import get_object_or_404, Http404 from django.utils.decorators import method_decorator from django.db.models.signals import m2m_changed +from common.const.http import POST from common.exceptions import SomeoneIsDoingThis from common.const.signals import PRE_REMOVE, POST_REMOVE from assets.models import Asset @@ -19,6 +21,7 @@ from common.const.distributed_lock_key import UPDATE_NODE_TREE_LOCK_KEY from orgs.mixins.api import OrgModelViewSet from orgs.mixins import generics from orgs.lock import org_level_transaction_lock +from assets.tasks import check_node_assets_amount_period_task from ..hands import IsOrgAdmin from ..models import Node from ..tasks import ( @@ -46,6 +49,11 @@ class NodeViewSet(OrgModelViewSet): permission_classes = (IsOrgAdmin,) serializer_class = serializers.NodeSerializer + @action(methods=[POST], detail=False, url_name='launch-check-assets-amount-task') + def launch_check_assets_amount_task(self, request): + task = check_node_assets_amount_period_task.delay() + return Response(data={'task': task.id}) + # 仅支持根节点指直接创建,子节点下的节点需要通过children接口创建 def perform_create(self, serializer): child_key = Node.org_root().get_next_child_key() diff --git a/apps/assets/tasks/nodes_amount.py b/apps/assets/tasks/nodes_amount.py index 3ae191788..cd929d131 100644 --- a/apps/assets/tasks/nodes_amount.py +++ b/apps/assets/tasks/nodes_amount.py @@ -1,13 +1,19 @@ from celery import shared_task +from django.utils.translation import gettext_lazy as _ from ops.celery.decorator import register_as_period_task from assets.utils import check_node_assets_amount + +from common.utils.lock import AcquireFailed from common.utils import get_logger logger = get_logger(__file__) -@register_as_period_task(crontab='0 2 * * *') @shared_task(queue='celery_heavy_tasks') -def check_node_assets_amount_celery_task(): - check_node_assets_amount() +@register_as_period_task(crontab='0 2 * * *') +def check_node_assets_amount_period_task(): + try: + check_node_assets_amount() + except AcquireFailed: + logger.error(_('The task of self-checking is already running and cannot be started repeatedly')) diff --git a/apps/assets/utils.py b/apps/assets/utils.py index c4c112c16..2805ac034 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils.py @@ -5,6 +5,7 @@ import time from django.db.models import Q from common.utils import get_logger, dict_get_any, is_uuid, get_object_or_none +from common.utils.lock import DistributedLock from common.http import is_true from .models import Asset, Node @@ -12,6 +13,7 @@ from .models import Asset, Node logger = get_logger(__file__) +@DistributedLock(name="assets.node.check_node_assets_amount", blocking=False) def check_node_assets_amount(): for node in Node.objects.all(): logger.info(f'Check node assets amount: {node}') diff --git a/apps/common/utils/inspect.py b/apps/common/utils/inspect.py new file mode 100644 index 000000000..650d3b434 --- /dev/null +++ b/apps/common/utils/inspect.py @@ -0,0 +1,10 @@ +import inspect + + +def copy_function_args(func, locals_dict: dict): + signature = inspect.signature(func) + keys = signature.parameters.keys() + kwargs = {} + for k in keys: + kwargs[k] = locals_dict.get(k) + return kwargs diff --git a/apps/common/utils/lock.py b/apps/common/utils/lock.py new file mode 100644 index 000000000..9041a2578 --- /dev/null +++ b/apps/common/utils/lock.py @@ -0,0 +1,55 @@ +from functools import wraps + +from redis_lock import Lock as RedisLock +from redis import Redis + +from common.utils import get_logger +from common.utils.inspect import copy_function_args +from apps.jumpserver.const import CONFIG + +logger = get_logger(__file__) + + +class AcquireFailed(RuntimeError): + pass + + +class DistributedLock(RedisLock): + def __init__(self, name, blocking=True, expire=60*2, auto_renewal=True): + """ + 使用 redis 构造的分布式锁 + + :param name: + 锁的名字,要全局唯一 + :param blocking: + 该参数只在锁作为装饰器或者 `with` 时有效。 + :param expire: + 锁的过期时间,注意不一定是锁到这个时间就释放了,分两种情况 + 当 `auto_renewal=False` 时,锁会释放 + 当 `auto_renewal=True` 时,如果过期之前程序还没释放锁,我们会延长锁的存活时间。 + 这里的作用是防止程序意外终止没有释放锁,导致死锁。 + """ + self.kwargs_copy = copy_function_args(self.__init__, locals()) + redis = Redis(host=CONFIG.REDIS_HOST, port=CONFIG.REDIS_PORT, password=CONFIG.REDIS_PASSWORD) + super().__init__(redis_client=redis, name=name, expire=expire, auto_renewal=auto_renewal) + self._blocking = blocking + + def __enter__(self): + acquired = self.acquire(blocking=self._blocking) + if self._blocking and not acquired: + raise EnvironmentError("Lock wasn't acquired, but blocking=True") + if not acquired: + raise AcquireFailed + return self + + def __exit__(self, exc_type=None, exc_value=None, traceback=None): + self.release() + + def __call__(self, func): + @wraps(func) + def inner(*args, **kwds): + # 要创建一个新的锁对象 + with self.__class__(**self.kwargs_copy): + return func(*args, **kwds) + + return inner diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 211388dd871be071493b0820f3e599a00cd8b372..73c8027155d078501eab4e5df9ae031abf1d8c1d 100644 GIT binary patch delta 18013 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 NhOJv_{nk6h{{evgD=Pp1 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 7ddf14999..1b82453bb 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-09 18:14+0800\n" +"POT-Creation-Date: 2020-12-10 17:04+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -41,7 +41,7 @@ msgstr "远程应用" #: orgs/models.py:23 perms/models/base.py:48 settings/models.py:27 #: terminal/models.py:28 terminal/models.py:372 terminal/models.py:404 #: terminal/models.py:441 users/forms/profile.py:20 users/models/group.py:15 -#: users/models/user.py:501 users/templates/users/_select_user_modal.html:13 +#: users/models/user.py:495 users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 #: users/templates/users/user_database_app_permission.html:36 @@ -94,7 +94,7 @@ msgstr "类型" #: 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.py:38 #: terminal/models.py:411 terminal/models.py:448 tickets/models/ticket.py:43 -#: users/models/group.py:16 users/models/user.py:534 +#: users/models/group.py:16 users/models/user.py:528 #: 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 @@ -182,7 +182,7 @@ msgstr "参数" #: 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:542 users/serializers/group.py:35 +#: 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 @@ -237,7 +237,7 @@ msgstr "目标URL" #: 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:499 +#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:493 #: users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:53 #: users/templates/users/user_list.html:15 @@ -251,7 +251,8 @@ msgstr "用户名" #: 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 authentication/forms.py:13 +#: 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 #: users/forms/user.py:22 users/forms/user.py:193 @@ -295,15 +296,15 @@ msgstr "删除失败,存在关联资产" msgid "Number required" msgstr "需要为数字" -#: assets/api/node.py:58 +#: assets/api/node.py:66 msgid "You can't update the root node name" msgstr "不能修改根节点名称" -#: assets/api/node.py:65 +#: assets/api/node.py:73 msgid "You can't delete the root node ({})" msgstr "不能删除根节点 ({})" -#: assets/api/node.py:68 +#: assets/api/node.py:76 msgid "Deletion failed and the node contains children or assets" msgstr "删除失败,节点包含子节点或资产" @@ -364,7 +365,7 @@ msgstr "节点" #: assets/models/asset.py:200 assets/models/cmd_filter.py:22 #: assets/models/domain.py:56 assets/models/label.py:22 -#: authentication/models.py:48 +#: authentication/models.py:46 msgid "Is active" msgstr "激活" @@ -484,7 +485,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:520 +#: assets/models/cluster.py:22 users/models/user.py:514 #: users/templates/users/user_detail.html:62 msgid "Phone" msgstr "手机" @@ -510,7 +511,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:661 +#: users/models/user.py:655 msgid "System" msgstr "系统" @@ -611,8 +612,8 @@ msgid "Default asset group" msgstr "默认资产组" #: assets/models/label.py:15 audits/models.py:36 audits/models.py:56 -#: audits/models.py:69 audits/serializers.py:80 authentication/models.py:46 -#: authentication/models.py:90 orgs/models.py:18 orgs/models.py:396 +#: 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:169 #: perms/models/base.py:49 templates/index.html:78 @@ -621,7 +622,7 @@ msgstr "默认资产组" #: tickets/models/ticket.py:30 tickets/models/ticket.py:136 #: tickets/serializers/request_asset_perm.py:66 #: tickets/serializers/ticket.py:31 users/forms/group.py:15 -#: users/models/user.py:159 users/models/user.py:649 +#: users/models/user.py:159 users/models/user.py:643 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -715,7 +716,7 @@ msgstr "登录模式" msgid "SFTP Root" msgstr "SFTP根路径" -#: assets/models/user.py:110 authentication/models.py:88 +#: assets/models/user.py:110 authentication/models.py:86 msgid "Token" msgstr "" @@ -809,14 +810,14 @@ msgid "Backend" msgstr "后端" #: assets/serializers/asset_user.py:75 users/forms/profile.py:148 -#: users/models/user.py:531 users/templates/users/user_password_update.html:48 +#: users/models/user.py:525 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:528 +#: assets/serializers/asset_user.py:79 users/models/user.py:522 msgid "Private key" msgstr "ssh私钥" @@ -936,6 +937,11 @@ msgstr "更新节点资产硬件信息: {}" msgid "Gather assets users" msgstr "收集资产上的用户" +#: assets/tasks/nodes_amount.py:19 +msgid "" +"The task of self-checking is already running and cannot be started repeatedly" +msgstr "自检程序已经在运行,不能重复启动" + #: assets/tasks/push_system_user.py:184 #: assets/tasks/system_user_connectivity.py:89 msgid "System user is dynamic: {}" @@ -1125,14 +1131,14 @@ msgstr "登录IP" msgid "Login city" msgstr "登录城市" -#: audits/models.py:103 audits/serializers.py:37 +#: audits/models.py:103 audits/serializers.py:38 msgid "User agent" 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:523 +#: users/forms/profile.py:52 users/models/user.py:517 #: users/serializers/user.py:232 users/templates/users/user_detail.html:77 #: users/templates/users/user_profile.html:87 msgid "MFA" @@ -1153,6 +1159,10 @@ msgstr "状态" msgid "Date login" msgstr "登录日期" +#: audits/models.py:108 +msgid "Login backend" +msgstr "登录引擎" + #: audits/serializers.py:15 msgid "Operate for display" msgstr "操作(显示名称)" @@ -1165,32 +1175,40 @@ msgstr "状态(显示名称)" msgid "MFA for display" msgstr "多因子认证状态(显示名称)" -#: audits/serializers.py:65 audits/serializers.py:77 ops/models/adhoc.py:244 +#: audits/serializers.py:66 audits/serializers.py:78 ops/models/adhoc.py:244 #: terminal/serializers/session.py:34 msgid "Is success" msgstr "是否成功" -#: audits/serializers.py:76 ops/models/command.py:24 +#: audits/serializers.py:77 ops/models/command.py:24 #: xpack/plugins/cloud/models.py:222 msgid "Result" msgstr "结果" -#: audits/serializers.py:78 +#: audits/serializers.py:79 msgid "Hosts" msgstr "主机" -#: audits/serializers.py:79 +#: audits/serializers.py:80 msgid "Run as" msgstr "运行用户" -#: audits/serializers.py:81 +#: audits/serializers.py:82 msgid "Run as for display" msgstr "运行用户(显示名称)" -#: audits/serializers.py:82 +#: audits/serializers.py:83 msgid "User for display" msgstr "用户(显示名称)" +#: audits/signals_handler.py:38 +msgid "SSH Key" +msgstr "SSH 密钥" + +#: audits/signals_handler.py:43 +msgid "SSO" +msgstr "" + #: authentication/api/mfa.py:60 msgid "Code is invalid" msgstr "Code无效" @@ -1333,7 +1351,7 @@ msgstr "你的密码过于简单,为了安全,请修改" #: authentication/errors.py:227 authentication/views/login.py:262 msgid "Your password has expired, please reset before logging in" -msgstr "您的密码已过期,请先修改再登录" +msgstr "您的密码已过期,先修改再登录" #: authentication/forms.py:26 authentication/forms.py:34 #: authentication/templates/authentication/login.html:39 @@ -1342,7 +1360,7 @@ msgstr "您的密码已过期,请先修改再登录" msgid "MFA code" msgstr "多因子认证验证码" -#: authentication/models.py:22 +#: authentication/models.py:20 #: authentication/templates/authentication/_access_key_modal.html:32 #: perms/models/base.py:51 users/templates/users/_select_user_modal.html:18 #: users/templates/users/user_detail.html:132 @@ -1350,24 +1368,24 @@ msgstr "多因子认证验证码" msgid "Active" msgstr "激活中" -#: authentication/models.py:42 +#: authentication/models.py:40 msgid "Private Token" msgstr "SSH密钥" -#: authentication/models.py:47 users/templates/users/user_detail.html:258 +#: authentication/models.py:45 users/templates/users/user_detail.html:258 msgid "Reviewers" msgstr "审批人" -#: authentication/models.py:56 tickets/models/ticket.py:23 +#: authentication/models.py:54 tickets/models/ticket.py:23 #: users/templates/users/user_detail.html:250 msgid "Login confirm" msgstr "登录复核" -#: authentication/models.py:66 +#: authentication/models.py:64 msgid "City" msgstr "城市" -#: authentication/models.py:89 +#: authentication/models.py:87 msgid "Expired" msgstr "过期时间" @@ -1857,7 +1875,7 @@ msgstr "组织管理员" msgid "Organization auditor" msgstr "组织审计员" -#: orgs/models.py:397 users/forms/user.py:27 users/models/user.py:511 +#: orgs/models.py:397 users/forms/user.py:27 users/models/user.py:505 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:73 #: users/templates/users/user_list.html:16 @@ -1890,7 +1908,7 @@ msgstr "提示:RDP 协议不支持单独控制上传或下载文件" #: 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:507 users/templates/users/_select_user_modal.html:16 +#: users/models/user.py:501 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 @@ -1964,7 +1982,7 @@ msgid "Asset permission" msgstr "资产授权" #: perms/models/base.py:53 tickets/serializers/request_asset_perm.py:31 -#: users/models/user.py:539 users/templates/users/user_detail.html:93 +#: users/models/user.py:533 users/templates/users/user_detail.html:93 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" @@ -3116,7 +3134,7 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:89 users/models/user.py:503 +#: users/forms/profile.py:89 users/models/user.py:497 #: users/templates/users/user_detail.html:57 #: users/templates/users/user_profile.html:59 msgid "Email" @@ -3157,7 +3175,7 @@ msgstr "不能和原来的密钥相同" msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/user.py:31 users/models/user.py:546 +#: users/forms/user.py:31 users/models/user.py:540 #: users/templates/users/user_detail.html:89 #: users/templates/users/user_list.html:18 #: users/templates/users/user_profile.html:102 @@ -3203,27 +3221,27 @@ msgstr "系统审计员" msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:490 +#: users/models/user.py:485 msgid "Local" msgstr "数据库" -#: users/models/user.py:514 +#: users/models/user.py:508 msgid "Avatar" msgstr "头像" -#: users/models/user.py:517 users/templates/users/user_detail.html:68 +#: users/models/user.py:511 users/templates/users/user_detail.html:68 msgid "Wechat" msgstr "微信" -#: users/models/user.py:550 +#: users/models/user.py:544 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:657 +#: users/models/user.py:651 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:660 +#: users/models/user.py:654 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" From 4424c4bde2bea4630faf69203366924a180f25dd Mon Sep 17 00:00:00 2001 From: xinwen Date: Thu, 10 Dec 2020 17:47:35 +0800 Subject: [PATCH 53/57] =?UTF-8?q?perf(asset):=20=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E8=8A=82=E7=82=B9=E8=B5=84=E4=BA=A7=E6=95=B0?= =?UTF-8?q?=E9=87=8F=E8=87=AA=E6=A3=80=E7=A8=8B=E5=BA=8F=E6=97=B6=E5=8C=BA?= =?UTF-8?q?=E5=88=86=E7=BB=84=E7=BB=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/node.py | 5 +++-- apps/assets/tasks/nodes_amount.py | 14 +++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index b1e01c9c1..924529ce1 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -21,7 +21,8 @@ from common.const.distributed_lock_key import UPDATE_NODE_TREE_LOCK_KEY from orgs.mixins.api import OrgModelViewSet from orgs.mixins import generics from orgs.lock import org_level_transaction_lock -from assets.tasks import check_node_assets_amount_period_task +from orgs.utils import current_org +from assets.tasks import check_node_assets_amount_task from ..hands import IsOrgAdmin from ..models import Node from ..tasks import ( @@ -51,7 +52,7 @@ class NodeViewSet(OrgModelViewSet): @action(methods=[POST], detail=False, url_name='launch-check-assets-amount-task') def launch_check_assets_amount_task(self, request): - task = check_node_assets_amount_period_task.delay() + task = check_node_assets_amount_task.delay(current_org.id) return Response(data={'task': task.id}) # 仅支持根节点指直接创建,子节点下的节点需要通过children接口创建 diff --git a/apps/assets/tasks/nodes_amount.py b/apps/assets/tasks/nodes_amount.py index cd929d131..e1e437797 100644 --- a/apps/assets/tasks/nodes_amount.py +++ b/apps/assets/tasks/nodes_amount.py @@ -1,6 +1,8 @@ from celery import shared_task from django.utils.translation import gettext_lazy as _ +from orgs.models import Organization +from orgs.utils import tmp_to_org from ops.celery.decorator import register_as_period_task from assets.utils import check_node_assets_amount @@ -11,9 +13,15 @@ logger = get_logger(__file__) @shared_task(queue='celery_heavy_tasks') -@register_as_period_task(crontab='0 2 * * *') -def check_node_assets_amount_period_task(): +def check_node_assets_amount_task(org_id=Organization.ROOT_ID): try: - check_node_assets_amount() + with tmp_to_org(Organization.get_instance(org_id)): + check_node_assets_amount() except AcquireFailed: logger.error(_('The task of self-checking is already running and cannot be started repeatedly')) + + +@register_as_period_task(crontab='0 2 * * *') +@shared_task(queue='celery_heavy_tasks') +def check_node_assets_amount_period_task(): + check_node_assets_amount_task() From 5aee2ce3dbd028c17ebb9314bc296f24c5b77a90 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Thu, 10 Dec 2020 20:46:45 +0800 Subject: [PATCH 54/57] =?UTF-8?q?chore:=20=E5=8D=87=E7=BA=A7=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E5=BA=93=E7=89=88=E6=9C=AC=20(#5205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 升级依赖库版本 * fix: 几个库回退几个版本 Co-authored-by: ibuler --- requirements/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index fa287298e..b635353de 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -88,9 +88,9 @@ httpsig==1.3.0 treelib==1.5.3 django-proxy==1.2.1 flower==0.9.3 -channels-redis==2.4.0 -channels==2.3.0 -daphne==2.3.0 +channels-redis==3.2.0 +channels==2.4.0 +daphne==2.4.1 psutil==5.6.6 django-cas-ng==4.0.1 python-cas==1.5.0 From d4feaf1e08ba0559cd732d8f495c9155cb93e8d2 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Thu, 10 Dec 2020 20:48:10 +0800 Subject: [PATCH 55/57] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=94=B1?= =?UTF-8?q?=E4=BA=8E=E6=9B=B4=E6=96=B0django=20captch=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=BC=95=E8=B5=B7=E7=9A=84css=E4=B8=A2=E5=A4=B1=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20(#5204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 修复由于更新django captch版本引起的css丢失问题 * perf: 优化验证码的高度 Co-authored-by: ibuler --- apps/authentication/forms.py | 8 +++-- .../authentication/_captcha_field.html | 29 +++++++++++++++++++ apps/jumpserver/settings/base.py | 1 + apps/jumpserver/settings/libs.py | 4 +-- apps/jumpserver/urls.py | 1 - apps/templates/captcha/field.html | 12 -------- apps/templates/captcha/hidden_field.html | 1 - apps/templates/captcha/image.html | 4 --- apps/templates/captcha/text_field.html | 7 ----- 9 files changed, 38 insertions(+), 29 deletions(-) create mode 100644 apps/authentication/templates/authentication/_captcha_field.html delete mode 100644 apps/templates/captcha/field.html delete mode 100644 apps/templates/captcha/hidden_field.html delete mode 100644 apps/templates/captcha/image.html delete mode 100644 apps/templates/captcha/text_field.html diff --git a/apps/authentication/forms.py b/apps/authentication/forms.py index 2f03d935b..fe28edb68 100644 --- a/apps/authentication/forms.py +++ b/apps/authentication/forms.py @@ -4,7 +4,7 @@ from django import forms from django.conf import settings from django.utils.translation import gettext_lazy as _ -from captcha.fields import CaptchaField +from captcha.fields import CaptchaField, CaptchaTextInput class UserLoginForm(forms.Form): @@ -26,8 +26,12 @@ class UserCheckOtpCodeForm(forms.Form): otp_code = forms.CharField(label=_('MFA code'), max_length=6) +class CustomCaptchaTextInput(CaptchaTextInput): + template_name = 'authentication/_captcha_field.html' + + class CaptchaMixin(forms.Form): - captcha = CaptchaField() + captcha = CaptchaField(widget=CustomCaptchaTextInput) class ChallengeMixin(forms.Form): diff --git a/apps/authentication/templates/authentication/_captcha_field.html b/apps/authentication/templates/authentication/_captcha_field.html new file mode 100644 index 000000000..a190aacb7 --- /dev/null +++ b/apps/authentication/templates/authentication/_captcha_field.html @@ -0,0 +1,29 @@ +{% load i18n %} +{% spaceless %} + captcha +

+ + +{% endspaceless %} \ No newline at end of file diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index 47b3d7a04..2a6291751 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -64,6 +64,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.forms', ] diff --git a/apps/jumpserver/settings/libs.py b/apps/jumpserver/settings/libs.py index 782d2bc06..8a8df7ca4 100644 --- a/apps/jumpserver/settings/libs.py +++ b/apps/jumpserver/settings/libs.py @@ -64,10 +64,10 @@ SWAGGER_SETTINGS = { # Captcha settings, more see https://django-simple-captcha.readthedocs.io/en/latest/advanced.html -CAPTCHA_IMAGE_SIZE = (80, 33) +CAPTCHA_IMAGE_SIZE = (140, 34) CAPTCHA_FOREGROUND_COLOR = '#001100' CAPTCHA_NOISE_FUNCTIONS = ('captcha.helpers.noise_dots',) -CAPTCHA_TEST_MODE = CONFIG.CAPTCHA_TEST_MODE +CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.math_challenge' # Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html BOOTSTRAP3 = { diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index c6d85b48b..47518d946 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -30,7 +30,6 @@ api_v2 = [ path('users/', include('users.urls.api_urls_v2', namespace='api-users-v2')), ] - app_view_patterns = [ path('auth/', include('authentication.urls.view_urls'), name='auth'), path('ops/', include('ops.urls.view_urls'), name='ops'), diff --git a/apps/templates/captcha/field.html b/apps/templates/captcha/field.html deleted file mode 100644 index 6979e870c..000000000 --- a/apps/templates/captcha/field.html +++ /dev/null @@ -1,12 +0,0 @@ -{{image}}{{hidden_field}}{{text_field}} - - \ No newline at end of file diff --git a/apps/templates/captcha/hidden_field.html b/apps/templates/captcha/hidden_field.html deleted file mode 100644 index 36d7490a3..000000000 --- a/apps/templates/captcha/hidden_field.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/templates/captcha/image.html b/apps/templates/captcha/image.html deleted file mode 100644 index b4a415536..000000000 --- a/apps/templates/captcha/image.html +++ /dev/null @@ -1,4 +0,0 @@ -{% load i18n %} -{% spaceless %} - {% if audio %}{% endif %}captcha{% if audio %}{% endif %} -{% endspaceless %} \ No newline at end of file diff --git a/apps/templates/captcha/text_field.html b/apps/templates/captcha/text_field.html deleted file mode 100644 index 413eb1893..000000000 --- a/apps/templates/captcha/text_field.html +++ /dev/null @@ -1,7 +0,0 @@ -{% load i18n %} -
-
- -
-
-
From 856e7c16e511819af1130cbe9df01091485274ce Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Thu, 10 Dec 2020 20:50:22 +0800 Subject: [PATCH 56/57] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E7=9B=91=E6=8E=A7;TerminalModel=E6=B7=BB=E5=8A=A0type?= =?UTF-8?q?=E5=AD=97=E6=AE=B5;=20(#5206)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 添加组件监控;TerminalModel添加type字段; * feat: Terminal序列类添加type字段 * feat: Terminal序列类添加type字段为只读 * feat: 修改组件status文案 * feat: 取消上传组件状态序列类count字段 * reactor: 修改termina/models目录结构 * feat: 修改ComponentTypeChoices * feat: 取消考虑CoreComponent类型 * feat: 修改Terminal status判断逻辑 * feat: 终端列表添加status过滤; 组件状态序列类添加default值 * feat: 添加PrometheusMetricsAPI * feat: 修改PrometheusMetricsAPI Co-authored-by: Bai --- apps/jumpserver/api.py | 11 +- apps/jumpserver/urls.py | 1 + apps/terminal/api/__init__.py | 1 + apps/terminal/api/component.py | 34 ++ apps/terminal/api/terminal.py | 15 +- apps/terminal/const.py | 24 + .../terminal/migrations/0030_terminal_type.py | 42 ++ apps/terminal/models.py | 486 ------------------ apps/terminal/models/__init__.py | 6 + apps/terminal/models/command.py | 21 + apps/terminal/models/session.py | 210 ++++++++ apps/terminal/models/status.py | 28 + apps/terminal/models/storage.py | 103 ++++ apps/terminal/models/task.py | 25 + apps/terminal/models/terminal.py | 247 +++++++++ apps/terminal/serializers/__init__.py | 1 + apps/terminal/serializers/components.py | 25 + apps/terminal/serializers/terminal.py | 10 +- apps/terminal/urls/api_urls.py | 5 +- apps/terminal/utils.py | 102 ++++ 20 files changed, 902 insertions(+), 495 deletions(-) create mode 100644 apps/terminal/api/component.py create mode 100644 apps/terminal/migrations/0030_terminal_type.py delete mode 100644 apps/terminal/models.py create mode 100644 apps/terminal/models/__init__.py create mode 100644 apps/terminal/models/command.py create mode 100644 apps/terminal/models/session.py create mode 100644 apps/terminal/models/status.py create mode 100644 apps/terminal/models/storage.py create mode 100644 apps/terminal/models/task.py create mode 100644 apps/terminal/models/terminal.py create mode 100644 apps/terminal/serializers/components.py diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index 026a90b9a..b74099fa3 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -2,13 +2,14 @@ from django.core.cache import cache from django.utils import timezone from django.utils.timesince import timesince from django.db.models import Count, Max -from django.http.response import JsonResponse +from django.http.response import JsonResponse, HttpResponse from rest_framework.views import APIView from collections import Counter from users.models import User from assets.models import Asset from terminal.models import Session +from terminal.utils import ComponentsPrometheusMetricsUtil from orgs.utils import current_org from common.permissions import IsOrgAdmin, IsOrgAuditor from common.utils import lazyproperty @@ -305,3 +306,11 @@ class IndexApi(TotalCountMixin, DatesLoginMetricMixin, APIView): return JsonResponse(data, status=200) +class PrometheusMetricsApi(APIView): + permission_classes = () + + def get(self, request, *args, **kwargs): + util = ComponentsPrometheusMetricsUtil() + metrics_text = util.get_prometheus_metrics_text() + return HttpResponse(metrics_text, content_type='text/plain; version=0.0.4; charset=utf-8') + diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 47518d946..d03c20ba3 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -23,6 +23,7 @@ api_v1 = [ path('common/', include('common.urls.api_urls', namespace='api-common')), path('applications/', include('applications.urls.api_urls', namespace='api-applications')), path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')), + path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()) ] api_v2 = [ diff --git a/apps/terminal/api/__init__.py b/apps/terminal/api/__init__.py index 640dacb6a..c0c6b8197 100644 --- a/apps/terminal/api/__init__.py +++ b/apps/terminal/api/__init__.py @@ -5,3 +5,4 @@ from .session import * from .command import * from .task import * from .storage import * +from .component import * diff --git a/apps/terminal/api/component.py b/apps/terminal/api/component.py new file mode 100644 index 000000000..aec404370 --- /dev/null +++ b/apps/terminal/api/component.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# + +import logging +from rest_framework import generics, status +from rest_framework.views import Response + +from .. import serializers +from ..utils import ComponentsMetricsUtil +from common.permissions import IsAppUser, IsSuperUser + +logger = logging.getLogger(__file__) + + +__all__ = [ + 'ComponentsStateAPIView', 'ComponentsMetricsAPIView', +] + + +class ComponentsStateAPIView(generics.CreateAPIView): + """ koko, guacamole, omnidb 上报状态 """ + permission_classes = (IsAppUser,) + serializer_class = serializers.ComponentsStateSerializer + + +class ComponentsMetricsAPIView(generics.GenericAPIView): + """ 返回汇总组件指标数据 """ + permission_classes = (IsSuperUser,) + + def get(self, request, *args, **kwargs): + component_type = request.query_params.get('type') + util = ComponentsMetricsUtil(component_type) + metrics = util.get_metrics() + return Response(metrics, status=status.HTTP_200_OK) diff --git a/apps/terminal/api/terminal.py b/apps/terminal/api/terminal.py index 2ee353e3e..a5e976034 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'] + filter_fields = ['name', 'remote_addr', 'type'] def create(self, request, *args, **kwargs): if isinstance(request.data, list): @@ -60,6 +60,15 @@ class TerminalViewSet(JMSBulkModelViewSet): logger.error("Register terminal error: {}".format(data)) return Response(data, status=400) + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + status = self.request.query_params.get('status') + if not status: + return queryset + filtered_queryset_id = [str(q.id) for q in queryset if q.status == status] + queryset = queryset.filter(id__in=filtered_queryset_id) + return queryset + def get_permissions(self): if self.action == "create": self.permission_classes = (AllowAny,) @@ -104,15 +113,11 @@ class StatusViewSet(viewsets.ModelViewSet): task_serializer_class = serializers.TaskSerializer def create(self, request, *args, **kwargs): - self.handle_status(request) self.handle_sessions() tasks = self.request.user.terminal.task_set.filter(is_finished=False) serializer = self.task_serializer_class(tasks, many=True) return Response(serializer.data, status=201) - def handle_status(self, request): - request.user.terminal.is_alive = True - def handle_sessions(self): sessions_id = self.request.data.get('sessions', []) # guacamole 上报的 session 是字符串 diff --git a/apps/terminal/const.py b/apps/terminal/const.py index 4d19d007c..b7a48fab3 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -108,3 +108,27 @@ COMMAND_STORAGE_TYPE_CHOICES_EXTENDS = [ 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 _ + + +class ComponentStatusChoices(TextChoices): + critical = 'critical', _('Critical') + high = 'high', _('High') + normal = 'normal', _('Normal') + + @classmethod + def status(cls): + return set(dict(cls.choices).keys()) + + +class TerminalTypeChoices(TextChoices): + koko = 'koko', 'KoKo' + guacamole = 'guacamole', 'Guacamole' + omnidb = 'omnidb', 'OmniDB' + + @classmethod + def types(cls): + return set(dict(cls.choices).keys()) + diff --git a/apps/terminal/migrations/0030_terminal_type.py b/apps/terminal/migrations/0030_terminal_type.py new file mode 100644 index 000000000..4e4d871f8 --- /dev/null +++ b/apps/terminal/migrations/0030_terminal_type.py @@ -0,0 +1,42 @@ +# Generated by Django 3.1 on 2020-12-10 07:05 + +from django.db import migrations, models + +TERMINAL_TYPE_KOKO = 'koko' +TERMINAL_TYPE_GUACAMOLE = 'guacamole' +TERMINAL_TYPE_OMNIDB = 'omnidb' + + +def migrate_terminal_type(apps, schema_editor): + terminal_model = apps.get_model("terminal", "Terminal") + db_alias = schema_editor.connection.alias + terminals = terminal_model.objects.using(db_alias).all() + for terminal in terminals: + name = terminal.name.lower() + if 'koko' in name: + _type = TERMINAL_TYPE_KOKO + elif 'gua' in name: + _type = TERMINAL_TYPE_GUACAMOLE + elif 'omnidb' in name: + _type = TERMINAL_TYPE_OMNIDB + else: + _type = TERMINAL_TYPE_KOKO + terminal.type = _type + terminal_model.objects.bulk_update(terminals, ['type']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0029_auto_20201116_1757'), + ] + + operations = [ + migrations.AddField( + model_name='terminal', + name='type', + field=models.CharField(choices=[('koko', 'KoKo'), ('guacamole', 'Guacamole'), ('omnidb', 'OmniDB')], default='koko', max_length=64, verbose_name='type'), + preserve_default=False, + ), + migrations.RunPython(migrate_terminal_type) + ] diff --git a/apps/terminal/models.py b/apps/terminal/models.py deleted file mode 100644 index 17eb59f00..000000000 --- a/apps/terminal/models.py +++ /dev/null @@ -1,486 +0,0 @@ -from __future__ import unicode_literals - -import os -import uuid -import jms_storage - -from django.db import models -from django.db.models.signals import post_save -from django.utils.translation import ugettext_lazy as _ -from django.utils import timezone -from django.conf import settings -from django.core.files.storage import default_storage -from django.core.cache import cache - -from assets.models import Asset -from users.models import User -from orgs.mixins.models import OrgModelMixin -from common.mixins import CommonModelMixin -from common.fields.model import EncryptJsonDictTextField -from common.db.models import ChoiceSet -from .backends import get_multi_command_storage -from .backends.command.models import AbstractSessionCommand -from . import const - - -class Terminal(models.Model): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - name = models.CharField(max_length=128, verbose_name=_('Name')) - remote_addr = models.CharField(max_length=128, blank=True, verbose_name=_('Remote Address')) - ssh_port = models.IntegerField(verbose_name=_('SSH Port'), default=2222) - http_port = models.IntegerField(verbose_name=_('HTTP Port'), default=5000) - command_storage = models.CharField(max_length=128, verbose_name=_("Command storage"), default='default') - replay_storage = models.CharField(max_length=128, verbose_name=_("Replay storage"), default='default') - user = models.OneToOneField(User, related_name='terminal', verbose_name='Application User', null=True, on_delete=models.CASCADE) - is_accepted = models.BooleanField(default=False, verbose_name='Is Accepted') - is_deleted = models.BooleanField(default=False) - date_created = models.DateTimeField(auto_now_add=True) - comment = models.TextField(blank=True, verbose_name=_('Comment')) - STATUS_KEY_PREFIX = 'terminal_status_' - - @property - def is_alive(self): - key = self.STATUS_KEY_PREFIX + str(self.id) - return bool(cache.get(key)) - - @is_alive.setter - def is_alive(self, value): - key = self.STATUS_KEY_PREFIX + str(self.id) - cache.set(key, value, 60) - - @property - def is_active(self): - if self.user and self.user.is_active: - return True - return False - - @is_active.setter - def is_active(self, active): - if self.user: - self.user.is_active = active - self.user.save() - - def get_command_storage(self): - storage = CommandStorage.objects.filter(name=self.command_storage).first() - return storage - - def get_command_storage_config(self): - s = self.get_command_storage() - if s: - config = s.config - else: - config = settings.DEFAULT_TERMINAL_COMMAND_STORAGE - return config - - def get_command_storage_setting(self): - config = self.get_command_storage_config() - return {"TERMINAL_COMMAND_STORAGE": config} - - def get_replay_storage(self): - storage = ReplayStorage.objects.filter(name=self.replay_storage).first() - return storage - - def get_replay_storage_config(self): - s = self.get_replay_storage() - if s: - config = s.config - else: - config = settings.DEFAULT_TERMINAL_REPLAY_STORAGE - return config - - def get_replay_storage_setting(self): - config = self.get_replay_storage_config() - return {"TERMINAL_REPLAY_STORAGE": config} - - @staticmethod - def get_login_title_setting(): - login_title = None - if settings.XPACK_ENABLED: - from xpack.plugins.interface.models import Interface - login_title = Interface.get_login_title() - return {'TERMINAL_HEADER_TITLE': login_title} - - @property - def config(self): - configs = {} - for k in dir(settings): - if not k.startswith('TERMINAL'): - continue - configs[k] = getattr(settings, k) - configs.update(self.get_command_storage_setting()) - configs.update(self.get_replay_storage_setting()) - configs.update(self.get_login_title_setting()) - configs.update({ - 'SECURITY_MAX_IDLE_TIME': settings.SECURITY_MAX_IDLE_TIME - }) - return configs - - @property - def service_account(self): - return self.user - - def create_app_user(self): - random = uuid.uuid4().hex[:6] - user, access_key = User.create_app_user( - name="{}-{}".format(self.name, random), comment=self.comment - ) - self.user = user - self.save() - return user, access_key - - def delete(self, using=None, keep_parents=False): - if self.user: - self.user.delete() - self.user = None - self.is_deleted = True - self.save() - return - - def __str__(self): - status = "Active" - if not self.is_accepted: - status = "NotAccept" - elif self.is_deleted: - status = "Deleted" - elif not self.is_active: - status = "Disable" - return '%s: %s' % (self.name, status) - - class Meta: - ordering = ('is_accepted',) - db_table = "terminal" - - -class Status(models.Model): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - session_online = models.IntegerField(verbose_name=_("Session Online"), default=0) - cpu_used = models.FloatField(verbose_name=_("CPU Usage")) - memory_used = models.FloatField(verbose_name=_("Memory Used")) - connections = models.IntegerField(verbose_name=_("Connections")) - threads = models.IntegerField(verbose_name=_("Threads")) - boot_time = models.FloatField(verbose_name=_("Boot Time")) - terminal = models.ForeignKey(Terminal, null=True, on_delete=models.CASCADE) - date_created = models.DateTimeField(auto_now_add=True) - - class Meta: - db_table = 'terminal_status' - get_latest_by = 'date_created' - - def __str__(self): - return self.date_created.strftime("%Y-%m-%d %H:%M:%S") - - -class Session(OrgModelMixin): - class LOGIN_FROM(ChoiceSet): - ST = 'ST', 'SSH Terminal' - WT = 'WT', 'Web Terminal' - - class PROTOCOL(ChoiceSet): - SSH = 'ssh', 'ssh' - RDP = 'rdp', 'rdp' - VNC = 'vnc', 'vnc' - TELNET = 'telnet', 'telnet' - MYSQL = 'mysql', 'mysql' - ORACLE = 'oracle', 'oracle' - MARIADB = 'mariadb', 'mariadb' - POSTGRESQL = 'postgresql', 'postgresql' - K8S = 'k8s', 'kubernetes' - - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - user = models.CharField(max_length=128, verbose_name=_("User"), db_index=True) - user_id = models.CharField(blank=True, default='', max_length=36, db_index=True) - asset = models.CharField(max_length=128, verbose_name=_("Asset"), db_index=True) - asset_id = models.CharField(blank=True, default='', max_length=36, db_index=True) - system_user = models.CharField(max_length=128, verbose_name=_("System user"), db_index=True) - system_user_id = models.CharField(blank=True, default='', max_length=36, db_index=True) - login_from = models.CharField(max_length=2, choices=LOGIN_FROM.choices, default="ST", verbose_name=_("Login from")) - remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) - is_success = models.BooleanField(default=True, db_index=True) - is_finished = models.BooleanField(default=False, db_index=True) - has_replay = models.BooleanField(default=False, verbose_name=_("Replay")) - has_command = models.BooleanField(default=False, verbose_name=_("Command")) - terminal = models.ForeignKey(Terminal, null=True, on_delete=models.DO_NOTHING, db_constraint=False) - protocol = models.CharField(choices=PROTOCOL.choices, default='ssh', max_length=16, db_index=True) - date_start = models.DateTimeField(verbose_name=_("Date start"), db_index=True, default=timezone.now) - date_end = models.DateTimeField(verbose_name=_("Date end"), null=True) - - upload_to = 'replay' - ACTIVE_CACHE_KEY_PREFIX = 'SESSION_ACTIVE_{}' - _DATE_START_FIRST_HAS_REPLAY_RDP_SESSION = None - - def get_rel_replay_path(self, version=2): - """ - 获取session日志的文件路径 - :param version: 原来后缀是 .gz,为了统一新版本改为 .replay.gz - :return: - """ - suffix = '.replay.gz' - if version == 1: - suffix = '.gz' - date = self.date_start.strftime('%Y-%m-%d') - return os.path.join(date, str(self.id) + suffix) - - def get_local_path(self, version=2): - rel_path = self.get_rel_replay_path(version=version) - if version == 2: - local_path = os.path.join(self.upload_to, rel_path) - else: - local_path = rel_path - return local_path - - @property - def asset_obj(self): - return Asset.objects.get(id=self.asset_id) - - @property - def _date_start_first_has_replay_rdp_session(self): - if self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION is None: - instance = self.__class__.objects.filter( - protocol='rdp', has_replay=True - ).order_by('date_start').first() - if not instance: - date_start = timezone.now() - timezone.timedelta(days=365) - else: - date_start = instance.date_start - self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION = date_start - return self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION - - def can_replay(self): - if self.has_replay: - return True - if self.date_start < self._date_start_first_has_replay_rdp_session: - return True - return False - - @property - def can_join(self): - _PROTOCOL = self.PROTOCOL - if self.is_finished: - return False - if self.protocol in [_PROTOCOL.SSH, _PROTOCOL.TELNET, _PROTOCOL.K8S]: - return True - else: - return False - - @property - def db_protocols(self): - _PROTOCOL = self.PROTOCOL - return [_PROTOCOL.MYSQL, _PROTOCOL.MARIADB, _PROTOCOL.ORACLE, _PROTOCOL.POSTGRESQL] - - @property - def can_terminate(self): - _PROTOCOL = self.PROTOCOL - if self.is_finished: - return False - if self.protocol in self.db_protocols: - return False - else: - return True - - def save_replay_to_storage(self, f): - local_path = self.get_local_path() - try: - name = default_storage.save(local_path, f) - except OSError as e: - return None, e - - if settings.SERVER_REPLAY_STORAGE: - from .tasks import upload_session_replay_to_external_storage - upload_session_replay_to_external_storage.delay(str(self.id)) - return name, None - - @classmethod - def set_sessions_active(cls, sessions_id): - data = {cls.ACTIVE_CACHE_KEY_PREFIX.format(i): i for i in sessions_id} - cache.set_many(data, timeout=5*60) - - @classmethod - def get_active_sessions(cls): - return cls.objects.filter(is_finished=False) - - def is_active(self): - if self.protocol in ['ssh', 'telnet', 'rdp', 'mysql']: - key = self.ACTIVE_CACHE_KEY_PREFIX.format(self.id) - return bool(cache.get(key)) - return True - - @property - def command_amount(self): - command_store = get_multi_command_storage() - return command_store.count(session=str(self.id)) - - @property - def login_from_display(self): - return self.get_login_from_display() - - @classmethod - def generate_fake(cls, count=100, is_finished=True): - import random - from orgs.models import Organization - from users.models import User - from assets.models import Asset, SystemUser - from orgs.utils import get_current_org - from common.utils.random import random_datetime, random_ip - - org = get_current_org() - if not org or not org.is_real(): - Organization.default().change_to() - i = 0 - users = User.objects.all()[:100] - assets = Asset.objects.all()[:100] - system_users = SystemUser.objects.all()[:100] - while i < count: - user_random = random.choices(users, k=10) - assets_random = random.choices(assets, k=10) - system_users = random.choices(system_users, k=10) - - ziped = zip(user_random, assets_random, system_users) - sessions = [] - now = timezone.now() - month_ago = now - timezone.timedelta(days=30) - for user, asset, system_user in ziped: - ip = random_ip() - date_start = random_datetime(month_ago, now) - date_end = random_datetime(date_start, date_start+timezone.timedelta(hours=2)) - data = dict( - user=str(user), user_id=user.id, - asset=str(asset), asset_id=asset.id, - system_user=str(system_user), system_user_id=system_user.id, - remote_addr=ip, - date_start=date_start, - date_end=date_end, - is_finished=is_finished, - ) - sessions.append(Session(**data)) - cls.objects.bulk_create(sessions) - i += 10 - - class Meta: - db_table = "terminal_session" - ordering = ["-date_start"] - - def __str__(self): - return "{0.id} of {0.user} to {0.asset}".format(self) - - -class Task(models.Model): - NAME_CHOICES = ( - ("kill_session", "Kill Session"), - ) - - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - name = models.CharField(max_length=128, choices=NAME_CHOICES, verbose_name=_("Name")) - args = models.CharField(max_length=1024, verbose_name=_("Args")) - terminal = models.ForeignKey(Terminal, null=True, on_delete=models.SET_NULL) - is_finished = models.BooleanField(default=False) - date_created = models.DateTimeField(auto_now_add=True) - date_finished = models.DateTimeField(null=True) - - class Meta: - db_table = "terminal_task" - - -class CommandManager(models.Manager): - def bulk_create(self, objs, **kwargs): - resp = super().bulk_create(objs, **kwargs) - for i in objs: - post_save.send(i.__class__, instance=i, created=True) - return resp - - -class Command(AbstractSessionCommand): - objects = CommandManager() - - class Meta: - db_table = "terminal_command" - ordering = ('-timestamp',) - - -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 - ) - meta = EncryptJsonDictTextField(default={}) - comment = models.TextField( - max_length=128, default='', blank=True, verbose_name=_('Comment') - ) - - def __str__(self): - return self.name - - @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(): - return True - storage = jms_storage.get_log_storage(self.config) - return storage.ping() - - def is_using(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 - ) - meta = EncryptJsonDictTextField(default={}) - comment = models.TextField( - max_length=128, default='', blank=True, verbose_name=_('Comment') - ) - - def __str__(self): - return self.name - - def convert_type(self): - s3_type_list = [const.REPLAY_STORAGE_TYPE_CEPH] - tp = self.type - if tp in s3_type_list: - tp = const.REPLAY_STORAGE_TYPE_S3 - return tp - - 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 config(self): - config = self.meta - extra_config = self.get_extra_config() - config.update(extra_config) - return config - - def in_defaults(self): - return self.type in self.TYPE_DEFAULTS - - def is_valid(self): - if self.in_defaults(): - 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): - return Terminal.objects.filter(replay_storage=self.name).exists() diff --git a/apps/terminal/models/__init__.py b/apps/terminal/models/__init__.py new file mode 100644 index 000000000..1de5fd31e --- /dev/null +++ b/apps/terminal/models/__init__.py @@ -0,0 +1,6 @@ +from .command import * +from .session import * +from .status import * +from .storage import * +from .task import * +from .terminal import * diff --git a/apps/terminal/models/command.py b/apps/terminal/models/command.py new file mode 100644 index 000000000..fba906226 --- /dev/null +++ b/apps/terminal/models/command.py @@ -0,0 +1,21 @@ +from __future__ import unicode_literals + +from django.db import models +from django.db.models.signals import post_save +from ..backends.command.models import AbstractSessionCommand + + +class CommandManager(models.Manager): + def bulk_create(self, objs, **kwargs): + resp = super().bulk_create(objs, **kwargs) + for i in objs: + post_save.send(i.__class__, instance=i, created=True) + return resp + + +class Command(AbstractSessionCommand): + objects = CommandManager() + + class Meta: + db_table = "terminal_command" + ordering = ('-timestamp',) diff --git a/apps/terminal/models/session.py b/apps/terminal/models/session.py new file mode 100644 index 000000000..4e2a1b99a --- /dev/null +++ b/apps/terminal/models/session.py @@ -0,0 +1,210 @@ +from __future__ import unicode_literals + +import os +import uuid + +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone +from django.conf import settings +from django.core.files.storage import default_storage +from django.core.cache import cache + +from assets.models import Asset +from orgs.mixins.models import OrgModelMixin +from common.db.models import ChoiceSet +from ..backends import get_multi_command_storage +from .terminal import Terminal + + +class Session(OrgModelMixin): + class LOGIN_FROM(ChoiceSet): + ST = 'ST', 'SSH Terminal' + WT = 'WT', 'Web Terminal' + + class PROTOCOL(ChoiceSet): + SSH = 'ssh', 'ssh' + RDP = 'rdp', 'rdp' + VNC = 'vnc', 'vnc' + TELNET = 'telnet', 'telnet' + MYSQL = 'mysql', 'mysql' + ORACLE = 'oracle', 'oracle' + MARIADB = 'mariadb', 'mariadb' + POSTGRESQL = 'postgresql', 'postgresql' + K8S = 'k8s', 'kubernetes' + + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + user = models.CharField(max_length=128, verbose_name=_("User"), db_index=True) + user_id = models.CharField(blank=True, default='', max_length=36, db_index=True) + asset = models.CharField(max_length=128, verbose_name=_("Asset"), db_index=True) + asset_id = models.CharField(blank=True, default='', max_length=36, db_index=True) + system_user = models.CharField(max_length=128, verbose_name=_("System user"), db_index=True) + system_user_id = models.CharField(blank=True, default='', max_length=36, db_index=True) + login_from = models.CharField(max_length=2, choices=LOGIN_FROM.choices, default="ST", verbose_name=_("Login from")) + remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) + is_success = models.BooleanField(default=True, db_index=True) + is_finished = models.BooleanField(default=False, db_index=True) + has_replay = models.BooleanField(default=False, verbose_name=_("Replay")) + has_command = models.BooleanField(default=False, verbose_name=_("Command")) + terminal = models.ForeignKey(Terminal, null=True, on_delete=models.DO_NOTHING, db_constraint=False) + protocol = models.CharField(choices=PROTOCOL.choices, default='ssh', max_length=16, db_index=True) + date_start = models.DateTimeField(verbose_name=_("Date start"), db_index=True, default=timezone.now) + date_end = models.DateTimeField(verbose_name=_("Date end"), null=True) + + upload_to = 'replay' + ACTIVE_CACHE_KEY_PREFIX = 'SESSION_ACTIVE_{}' + _DATE_START_FIRST_HAS_REPLAY_RDP_SESSION = None + + def get_rel_replay_path(self, version=2): + """ + 获取session日志的文件路径 + :param version: 原来后缀是 .gz,为了统一新版本改为 .replay.gz + :return: + """ + suffix = '.replay.gz' + if version == 1: + suffix = '.gz' + date = self.date_start.strftime('%Y-%m-%d') + return os.path.join(date, str(self.id) + suffix) + + def get_local_path(self, version=2): + rel_path = self.get_rel_replay_path(version=version) + if version == 2: + local_path = os.path.join(self.upload_to, rel_path) + else: + local_path = rel_path + return local_path + + @property + def asset_obj(self): + return Asset.objects.get(id=self.asset_id) + + @property + def _date_start_first_has_replay_rdp_session(self): + if self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION is None: + instance = self.__class__.objects.filter( + protocol='rdp', has_replay=True + ).order_by('date_start').first() + if not instance: + date_start = timezone.now() - timezone.timedelta(days=365) + else: + date_start = instance.date_start + self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION = date_start + return self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION + + def can_replay(self): + if self.has_replay: + return True + if self.date_start < self._date_start_first_has_replay_rdp_session: + return True + return False + + @property + def can_join(self): + _PROTOCOL = self.PROTOCOL + if self.is_finished: + return False + if self.protocol in [_PROTOCOL.SSH, _PROTOCOL.TELNET, _PROTOCOL.K8S]: + return True + else: + return False + + @property + def db_protocols(self): + _PROTOCOL = self.PROTOCOL + return [_PROTOCOL.MYSQL, _PROTOCOL.MARIADB, _PROTOCOL.ORACLE, _PROTOCOL.POSTGRESQL] + + @property + def can_terminate(self): + _PROTOCOL = self.PROTOCOL + if self.is_finished: + return False + if self.protocol in self.db_protocols: + return False + else: + return True + + def save_replay_to_storage(self, f): + local_path = self.get_local_path() + try: + name = default_storage.save(local_path, f) + except OSError as e: + return None, e + + if settings.SERVER_REPLAY_STORAGE: + from .tasks import upload_session_replay_to_external_storage + upload_session_replay_to_external_storage.delay(str(self.id)) + return name, None + + @classmethod + def set_sessions_active(cls, sessions_id): + data = {cls.ACTIVE_CACHE_KEY_PREFIX.format(i): i for i in sessions_id} + cache.set_many(data, timeout=5*60) + + @classmethod + def get_active_sessions(cls): + return cls.objects.filter(is_finished=False) + + def is_active(self): + if self.protocol in ['ssh', 'telnet', 'rdp', 'mysql']: + key = self.ACTIVE_CACHE_KEY_PREFIX.format(self.id) + return bool(cache.get(key)) + return True + + @property + def command_amount(self): + command_store = get_multi_command_storage() + return command_store.count(session=str(self.id)) + + @property + def login_from_display(self): + return self.get_login_from_display() + + @classmethod + def generate_fake(cls, count=100, is_finished=True): + import random + from orgs.models import Organization + from users.models import User + from assets.models import Asset, SystemUser + from orgs.utils import get_current_org + from common.utils.random import random_datetime, random_ip + + org = get_current_org() + if not org or not org.is_real(): + Organization.default().change_to() + i = 0 + users = User.objects.all()[:100] + assets = Asset.objects.all()[:100] + system_users = SystemUser.objects.all()[:100] + while i < count: + user_random = random.choices(users, k=10) + assets_random = random.choices(assets, k=10) + system_users = random.choices(system_users, k=10) + + ziped = zip(user_random, assets_random, system_users) + sessions = [] + now = timezone.now() + month_ago = now - timezone.timedelta(days=30) + for user, asset, system_user in ziped: + ip = random_ip() + date_start = random_datetime(month_ago, now) + date_end = random_datetime(date_start, date_start+timezone.timedelta(hours=2)) + data = dict( + user=str(user), user_id=user.id, + asset=str(asset), asset_id=asset.id, + system_user=str(system_user), system_user_id=system_user.id, + remote_addr=ip, + date_start=date_start, + date_end=date_end, + is_finished=is_finished, + ) + sessions.append(Session(**data)) + cls.objects.bulk_create(sessions) + i += 10 + + class Meta: + db_table = "terminal_session" + ordering = ["-date_start"] + + def __str__(self): + return "{0.id} of {0.user} to {0.asset}".format(self) diff --git a/apps/terminal/models/status.py b/apps/terminal/models/status.py new file mode 100644 index 000000000..a0607e5dc --- /dev/null +++ b/apps/terminal/models/status.py @@ -0,0 +1,28 @@ +from __future__ import unicode_literals + +import uuid + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from .terminal import Terminal + + +class Status(models.Model): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + session_online = models.IntegerField(verbose_name=_("Session Online"), default=0) + cpu_used = models.FloatField(verbose_name=_("CPU Usage")) + memory_used = models.FloatField(verbose_name=_("Memory Used")) + connections = models.IntegerField(verbose_name=_("Connections")) + threads = models.IntegerField(verbose_name=_("Threads")) + boot_time = models.FloatField(verbose_name=_("Boot Time")) + terminal = models.ForeignKey(Terminal, null=True, on_delete=models.CASCADE) + date_created = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = 'terminal_status' + get_latest_by = 'date_created' + + def __str__(self): + return self.date_created.strftime("%Y-%m-%d %H:%M:%S") + diff --git a/apps/terminal/models/storage.py b/apps/terminal/models/storage.py new file mode 100644 index 000000000..66fbe393d --- /dev/null +++ b/apps/terminal/models/storage.py @@ -0,0 +1,103 @@ +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 + + +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 + ) + meta = EncryptJsonDictTextField(default={}) + comment = models.TextField( + max_length=128, default='', blank=True, verbose_name=_('Comment') + ) + + def __str__(self): + return self.name + + @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(): + return True + storage = jms_storage.get_log_storage(self.config) + return storage.ping() + + def is_using(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 + ) + meta = EncryptJsonDictTextField(default={}) + comment = models.TextField( + max_length=128, default='', blank=True, verbose_name=_('Comment') + ) + + def __str__(self): + return self.name + + def convert_type(self): + s3_type_list = [const.REPLAY_STORAGE_TYPE_CEPH] + tp = self.type + if tp in s3_type_list: + tp = const.REPLAY_STORAGE_TYPE_S3 + return tp + + 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 config(self): + config = self.meta + extra_config = self.get_extra_config() + config.update(extra_config) + return config + + def in_defaults(self): + return self.type in self.TYPE_DEFAULTS + + def is_valid(self): + if self.in_defaults(): + 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): + return Terminal.objects.filter(replay_storage=self.name).exists() diff --git a/apps/terminal/models/task.py b/apps/terminal/models/task.py new file mode 100644 index 000000000..c863c9f77 --- /dev/null +++ b/apps/terminal/models/task.py @@ -0,0 +1,25 @@ +from __future__ import unicode_literals + +import uuid + +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from .terminal import Terminal + + +class Task(models.Model): + NAME_CHOICES = ( + ("kill_session", "Kill Session"), + ) + + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + name = models.CharField(max_length=128, choices=NAME_CHOICES, verbose_name=_("Name")) + args = models.CharField(max_length=1024, verbose_name=_("Args")) + terminal = models.ForeignKey(Terminal, null=True, on_delete=models.SET_NULL) + is_finished = models.BooleanField(default=False) + date_created = models.DateTimeField(auto_now_add=True) + date_finished = models.DateTimeField(null=True) + + class Meta: + db_table = "terminal_task" + diff --git a/apps/terminal/models/terminal.py b/apps/terminal/models/terminal.py new file mode 100644 index 000000000..fb7a1dc24 --- /dev/null +++ b/apps/terminal/models/terminal.py @@ -0,0 +1,247 @@ +from __future__ import unicode_literals +import uuid + +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.conf import settings +from django.core.cache import cache + +from users.models import User +from .. import const + + +class ComputeStatusMixin: + + # system status + @staticmethod + def _common_compute_system_status(value, thresholds): + if thresholds[0] <= value <= thresholds[1]: + return const.ComponentStatusChoices.normal.value + elif thresholds[1] < value <= thresholds[2]: + return const.ComponentStatusChoices.high.value + else: + return const.ComponentStatusChoices.critical.value + + def _compute_system_cpu_load_1_status(self, value): + thresholds = [0, 5, 20] + return self._common_compute_system_status(value, thresholds) + + def _compute_system_memory_used_percent_status(self, value): + thresholds = [0, 85, 95] + return self._common_compute_system_status(value, thresholds) + + def _compute_system_disk_used_percent_status(self, value): + thresholds = [0, 80, 99] + return self._common_compute_system_status(value, thresholds) + + def _compute_system_status(self, state): + system_status_keys = [ + 'system_cpu_load_1', 'system_memory_used_percent', 'system_disk_used_percent' + ] + system_status = [] + for system_status_key in system_status_keys: + state_value = state[system_status_key] + status = getattr(self, f'_compute_{system_status_key}_status')(state_value) + system_status.append(status) + return system_status + + def _compute_component_status(self, state): + system_status = self._compute_system_status(state) + if const.ComponentStatusChoices.critical in system_status: + return const.ComponentStatusChoices.critical + elif const.ComponentStatusChoices.high in system_status: + return const.ComponentStatusChoices.high + else: + return const.ComponentStatusChoices.normal + + @staticmethod + def _compute_component_status_display(status): + return getattr(const.ComponentStatusChoices, status).label + + +class TerminalStateMixin(ComputeStatusMixin): + CACHE_KEY_COMPONENT_STATE = 'CACHE_KEY_COMPONENT_STATE_TERMINAL_{}' + CACHE_TIMEOUT = 120 + + @property + def cache_key(self): + return self.CACHE_KEY_COMPONENT_STATE.format(str(self.id)) + + # get + def _get_from_cache(self): + return cache.get(self.cache_key) + + def _set_to_cache(self, state): + cache.set(self.cache_key, state, self.CACHE_TIMEOUT) + + # set + def _add_status(self, state): + status = self._compute_component_status(state) + status_display = self._compute_component_status_display(status) + state.update({ + 'status': status, + 'status_display': status_display + }) + + @property + def state(self): + state = self._get_from_cache() + return state or {} + + @state.setter + def state(self, state): + self._add_status(state) + self._set_to_cache(state) + + +class TerminalStatusMixin(TerminalStateMixin): + + # alive + @property + def is_alive(self): + return bool(self.state) + + # status + @property + def status(self): + if self.is_alive: + return self.state['status'] + else: + return const.ComponentStatusChoices.critical.value + + @property + def status_display(self): + return self._compute_component_status_display(self.status) + + @property + def is_normal(self): + return self.status == const.ComponentStatusChoices.normal.value + + @property + def is_high(self): + return self.status == const.ComponentStatusChoices.high.value + + @property + def is_critical(self): + return self.status == const.ComponentStatusChoices.critical.value + + +class Terminal(TerminalStatusMixin, models.Model): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + name = models.CharField(max_length=128, verbose_name=_('Name')) + type = models.CharField(choices=const.TerminalTypeChoices.choices, max_length=64, verbose_name=_('type')) + remote_addr = models.CharField(max_length=128, blank=True, verbose_name=_('Remote Address')) + ssh_port = models.IntegerField(verbose_name=_('SSH Port'), default=2222) + http_port = models.IntegerField(verbose_name=_('HTTP Port'), default=5000) + command_storage = models.CharField(max_length=128, verbose_name=_("Command storage"), default='default') + replay_storage = models.CharField(max_length=128, verbose_name=_("Replay storage"), default='default') + user = models.OneToOneField(User, related_name='terminal', verbose_name='Application User', null=True, on_delete=models.CASCADE) + is_accepted = models.BooleanField(default=False, verbose_name='Is Accepted') + is_deleted = models.BooleanField(default=False) + date_created = models.DateTimeField(auto_now_add=True) + comment = models.TextField(blank=True, verbose_name=_('Comment')) + + @property + def is_active(self): + if self.user and self.user.is_active: + return True + return False + + @is_active.setter + def is_active(self, active): + if self.user: + self.user.is_active = active + self.user.save() + + def get_command_storage(self): + from .storage import CommandStorage + storage = CommandStorage.objects.filter(name=self.command_storage).first() + return storage + + def get_command_storage_config(self): + s = self.get_command_storage() + if s: + config = s.config + else: + config = settings.DEFAULT_TERMINAL_COMMAND_STORAGE + return config + + def get_command_storage_setting(self): + config = self.get_command_storage_config() + return {"TERMINAL_COMMAND_STORAGE": config} + + def get_replay_storage(self): + from .storage import ReplayStorage + storage = ReplayStorage.objects.filter(name=self.replay_storage).first() + return storage + + def get_replay_storage_config(self): + s = self.get_replay_storage() + if s: + config = s.config + else: + config = settings.DEFAULT_TERMINAL_REPLAY_STORAGE + return config + + def get_replay_storage_setting(self): + config = self.get_replay_storage_config() + return {"TERMINAL_REPLAY_STORAGE": config} + + @staticmethod + def get_login_title_setting(): + login_title = None + if settings.XPACK_ENABLED: + from xpack.plugins.interface.models import Interface + login_title = Interface.get_login_title() + return {'TERMINAL_HEADER_TITLE': login_title} + + @property + def config(self): + configs = {} + for k in dir(settings): + if not k.startswith('TERMINAL'): + continue + configs[k] = getattr(settings, k) + configs.update(self.get_command_storage_setting()) + configs.update(self.get_replay_storage_setting()) + configs.update(self.get_login_title_setting()) + configs.update({ + 'SECURITY_MAX_IDLE_TIME': settings.SECURITY_MAX_IDLE_TIME + }) + return configs + + @property + def service_account(self): + return self.user + + def create_app_user(self): + random = uuid.uuid4().hex[:6] + user, access_key = User.create_app_user( + name="{}-{}".format(self.name, random), comment=self.comment + ) + self.user = user + self.save() + return user, access_key + + def delete(self, using=None, keep_parents=False): + if self.user: + self.user.delete() + self.user = None + self.is_deleted = True + self.save() + return + + def __str__(self): + status = "Active" + if not self.is_accepted: + status = "NotAccept" + elif self.is_deleted: + status = "Deleted" + elif not self.is_active: + status = "Disable" + return '%s: %s' % (self.name, status) + + class Meta: + ordering = ('is_accepted',) + db_table = "terminal" + diff --git a/apps/terminal/serializers/__init__.py b/apps/terminal/serializers/__init__.py index f1714dc21..e958d7955 100644 --- a/apps/terminal/serializers/__init__.py +++ b/apps/terminal/serializers/__init__.py @@ -4,3 +4,4 @@ from .terminal import * from .session import * from .storage import * from .command import * +from .components import * diff --git a/apps/terminal/serializers/components.py b/apps/terminal/serializers/components.py new file mode 100644 index 000000000..7cc1612ad --- /dev/null +++ b/apps/terminal/serializers/components.py @@ -0,0 +1,25 @@ + +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + + +class ComponentsStateSerializer(serializers.Serializer): + # system + system_cpu_load_1 = serializers.FloatField( + required=False, default=0, label=_("System cpu load 1 minutes") + ) + system_memory_used_percent = serializers.FloatField( + required=False, default=0, label=_('System memory used percent') + ) + system_disk_used_percent = serializers.FloatField( + required=False, default=0, label=_('System disk used percent') + ) + # sessions + session_active_count = serializers.IntegerField( + required=False, default=0, label=_("Session active count") + ) + + def save(self, **kwargs): + request = self.context['request'] + terminal = request.user.terminal + terminal.state = self.validated_data diff --git a/apps/terminal/serializers/terminal.py b/apps/terminal/serializers/terminal.py index 896c44440..ef285c869 100644 --- a/apps/terminal/serializers/terminal.py +++ b/apps/terminal/serializers/terminal.py @@ -6,19 +6,25 @@ from common.utils import is_uuid from ..models import ( Terminal, Status, Session, Task, CommandStorage, ReplayStorage ) +from .components import ComponentsStateSerializer class TerminalSerializer(BulkModelSerializer): session_online = serializers.SerializerMethodField() is_alive = serializers.BooleanField(read_only=True) + status = serializers.CharField(read_only=True) + status_display = serializers.CharField(read_only=True) + state = ComponentsStateSerializer(read_only=True) class Meta: model = Terminal fields = [ - 'id', 'name', 'remote_addr', 'http_port', 'ssh_port', + 'id', 'name', 'type', 'remote_addr', 'http_port', 'ssh_port', 'comment', 'is_accepted', "is_active", 'session_online', - 'is_alive', 'date_created', 'command_storage', 'replay_storage' + 'is_alive', 'date_created', 'command_storage', 'replay_storage', + 'status', 'status_display', 'state' ] + read_only_fields = ['type', 'date_created'] @staticmethod def get_kwargs_may_be_uuid(value): diff --git a/apps/terminal/urls/api_urls.py b/apps/terminal/urls/api_urls.py index d1c860f03..5a8efe8e8 100644 --- a/apps/terminal/urls/api_urls.py +++ b/apps/terminal/urls/api_urls.py @@ -33,7 +33,10 @@ urlpatterns = [ path('commands/export/', api.CommandExportApi.as_view(), name="command-export"), path('commands/insecure-command/', api.InsecureCommandAlertAPI.as_view(), name="command-alert"), path('replay-storages//test-connective/', api.ReplayStorageTestConnectiveApi.as_view(), name='replay-storage-test-connective'), - path('command-storages//test-connective/', api.CommandStorageTestConnectiveApi.as_view(), name='command-storage-test-connective') + path('command-storages//test-connective/', api.CommandStorageTestConnectiveApi.as_view(), name='command-storage-test-connective'), + # components + path('components/metrics/', api.ComponentsMetricsAPIView.as_view(), name='components-metrics'), + path('components/state/', api.ComponentsStateAPIView.as_view(), name='components-state'), # v2: get session's replay # path('v2/sessions//replay/', # api.SessionReplayV2ViewSet.as_view({'get': 'retrieve'}), diff --git a/apps/terminal/utils.py b/apps/terminal/utils.py index 9c87695d0..68456dfbe 100644 --- a/apps/terminal/utils.py +++ b/apps/terminal/utils.py @@ -11,6 +11,7 @@ import jms_storage from common.tasks import send_mail_async from common.utils import get_logger, reverse from settings.models import Setting +from . import const from .models import ReplayStorage, Session, Command @@ -101,3 +102,104 @@ def send_command_alert_mail(command): logger.debug(message) send_mail_async.delay(subject, message, recipient_list, html_message=message) + + +class ComponentsMetricsUtil(object): + + def __init__(self, component_type=None): + self.type = component_type + self.components = [] + self.initial_components() + + def initial_components(self): + from .models import Terminal + terminals = Terminal.objects.all().order_by('type') + if self.type: + terminals = terminals.filter(type=self.type) + self.components = list(terminals) + + def get_metrics(self): + total_count = normal_count = high_count = critical_count = session_active_total = 0 + for component in self.components: + total_count += 1 + if not component.is_alive: + critical_count += 1 + continue + session_active_total += component.state.get('session_active_count', 0) + if component.is_normal: + normal_count += 1 + elif component.is_high: + high_count += 1 + else: + critical_count += 1 + metrics = { + 'total': total_count, + 'normal': normal_count, + 'high': high_count, + 'critical': critical_count, + 'session_active': session_active_total + } + return metrics + + +class ComponentsPrometheusMetricsUtil(ComponentsMetricsUtil): + + @staticmethod + def get_status_metrics(metrics): + return { + 'any': metrics['total'], + 'normal': metrics['normal'], + 'high': metrics['high'], + 'critical': metrics['critical'] + } + + def get_prometheus_metrics_text(self): + prometheus_metrics = [] + prometheus_metrics.append('# JumpServer 各组件状态个数汇总') + base_status_metric_text = 'jumpserver_components_status_total{component_type="%s", status="%s"} %s' + for component in self.components: + component_type = component.type + base_metrics = self.get_metrics() + + prometheus_metrics.append(f'## 组件: {component_type}') + status_metrics = self.get_status_metrics(base_metrics) + for status, value in status_metrics.items(): + metric_text = base_status_metric_text % (component_type, status, value) + prometheus_metrics.append(metric_text) + + prometheus_metrics.append('\n') + prometheus_metrics.append('# JumpServer 各组件在线会话数汇总') + base_session_active_metric_text = 'jumpserver_components_session_active_total{component_type="%s"} %s' + for component in self.components: + component_type = component.type + prometheus_metrics.append(f'## 组件: {component_type}') + base_metrics = self.get_metrics() + metric_text = base_session_active_metric_text % ( + component_type, + base_metrics['session_active'] + ) + prometheus_metrics.append(metric_text) + + prometheus_metrics.append('\n') + prometheus_metrics.append('# JumpServer 各组件节点一些指标') + base_system_state_metric_text = 'jumpserver_components_%s{component_type="%s", component="%s"} %s' + system_states_name = [ + 'system_cpu_load_1', 'system_memory_used_percent', + 'system_disk_used_percent', 'session_active_count' + ] + for system_state_name in system_states_name: + prometheus_metrics.append(f'## 指标: {system_state_name}') + for component in self.components: + if not component.is_alive: + continue + component_type = component.type + metric_text = base_system_state_metric_text % ( + system_state_name, + component_type, + component.name, + component.state.get(system_state_name) + ) + prometheus_metrics.append(metric_text) + + prometheus_metrics_text = '\n'.join(prometheus_metrics) + return prometheus_metrics_text From 2176fd8facaede5990bd9491e3f2b32581e95fb0 Mon Sep 17 00:00:00 2001 From: Bai Date: Thu, 10 Dec 2020 21:28:04 +0800 Subject: [PATCH 57/57] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E7=BF=BB?= =?UTF-8?q?=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/locale/zh/LC_MESSAGES/django.mo | Bin 62898 -> 63498 bytes apps/locale/zh/LC_MESSAGES/django.po | 217 +++++++++++++----------- apps/terminal/serializers/components.py | 2 +- 3 files changed, 122 insertions(+), 97 deletions(-) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 73c8027155d078501eab4e5df9ae031abf1d8c1d..0d5b71dab2dd5a9aca56c52d83db4f6fe53654c8 100644 GIT binary patch delta 19134 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%0DjWE5 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%0D`Qf6YcNW*B?(8fDK{v;{X+Bd?&q@-3qOaJWJ z!zxR@9}xp+u$+$XuT5ks5UWf&M*IwYy8D-(fm{f+r|}voH~CwnIOG?SqW&|=YRbA6 z6U)I!J;=8s7LCnR!Sw<0j>O7ZTNiadM?+5fhETVUls=Guf9Y!tYtr$}e_NAJOXH2y zKO^NLe}>p2^66;%%G&-yye9ct#NJ;^i2Y8x*Z36kQ?VCkom^fr_L~|3;jz2AJqwE6 z-0#QKu`?!Ii8ISL>3;k!eS3Y{yWgyRlfR7}KjmOx?8F)Slf^cSX&4r}dVh_e*jGnh urj8A`elE_e>o=|>oW1JS_-(g$jhU6<=8@QkHv\n" "Language-Team: JumpServer team\n" @@ -39,9 +39,10 @@ msgstr "远程应用" #: 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.py:28 terminal/models.py:372 terminal/models.py:404 -#: terminal/models.py:441 users/forms/profile.py:20 users/models/group.py:15 -#: users/models/user.py:495 users/templates/users/_select_user_modal.html:13 +#: terminal/models/storage.py:21 terminal/models/storage.py:58 +#: 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/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 #: users/templates/users/user_database_app_permission.html:36 @@ -76,8 +77,9 @@ msgstr "分类" #: applications/serializers/application.py:17 assets/models/cmd_filter.py:52 #: perms/models/application_permission.py:20 #: perms/serializers/application/permission.py:17 -#: perms/serializers/application/user_permission.py:34 terminal/models.py:406 -#: terminal/models.py:443 tickets/models/ticket.py:40 +#: perms/serializers/application/user_permission.py:34 +#: terminal/models/storage.py:23 terminal/models/storage.py:60 +#: tickets/models/ticket.py:40 #: users/templates/users/user_granted_database_app.html:35 msgid "Type" msgstr "类型" @@ -92,10 +94,10 @@ msgstr "类型" #: assets/models/cmd_filter.py:57 assets/models/domain.py:22 #: assets/models/domain.py:55 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.py:38 -#: terminal/models.py:411 terminal/models.py:448 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:28 +#: terminal/models/storage.py:65 terminal/models/terminal.py:142 +#: tickets/models/ticket.py:43 users/models/group.py:16 +#: users/models/user.py:528 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 @@ -154,7 +156,7 @@ msgstr "Kubernetes应用" #: audits/models.py:38 perms/forms/asset_permission.py:89 #: perms/models/asset_permission.py:92 templates/index.html:82 #: terminal/backends/command/models.py:19 -#: terminal/backends/command/serializers.py:13 terminal/models.py:192 +#: terminal/backends/command/serializers.py:13 terminal/models/session.py:39 #: users/templates/users/user_asset_permission.html:40 #: users/templates/users/user_asset_permission.html:70 #: users/templates/users/user_granted_remote_app.html:36 @@ -296,15 +298,15 @@ msgstr "删除失败,存在关联资产" msgid "Number required" msgstr "需要为数字" -#: assets/api/node.py:66 +#: assets/api/node.py:67 msgid "You can't update the root node name" msgstr "不能修改根节点名称" -#: assets/api/node.py:73 +#: assets/api/node.py:74 msgid "You can't delete the root node ({})" msgstr "不能删除根节点 ({})" -#: assets/api/node.py:76 +#: assets/api/node.py:77 msgid "Deletion failed and the node contains children or assets" msgstr "删除失败,节点包含子节点或资产" @@ -528,7 +530,7 @@ msgid "Regex" msgstr "正则表达式" #: assets/models/cmd_filter.py:41 ops/models/command.py:23 -#: terminal/backends/command/serializers.py:15 terminal/models.py:201 +#: terminal/backends/command/serializers.py:15 terminal/models/session.py:48 msgid "Command" msgstr "命令" @@ -618,7 +620,7 @@ msgstr "默认资产组" #: perms/forms/remote_app_permission.py:40 perms/models/asset_permission.py:169 #: perms/models/base.py:49 templates/index.html:78 #: terminal/backends/command/models.py:18 -#: terminal/backends/command/serializers.py:12 terminal/models.py:190 +#: 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:66 #: tickets/serializers/ticket.py:31 users/forms/group.py:15 @@ -736,7 +738,7 @@ msgstr "用户组" #: perms/models/k8s_app_permission.py:22 #: perms/models/remote_app_permission.py:16 templates/_nav.html:45 #: terminal/backends/command/models.py:20 -#: terminal/backends/command/serializers.py:14 terminal/models.py:194 +#: terminal/backends/command/serializers.py:14 terminal/models/session.py:41 #: tickets/api/request_asset_perm.py:68 #: tickets/serializers/request_asset_perm.py:27 #: users/templates/users/_granted_assets.html:27 @@ -937,7 +939,7 @@ msgstr "更新节点资产硬件信息: {}" msgid "Gather assets users" msgstr "收集资产上的用户" -#: assets/tasks/nodes_amount.py:19 +#: assets/tasks/nodes_amount.py:21 msgid "" "The task of self-checking is already running and cannot be started repeatedly" msgstr "自检程序已经在运行,不能重复启动" @@ -1040,7 +1042,7 @@ msgid "Symlink" msgstr "建立软链接" #: audits/models.py:37 audits/models.py:60 audits/models.py:71 -#: terminal/models.py:197 +#: terminal/models/session.py:44 msgid "Remote addr" msgstr "远端地址" @@ -1058,7 +1060,7 @@ msgid "Success" msgstr "成功" #: audits/models.py:43 ops/models/command.py:28 perms/models/base.py:52 -#: terminal/models.py:204 tickets/serializers/request_asset_perm.py:29 +#: terminal/models/session.py:51 tickets/serializers/request_asset_perm.py:29 #: xpack/plugins/change_auth_plan/models.py:177 #: xpack/plugins/change_auth_plan/models.py:307 #: xpack/plugins/gathered_user/models.py:76 @@ -1353,7 +1355,7 @@ msgstr "你的密码过于简单,为了安全,请修改" msgid "Your password has expired, please reset before logging in" msgstr "您的密码已过期,先修改再登录" -#: authentication/forms.py:26 authentication/forms.py:34 +#: authentication/forms.py:26 authentication/forms.py:38 #: authentication/templates/authentication/login.html:39 #: authentication/templates/authentication/xpack_login.html:119 #: users/forms/user.py:199 @@ -1440,6 +1442,15 @@ msgstr "删除成功" msgid "Close" msgstr "关闭" +#: authentication/templates/authentication/_captcha_field.html:8 +msgid "Play CAPTCHA as audio file" +msgstr "语言播放验证码" + +#: authentication/templates/authentication/_captcha_field.html:15 +#: users/forms/profile.py:90 +msgid "Captcha" +msgstr "验证码" + #: authentication/templates/authentication/_mfa_confirm_modal.html:5 msgid "MFA confirm" msgstr "多因子认证校验" @@ -1584,7 +1595,7 @@ msgstr "更新人" #: common/drf/parsers/base.py:16 msgid "The file content overflowed (The maximum length `{}` bytes)" -msgstr "" +msgstr "文件内容益处 (最大长度 `{}` 字节)" #: common/exceptions.py:15 #, python-format @@ -2504,14 +2515,6 @@ msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项" msgid "Home page" msgstr "首页" -#: templates/captcha/image.html:3 -msgid "Play CAPTCHA as audio file" -msgstr "语言播放验证码" - -#: templates/captcha/text_field.html:4 users/forms/profile.py:90 -msgid "Captcha" -msgstr "验证码" - #: templates/delete_confirm.html:6 msgid "Confirm delete" msgstr "确认删除" @@ -2741,70 +2744,105 @@ msgstr "风险等级(显示名称)" msgid "Timestamp" msgstr "时间戳" +#: terminal/const.py:117 +msgid "Critical" +msgstr "严重" + +#: terminal/const.py:118 +msgid "High" +msgstr "较高" + +#: terminal/const.py:119 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 +msgid "Normal" +msgstr "正常" + #: terminal/exceptions.py:8 msgid "Bulk create not support" msgstr "不支持批量创建" -#: terminal/models.py:29 -msgid "Remote Address" -msgstr "远端地址" - -#: terminal/models.py:30 -msgid "SSH Port" -msgstr "SSH端口" - -#: terminal/models.py:31 -msgid "HTTP Port" -msgstr "HTTP端口" - -#: terminal/models.py:32 -msgid "Command storage" -msgstr "命令存储" - -#: terminal/models.py:33 -msgid "Replay storage" -msgstr "录像存储" - -#: terminal/models.py:156 -msgid "Session Online" -msgstr "在线会话" - -#: terminal/models.py:157 -msgid "CPU Usage" -msgstr "CPU使用" - -#: terminal/models.py:158 -msgid "Memory Used" -msgstr "内存使用" - -#: terminal/models.py:159 -msgid "Connections" -msgstr "连接数" - -#: terminal/models.py:160 -msgid "Threads" -msgstr "线程数" - -#: terminal/models.py:161 -msgid "Boot Time" -msgstr "运行时间" - -#: terminal/models.py:196 +#: terminal/models/session.py:43 msgid "Login from" msgstr "登录来源" -#: terminal/models.py:200 +#: terminal/models/session.py:47 msgid "Replay" msgstr "回放" -#: terminal/models.py:205 +#: terminal/models/session.py:52 msgid "Date end" msgstr "结束日期" -#: terminal/models.py:373 +#: terminal/models/status.py:13 +msgid "Session Online" +msgstr "在线会话" + +#: terminal/models/status.py:14 +msgid "CPU Usage" +msgstr "CPU使用" + +#: terminal/models/status.py:15 +msgid "Memory Used" +msgstr "内存使用" + +#: terminal/models/status.py:16 +msgid "Connections" +msgstr "连接数" + +#: terminal/models/status.py:17 +msgid "Threads" +msgstr "线程数" + +#: terminal/models/status.py:18 +msgid "Boot Time" +msgstr "运行时间" + +#: terminal/models/task.py:17 msgid "Args" msgstr "参数" +#: terminal/models/terminal.py:132 +msgid "type" +msgstr "类型" + +#: terminal/models/terminal.py:133 +msgid "Remote Address" +msgstr "远端地址" + +#: terminal/models/terminal.py:134 +msgid "SSH Port" +msgstr "SSH端口" + +#: terminal/models/terminal.py:135 +msgid "HTTP Port" +msgstr "HTTP端口" + +#: terminal/models/terminal.py:136 +msgid "Command storage" +msgstr "命令存储" + +#: terminal/models/terminal.py:137 +msgid "Replay storage" +msgstr "录像存储" + +#: terminal/serializers/components.py:9 +msgid "System cpu load (1 minutes)" +msgstr "系统CPU负载 (1分钟)" + +#: terminal/serializers/components.py:12 +msgid "System memory used percent" +msgstr "系统内存使用百分比" + +#: terminal/serializers/components.py:15 +msgid "System disk used percent" +msgstr "系统磁盘使用百分比" + +#: terminal/serializers/components.py:19 +msgid "Session active count" +msgstr "活跃会话数量" + #: terminal/serializers/session.py:30 msgid "User ID" msgstr "用户 ID" @@ -2829,18 +2867,18 @@ msgstr "是否可重放" msgid "Can join" msgstr "是否可加入" -#: terminal/serializers/terminal.py:38 terminal/serializers/terminal.py:46 +#: terminal/serializers/terminal.py:44 terminal/serializers/terminal.py:52 msgid "Not found" msgstr "没有发现" -#: terminal/utils.py:73 +#: terminal/utils.py:74 #, python-format msgid "" "Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $" "%(command)s" msgstr "危险命令告警: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s" -#: terminal/utils.py:80 +#: terminal/utils.py:81 #, python-format msgid "" "\n" @@ -3454,13 +3492,6 @@ msgstr "很弱" msgid "Weak" msgstr "弱" -#: 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 -msgid "Normal" -msgstr "正常" - #: users/templates/users/reset_password.html:51 #: users/templates/users/user_create.html:36 #: users/templates/users/user_password_update.html:105 @@ -4415,11 +4446,11 @@ msgstr "腾讯云" #: xpack/plugins/cloud/serializers.py:26 msgid "Tenant ID" -msgstr "" +msgstr "租户ID" #: xpack/plugins/cloud/serializers.py:30 msgid "Subscription ID" -msgstr "" +msgstr "订阅ID" #: xpack/plugins/cloud/serializers.py:88 msgid "History count" @@ -5066,9 +5097,6 @@ msgstr "社区版" #~ msgid "System user assets" #~ msgstr "系统用户关联资产" -#~ msgid "System user users" -#~ msgstr "系统用户关联用户" - #~ msgid "Select user" #~ msgstr "选择用户" @@ -5521,9 +5549,6 @@ msgstr "社区版" #~ msgid "List page size" #~ msgstr "资产分页每页数量" -#~ msgid "Session keep duration" -#~ msgstr "会话保留时长" - #~ msgid "" #~ "Units: days, Session, record, command will be delete if more than " #~ "duration, only in database" diff --git a/apps/terminal/serializers/components.py b/apps/terminal/serializers/components.py index 7cc1612ad..e557d8f4f 100644 --- a/apps/terminal/serializers/components.py +++ b/apps/terminal/serializers/components.py @@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _ class ComponentsStateSerializer(serializers.Serializer): # system system_cpu_load_1 = serializers.FloatField( - required=False, default=0, label=_("System cpu load 1 minutes") + required=False, default=0, label=_("System cpu load (1 minutes)") ) system_memory_used_percent = serializers.FloatField( required=False, default=0, label=_('System memory used percent')