From 353b66bf8f80281db57fb1eeeacbe66d3cf202c7 Mon Sep 17 00:00:00 2001
From: ibuler <ibuler@qq.com>
Date: Thu, 11 Nov 2021 19:07:13 +0800
Subject: [PATCH 01/25] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=88=9B?=
 =?UTF-8?q?=E5=BB=BAtoken=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/authentication/api/token.py     |  2 +-
 apps/authentication/serializers.py   | 10 ++++++++--
 apps/locale/zh/LC_MESSAGES/django.mo |  4 ++--
 apps/locale/zh/LC_MESSAGES/django.po |  8 ++++----
 4 files changed, 15 insertions(+), 9 deletions(-)

diff --git a/apps/authentication/api/token.py b/apps/authentication/api/token.py
index d8e8eb6fc..c46c4d5e2 100644
--- a/apps/authentication/api/token.py
+++ b/apps/authentication/api/token.py
@@ -33,8 +33,8 @@ class TokenCreateApi(AuthMixin, CreateAPIView):
             self.check_user_mfa_if_need(user)
             self.check_user_login_confirm_if_need(user)
             self.send_auth_signal(success=True, user=user)
-            self.clear_auth_mark()
             resp = super().create(request, *args, **kwargs)
+            self.clear_auth_mark()
             return resp
         except errors.AuthFailedError as e:
             return Response(e.as_data(), status=400)
diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py
index a87e1e942..44fea3242 100644
--- a/apps/authentication/serializers.py
+++ b/apps/authentication/serializers.py
@@ -54,9 +54,9 @@ class BearerTokenSerializer(serializers.Serializer):
         user.last_login = timezone.now()
         user.save(update_fields=['last_login'])
 
-    def create(self, validated_data):
+    def get_request_user(self):
         request = self.context.get('request')
-        if request.user and not request.user.is_anonymous:
+        if request.user and request.user.is_authenticated:
             user = request.user
         else:
             user_id = request.session.get('user_id')
@@ -65,6 +65,12 @@ class BearerTokenSerializer(serializers.Serializer):
                 raise serializers.ValidationError(
                     "user id {} not exist".format(user_id)
                 )
+        return user
+
+    def create(self, validated_data):
+        request = self.context.get('request')
+        user = self.get_request_user()
+
         token, date_expired = user.create_bearer_token(request)
         self.update_last_login(user)
 
diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo
index 03f88c57e..2aec9545b 100644
--- a/apps/locale/zh/LC_MESSAGES/django.mo
+++ b/apps/locale/zh/LC_MESSAGES/django.mo
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:925c5a219a4ee6835ad59e3b8e9f7ea5074ee3df6527c0f73ef1a50eaedaf59c
-size 91777
+oid sha256:fd23c3a1a8f833e99937d38db94b5e32b308c9c1912f8b258ff009d96f9bf2bb
+size 92755
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index 4305720a7..e5071bf14 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -3240,7 +3240,7 @@ msgstr ""
 
 #: settings/serializers/other.py:24
 msgid "Shell (Windows)"
-msgstr "Shell(Windows 资产)"
+msgstr "Windows shell"
 
 #: settings/serializers/other.py:25
 msgid "The shell type used when Windows assets perform ansible tasks"
@@ -3248,15 +3248,15 @@ msgstr "windows 资产执行 Ansible 任务时,使用的 Shell 类型。"
 
 #: settings/serializers/other.py:29
 msgid "Perm ungroup node"
-msgstr "授权未分组节点"
+msgstr "显示未分组节点"
 
 #: settings/serializers/other.py:30
 msgid "Perm single to ungroup node"
-msgstr "授权未分组节点"
+msgstr "放置单独授权的资产到未分组节点, 避免能看到资产所在节点,但该节点未被授权的问题"
 
 #: settings/serializers/other.py:34
 msgid "Help Docs URL"
-msgstr ""
+msgstr "文档链接"
 
 #: settings/serializers/other.py:35
 msgid "default: http://docs.jumpserver.org"

From 90477146edd5cecf61299df451119a35ced2c8a4 Mon Sep 17 00:00:00 2001
From: feng626 <1304903146@qq.com>
Date: Thu, 11 Nov 2021 19:03:01 +0800
Subject: [PATCH 02/25] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=85=A8?=
 =?UTF-8?q?=E5=B1=80ip=E9=BB=91=E5=90=8D=E5=8D=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/authentication/errors.py         |  8 +++++
 apps/authentication/mixins.py         | 14 ++++++---
 apps/authentication/views/login.py    |  3 +-
 apps/jumpserver/conf.py               |  1 +
 apps/jumpserver/settings/custom.py    |  1 +
 apps/locale/zh/LC_MESSAGES/django.po  | 10 ++++++
 apps/settings/serializers/security.py | 45 ++++++++++++++++++++++-----
 apps/users/utils.py                   | 35 +++++++++++++++++++--
 8 files changed, 103 insertions(+), 14 deletions(-)

diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py
index 19b13ab8e..1e9000e67 100644
--- a/apps/authentication/errors.py
+++ b/apps/authentication/errors.py
@@ -180,6 +180,14 @@ class BlockLoginError(AuthFailedNeedBlockMixin, AuthFailedError):
         super().__init__(username=username, ip=ip)
 
 
+class BlockGlobalIpLoginError(AuthFailedError):
+    error = 'block_global_ip_login'
+
+    def __init__(self, username, ip):
+        self.msg = _("IP is not allowed")
+        super().__init__(username=username, ip=ip)
+
+
 class SessionEmptyError(AuthFailedError):
     msg = session_empty_msg
     error = 'session_empty'
diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py
index a7d845662..69daf6330 100644
--- a/apps/authentication/mixins.py
+++ b/apps/authentication/mixins.py
@@ -8,7 +8,6 @@ from typing import Callable
 from django.utils.http import urlencode
 from django.core.cache import cache
 from django.conf import settings
-from django.urls import reverse_lazy
 from django.contrib import auth
 from django.utils.translation import ugettext as _
 from rest_framework.request import Request
@@ -18,10 +17,10 @@ from django.contrib.auth import (
 )
 from django.shortcuts import reverse, redirect, get_object_or_404
 
-from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil
+from common.utils import get_request_ip, get_logger, bulk_get, FlashMessageUtil
 from acls.models import LoginACL
 from users.models import User
-from users.utils import LoginBlockUtil, MFABlockUtils
+from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil
 from . import errors
 from .utils import rsa_decrypt, gen_key_pair
 from .signals import post_auth_success, post_auth_failed
@@ -76,7 +75,9 @@ def authenticate(request=None, **credentials):
         return user
 
     # The credentials supplied are invalid to all backends, fire signal
-    user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request)
+    user_login_failed.send(
+        sender=__name__, credentials=_clean_credentials(credentials), request=request
+    )
 
 
 auth.authenticate = authenticate
@@ -209,6 +210,10 @@ class AuthPreCheckMixin:
 
     def _check_is_block(self, username, raise_exception=True):
         ip = self.get_request_ip()
+
+        if LoginIpBlockUtil(ip).is_block():
+            raise errors.BlockGlobalIpLoginError(username=username, ip=ip)
+
         is_block = LoginBlockUtil(username, ip).is_block()
         if not is_block:
             return
@@ -224,6 +229,7 @@ class AuthPreCheckMixin:
             username = self.request.data.get("username")
         else:
             username = self.request.POST.get("username")
+
         self._check_is_block(username, raise_exception)
 
     def _check_only_allow_exists_user_auth(self, username):
diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py
index e14f47256..79b6839b4 100644
--- a/apps/authentication/views/login.py
+++ b/apps/authentication/views/login.py
@@ -133,7 +133,8 @@ class UserLoginView(mixins.AuthMixin, FormView):
                 errors.BlockMFAError,
                 errors.MFACodeRequiredError,
                 errors.SMSCodeRequiredError,
-                errors.UserPhoneNotSet
+                errors.UserPhoneNotSet,
+                errors.BlockGlobalIpLoginError
         ) as e:
             form.add_error('code', e.msg)
             return super().form_invalid(form)
diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py
index c634ca723..9d95609d2 100644
--- a/apps/jumpserver/conf.py
+++ b/apps/jumpserver/conf.py
@@ -292,6 +292,7 @@ class Config(dict):
         'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True,
         'SECURITY_VIEW_AUTH_NEED_MFA': True,
         'SECURITY_LOGIN_LIMIT_COUNT': 7,
+        'SECURITY_LOGIN_IP_BLACK_LIST': [],
         'SECURITY_LOGIN_LIMIT_TIME': 30,
         'SECURITY_MAX_IDLE_TIME': 30,
         'SECURITY_PASSWORD_EXPIRATION_TIME': 9999,
diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py
index edc46c795..a4be80f5b 100644
--- a/apps/jumpserver/settings/custom.py
+++ b/apps/jumpserver/settings/custom.py
@@ -34,6 +34,7 @@ TERMINAL_REPLAY_STORAGE = CONFIG.TERMINAL_REPLAY_STORAGE
 SECURITY_MFA_AUTH = CONFIG.SECURITY_MFA_AUTH
 SECURITY_COMMAND_EXECUTION = CONFIG.SECURITY_COMMAND_EXECUTION
 SECURITY_LOGIN_LIMIT_COUNT = CONFIG.SECURITY_LOGIN_LIMIT_COUNT
+SECURITY_LOGIN_IP_BLACK_LIST = CONFIG.SECURITY_LOGIN_IP_BLACK_LIST
 SECURITY_LOGIN_LIMIT_TIME = CONFIG.SECURITY_LOGIN_LIMIT_TIME  # Unit: minute
 SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME  # Unit: minute
 SECURITY_PASSWORD_EXPIRATION_TIME = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index e5071bf14..2b057ff8f 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -171,6 +171,16 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. "
 msgid "Username"
 msgstr "用户名"
 
+msgid "IP Black List"
+msgstr "IP 黑名单"
+
+msgid ""
+"Format for comma-delimited string. Such as: "
+"192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64"
+msgstr ""
+"格式为逗号分隔的字符串。例如: 192.168.10.1, 192.168.1.0/24, "
+"10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)"
+
 #: acls/serializers/login_asset_acl.py:24
 msgid ""
 "Format for comma-delimited string, with * indicating a match all. Such as: "
diff --git a/apps/settings/serializers/security.py b/apps/settings/serializers/security.py
index 51d03eab5..70b015948 100644
--- a/apps/settings/serializers/security.py
+++ b/apps/settings/serializers/security.py
@@ -1,6 +1,8 @@
 from django.utils.translation import ugettext_lazy as _
 from rest_framework import serializers
 
+from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
+
 
 class SecurityPasswordRuleSerializer(serializers.Serializer):
     SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField(
@@ -14,9 +16,24 @@ class SecurityPasswordRuleSerializer(serializers.Serializer):
     SECURITY_PASSWORD_UPPER_CASE = serializers.BooleanField(
         required=False, label=_('Must contain capital')
     )
-    SECURITY_PASSWORD_LOWER_CASE = serializers.BooleanField(required=False, label=_('Must contain lowercase'))
-    SECURITY_PASSWORD_NUMBER = serializers.BooleanField(required=False, label=_('Must contain numeric'))
-    SECURITY_PASSWORD_SPECIAL_CHAR = serializers.BooleanField(required=False, label=_('Must contain special'))
+    SECURITY_PASSWORD_LOWER_CASE = serializers.BooleanField(
+        required=False, label=_('Must contain lowercase')
+    )
+    SECURITY_PASSWORD_NUMBER = serializers.BooleanField(
+        required=False, label=_('Must contain numeric')
+    )
+    SECURITY_PASSWORD_SPECIAL_CHAR = serializers.BooleanField(
+        required=False, label=_('Must contain special')
+    )
+
+
+def ip_child_validator(ip_child):
+    is_valid = is_ip_address(ip_child) \
+               or is_ip_network(ip_child) \
+               or is_ip_segment(ip_child)
+    if not is_valid:
+        error = _('IP address invalid: `{}`').format(ip_child)
+        raise serializers.ValidationError(error)
 
 
 class SecurityAuthSerializer(serializers.Serializer):
@@ -40,6 +57,14 @@ class SecurityAuthSerializer(serializers.Serializer):
             'no login is allowed during this time interval.'
         )
     )
+    SECURITY_LOGIN_IP_BLACK_LIST = serializers.ListField(
+        default=[], label=_('IP Black List'), allow_empty=True,
+        child=serializers.CharField(max_length=1024, validators=[ip_child_validator]),
+        help_text=_(
+            'Format for comma-delimited string. Such as: '
+            '192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64'
+        )
+    )
     SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField(
         min_value=1, max_value=99999, required=True,
         label=_('User password expiration'),
@@ -72,7 +97,9 @@ class SecurityAuthSerializer(serializers.Serializer):
     SECURITY_MFA_VERIFY_TTL = serializers.IntegerField(
         min_value=5, max_value=60 * 60 * 10,
         label=_("MFA verify TTL"),
-        help_text=_("Unit: second, The verification MFA takes effect only when you view the account password"),
+        help_text=_(
+            "Unit: second, The verification MFA takes effect only when you view the account password"
+        )
     )
     SECURITY_LOGIN_CHALLENGE_ENABLED = serializers.BooleanField(
         required=False, default=False,
@@ -108,7 +135,9 @@ class SecurityAuthSerializer(serializers.Serializer):
 class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSerializer):
     SECURITY_SERVICE_ACCOUNT_REGISTRATION = serializers.BooleanField(
         required=True, label=_('Enable terminal register'),
-        help_text=_("Allow terminal register, after all terminal setup, you should disable this for security")
+        help_text=_(
+            "Allow terminal register, after all terminal setup, you should disable this for security"
+        )
     )
     SECURITY_WATERMARK_ENABLED = serializers.BooleanField(
         required=True, label=_('Enable watermark'),
@@ -142,6 +171,8 @@ class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSeri
     )
     SECURITY_CHECK_DIFFERENT_CITY_LOGIN = serializers.BooleanField(
         required=False, label=_('Remote Login Protection'),
-        help_text=_('The system determines whether the login IP address belongs to a common login city. '
-                    'If the account is logged in from a common login city, the system sends a remote login reminder')
+        help_text=_(
+            'The system determines whether the login IP address belongs to a common login city. '
+            'If the account is logged in from a common login city, the system sends a remote login reminder'
+        )
     )
diff --git a/apps/users/utils.py b/apps/users/utils.py
index f00e363a6..033bc1081 100644
--- a/apps/users/utils.py
+++ b/apps/users/utils.py
@@ -14,7 +14,6 @@ from common.tasks import send_mail_async
 from common.utils import reverse, get_object_or_none
 from .models import User
 
-
 logger = logging.getLogger('jumpserver')
 
 
@@ -101,7 +100,7 @@ def check_password_rules(password, is_org_admin=False):
         min_length = settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH
     else:
         min_length = settings.SECURITY_PASSWORD_MIN_LENGTH
-    pattern += '.{' + str(min_length-1) + ',}$'
+    pattern += '.{' + str(min_length - 1) + ',}$'
     match_obj = re.match(pattern, password)
     return bool(match_obj)
 
@@ -173,6 +172,33 @@ class BlockUtilBase:
         return bool(cache.get(self.block_key))
 
 
+class BlockGlobalIpUtilBase:
+    LIMIT_KEY_TMPL: str
+    BLOCK_KEY_TMPL: str
+
+    def __init__(self, ip):
+        self.ip = ip
+        self.limit_key = self.LIMIT_KEY_TMPL.format(ip)
+        self.block_key = self.BLOCK_KEY_TMPL.format(ip)
+        self.key_ttl = int(settings.SECURITY_LOGIN_LIMIT_TIME) * 60
+
+    def sign_limit_key_and_block_key(self):
+        count = cache.get(self.limit_key, 0)
+        count += 1
+        cache.set(self.limit_key, count, self.key_ttl)
+
+        limit_count = settings.SECURITY_LOGIN_LIMIT_COUNT
+        if count >= limit_count:
+            cache.set(self.block_key, True, self.key_ttl)
+
+    def is_block(self):
+        if self.ip in settings.SECURITY_LOGIN_IP_BLACK_LIST:
+            self.sign_limit_key_and_block_key()
+            return bool(cache.get(self.block_key))
+        else:
+            return False
+
+
 class LoginBlockUtil(BlockUtilBase):
     LIMIT_KEY_TMPL = "_LOGIN_LIMIT_{}_{}"
     BLOCK_KEY_TMPL = "_LOGIN_BLOCK_{}"
@@ -183,6 +209,11 @@ class MFABlockUtils(BlockUtilBase):
     BLOCK_KEY_TMPL = "_MFA_BLOCK_{}"
 
 
+class LoginIpBlockUtil(BlockGlobalIpUtilBase):
+    LIMIT_KEY_TMPL = "_LOGIN_LIMIT_{}"
+    BLOCK_KEY_TMPL = "_LOGIN_BLOCK_{}"
+
+
 def construct_user_email(username, email):
     if '@' not in email:
         if '@' in username:

From e9dc1ad86a8af50761dd5fddc36a7d91690e295e Mon Sep 17 00:00:00 2001
From: Eric <xplzv@126.com>
Date: Fri, 12 Nov 2021 14:49:25 +0800
Subject: [PATCH 03/25] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20ssh=20?=
 =?UTF-8?q?=E5=B7=A5=E5=85=B7=E8=BF=9E=E6=8E=A5=E6=97=A0=E6=B3=95=E4=BA=8C?=
 =?UTF-8?q?=E7=BA=A7=E7=99=BB=E5=BD=95=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/perms/serializers/asset/user_permission.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apps/perms/serializers/asset/user_permission.py b/apps/perms/serializers/asset/user_permission.py
index f844b7d4d..0603a0e08 100644
--- a/apps/perms/serializers/asset/user_permission.py
+++ b/apps/perms/serializers/asset/user_permission.py
@@ -28,7 +28,7 @@ class AssetSystemUserSerializer(serializers.ModelSerializer):
         model = SystemUser
         only_fields = (
             'id', 'name', 'username', 'priority', 'protocol', 'login_mode',
-            'sftp_root', 'username_same_with_user',
+            'sftp_root', 'username_same_with_user', 'su_enabled', 'su_from',
         )
         fields = list(only_fields) + ["actions"]
         read_only_fields = fields

From 43cbf4f6a97cac33058896381c199a14175c9a39 Mon Sep 17 00:00:00 2001
From: xinwen <coderWen@126.com>
Date: Fri, 12 Nov 2021 16:27:15 +0800
Subject: [PATCH 04/25] =?UTF-8?q?fix:=20=E5=88=9B=E5=BB=BA=E5=AD=98?=
 =?UTF-8?q?=E5=82=A8=E6=97=B6=E5=90=8D=E7=A7=B0=E9=87=8D=E5=A4=8D=E6=8A=A5?=
 =?UTF-8?q?=E9=94=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/terminal/serializers/storage.py | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/apps/terminal/serializers/storage.py b/apps/terminal/serializers/storage.py
index 61c018b37..3e4e99bc0 100644
--- a/apps/terminal/serializers/storage.py
+++ b/apps/terminal/serializers/storage.py
@@ -1,6 +1,5 @@
 # -*- coding: utf-8 -*-
 #
-import copy
 from rest_framework import serializers
 from urllib.parse import urlparse
 from django.utils.translation import ugettext_lazy as _
@@ -9,6 +8,7 @@ from common.drf.serializers import MethodSerializer
 from common.drf.fields import ReadableHiddenField
 from ..models import ReplayStorage, CommandStorage
 from .. import const
+from rest_framework.validators import UniqueValidator
 
 
 # Replay storage serializers
@@ -220,6 +220,9 @@ class CommandStorageSerializer(BaseStorageSerializer):
 
     class Meta(BaseStorageSerializer.Meta):
         model = CommandStorage
+        extra_kwargs = {
+            'name': {'validators': [UniqueValidator(queryset=CommandStorage.objects.all())]}
+        }
 
 
 # ReplayStorageSerializer
@@ -230,4 +233,6 @@ class ReplayStorageSerializer(BaseStorageSerializer):
 
     class Meta(BaseStorageSerializer.Meta):
         model = ReplayStorage
-
+        extra_kwargs = {
+            'name': {'validators': [UniqueValidator(queryset=ReplayStorage.objects.all())]}
+        }

From 9a7919f3aca8f15d7ba572dcb5f300d5f7576b70 Mon Sep 17 00:00:00 2001
From: xinwen <coderWen@126.com>
Date: Fri, 12 Nov 2021 15:33:33 +0800
Subject: [PATCH 05/25] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E5=85=AC?=
 =?UTF-8?q?=E9=92=A5=E6=B6=88=E6=81=AF=E4=B8=8D=E5=AF=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../_msg_rest_public_key_success.html         |  14 ++
 apps/locale/zh/LC_MESSAGES/django.po          | 219 ++++++++++--------
 apps/users/api/profile.py                     |   7 +-
 apps/users/notifications.py                   |  32 +++
 4 files changed, 169 insertions(+), 103 deletions(-)
 create mode 100644 apps/authentication/templates/authentication/_msg_rest_public_key_success.html

diff --git a/apps/authentication/templates/authentication/_msg_rest_public_key_success.html b/apps/authentication/templates/authentication/_msg_rest_public_key_success.html
new file mode 100644
index 000000000..a95bfdd9b
--- /dev/null
+++ b/apps/authentication/templates/authentication/_msg_rest_public_key_success.html
@@ -0,0 +1,14 @@
+{% load i18n %}
+<p>{% trans 'Hello' %} {{ name }},</p>
+
+<p>
+    {% trans 'Your public key has just been successfully updated' %}
+</p>
+<p>
+    <b>{% trans 'IP' %}:</b> {{ ip_address }} <br />
+    <b>{% trans 'Browser' %}:</b> {{ browser }}
+</p>
+<p>
+    {% trans 'If the public key update was not initiated by you, your account may have security issues' %} <br />
+    {% trans 'If you have any questions, you can contact the administrator' %}
+</p>
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index 2b057ff8f..0b4d4b4be 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: JumpServer 0.3.3\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-11-10 17:18+0800\n"
+"POT-Creation-Date: 2021-11-12 15:28+0800\n"
 "PO-Revision-Date: 2021-05-20 10:54+0800\n"
 "Last-Translator: ibuler <ibuler@qq.com>\n"
 "Language-Team: JumpServer team<ibuler@qq.com>\n"
@@ -52,7 +52,7 @@ msgid "Active"
 msgstr "激活中"
 
 #: acls/models/base.py:32 applications/models/application.py:179
-#: assets/models/asset.py:144 assets/models/asset.py:220
+#: assets/models/asset.py:144 assets/models/asset.py:232
 #: assets/models/base.py:180 assets/models/cluster.py:29
 #: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:64
 #: assets/models/domain.py:25 assets/models/domain.py:65
@@ -128,7 +128,7 @@ msgstr "系统用户"
 
 #: acls/models/login_asset_acl.py:22
 #: applications/serializers/attrs/application_category/remote_app.py:37
-#: assets/models/asset.py:357 assets/models/authbook.py:18
+#: assets/models/asset.py:350 assets/models/authbook.py:18
 #: assets/models/gathered_user.py:14 assets/serializers/system_user.py:258
 #: audits/models.py:38 perms/models/asset_permission.py:99
 #: templates/index.html:82 terminal/backends/command/models.py:19
@@ -192,16 +192,17 @@ msgstr ""
 
 #: acls/serializers/login_asset_acl.py:31 acls/serializers/rules/rules.py:32
 #: applications/serializers/attrs/application_type/mysql_workbench.py:18
-#: assets/models/asset.py:180 assets/models/domain.py:61
+#: assets/models/asset.py:211 assets/models/domain.py:61
 #: assets/serializers/account.py:12
 #: authentication/templates/authentication/_msg_rest_password_success.html:8
+#: authentication/templates/authentication/_msg_rest_public_key_success.html:8
 #: settings/serializers/terminal.py:8
 #: users/templates/users/_granted_assets.html:26
 #: users/templates/users/user_asset_permission.html:156
 msgid "IP"
 msgstr "IP"
 
-#: acls/serializers/login_asset_acl.py:35 assets/models/asset.py:181
+#: acls/serializers/login_asset_acl.py:35 assets/models/asset.py:212
 #: assets/serializers/account.py:13 assets/serializers/gathered_user.py:23
 #: settings/serializers/terminal.py:7
 #: users/templates/users/_granted_assets.html:25
@@ -215,7 +216,7 @@ msgid ""
 "options: {}"
 msgstr "格式为逗号分隔的字符串, * 表示匹配所有. 可选的协议有: {}"
 
-#: acls/serializers/login_asset_acl.py:55 assets/models/asset.py:184
+#: acls/serializers/login_asset_acl.py:55 assets/models/asset.py:214
 #: assets/models/domain.py:63 assets/models/user.py:200
 #: terminal/serializers/session.py:30 terminal/serializers/storage.py:69
 msgid "Protocol"
@@ -323,7 +324,7 @@ msgstr "类别"
 msgid "Type"
 msgstr "类型"
 
-#: applications/models/application.py:175 assets/models/asset.py:188
+#: applications/models/application.py:175 assets/models/asset.py:218
 #: assets/models/domain.py:30 assets/models/domain.py:64
 msgid "Domain"
 msgstr "网域"
@@ -375,7 +376,7 @@ msgstr "主机"
 #: applications/serializers/attrs/application_type/mysql_workbench.py:22
 #: applications/serializers/attrs/application_type/oracle.py:11
 #: applications/serializers/attrs/application_type/pgsql.py:11
-#: assets/models/asset.py:185 assets/models/domain.py:62
+#: assets/models/asset.py:215 assets/models/domain.py:62
 #: settings/serializers/auth/radius.py:15
 #: xpack/plugins/cloud/serializers/account_attrs.py:61
 msgid "Port"
@@ -463,103 +464,103 @@ msgstr "元数据"
 msgid "Internal"
 msgstr "内部的"
 
-#: assets/models/asset.py:163 assets/models/asset.py:187
+#: assets/models/asset.py:163 assets/models/asset.py:217
 #: assets/serializers/asset.py:65 perms/serializers/asset/user_permission.py:43
 msgid "Platform"
 msgstr "系统平台"
 
-#: assets/models/asset.py:186 assets/serializers/asset.py:67
+#: assets/models/asset.py:169
+msgid "Vendor"
+msgstr "制造商"
+
+#: assets/models/asset.py:170
+msgid "Model"
+msgstr "型号"
+
+#: assets/models/asset.py:171
+msgid "Serial number"
+msgstr "序列号"
+
+#: assets/models/asset.py:173
+msgid "CPU model"
+msgstr "CPU型号"
+
+#: assets/models/asset.py:174
+msgid "CPU count"
+msgstr "CPU数量"
+
+#: assets/models/asset.py:175
+msgid "CPU cores"
+msgstr "CPU核数"
+
+#: assets/models/asset.py:176
+msgid "CPU vcpus"
+msgstr "CPU总数"
+
+#: assets/models/asset.py:177
+msgid "Memory"
+msgstr "内存"
+
+#: assets/models/asset.py:178
+msgid "Disk total"
+msgstr "硬盘大小"
+
+#: assets/models/asset.py:179
+msgid "Disk info"
+msgstr "硬盘信息"
+
+#: assets/models/asset.py:181
+msgid "OS"
+msgstr "操作系统"
+
+#: assets/models/asset.py:182
+msgid "OS version"
+msgstr "系统版本"
+
+#: assets/models/asset.py:183
+msgid "OS arch"
+msgstr "系统架构"
+
+#: assets/models/asset.py:184
+msgid "Hostname raw"
+msgstr "主机名原始"
+
+#: assets/models/asset.py:216 assets/serializers/asset.py:67
 #: perms/serializers/asset/user_permission.py:41
 #: xpack/plugins/cloud/models.py:104 xpack/plugins/cloud/serializers/task.py:42
 msgid "Protocols"
 msgstr "协议组"
 
-#: assets/models/asset.py:189 assets/models/user.py:190
+#: assets/models/asset.py:219 assets/models/user.py:190
 #: perms/models/asset_permission.py:100
 #: xpack/plugins/change_auth_plan/models/asset.py:44
 #: xpack/plugins/gathered_user/models.py:24
 msgid "Nodes"
 msgstr "节点"
 
-#: assets/models/asset.py:190 assets/models/cmd_filter.py:22
+#: assets/models/asset.py:220 assets/models/cmd_filter.py:22
 #: assets/models/domain.py:66 assets/models/label.py:22
 msgid "Is active"
 msgstr "激活"
 
-#: assets/models/asset.py:193 assets/models/cluster.py:19
+#: assets/models/asset.py:223 assets/models/cluster.py:19
 #: assets/models/user.py:187 assets/models/user.py:340 templates/_nav.html:44
 msgid "Admin user"
 msgstr "特权用户"
 
-#: assets/models/asset.py:196
+#: assets/models/asset.py:226
 msgid "Public IP"
 msgstr "公网IP"
 
-#: assets/models/asset.py:197
+#: assets/models/asset.py:227
 msgid "Asset number"
 msgstr "资产编号"
 
-#: assets/models/asset.py:200
-msgid "Vendor"
-msgstr "制造商"
-
-#: assets/models/asset.py:201
-msgid "Model"
-msgstr "型号"
-
-#: assets/models/asset.py:202
-msgid "Serial number"
-msgstr "序列号"
-
-#: assets/models/asset.py:204
-msgid "CPU model"
-msgstr "CPU型号"
-
-#: assets/models/asset.py:205
-msgid "CPU count"
-msgstr "CPU数量"
-
-#: assets/models/asset.py:206
-msgid "CPU cores"
-msgstr "CPU核数"
-
-#: assets/models/asset.py:207
-msgid "CPU vcpus"
-msgstr "CPU总数"
-
-#: assets/models/asset.py:208
-msgid "Memory"
-msgstr "内存"
-
-#: assets/models/asset.py:209
-msgid "Disk total"
-msgstr "硬盘大小"
-
-#: assets/models/asset.py:210
-msgid "Disk info"
-msgstr "硬盘信息"
-
-#: assets/models/asset.py:212
-msgid "OS"
-msgstr "操作系统"
-
-#: assets/models/asset.py:213
-msgid "OS version"
-msgstr "系统版本"
-
-#: assets/models/asset.py:214
-msgid "OS arch"
-msgstr "系统架构"
-
-#: assets/models/asset.py:215
-msgid "Hostname raw"
-msgstr "主机名原始"
-
-#: assets/models/asset.py:217 templates/_nav.html:46
+#: assets/models/asset.py:229 templates/_nav.html:46
 msgid "Labels"
 msgstr "标签管理"
 
-#: assets/models/asset.py:218 assets/models/base.py:183
+#: assets/models/asset.py:230 assets/models/base.py:183
 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:26
 #: assets/models/cmd_filter.py:67 assets/models/group.py:21
 #: common/db/models.py:70 common/mixins/models.py:49 orgs/models.py:25
@@ -570,7 +571,7 @@ msgstr "标签管理"
 msgid "Created by"
 msgstr "创建者"
 
-#: assets/models/asset.py:219 assets/models/base.py:181
+#: assets/models/asset.py:231 assets/models/base.py:181
 #: assets/models/cluster.py:26 assets/models/domain.py:27
 #: assets/models/gathered_user.py:19 assets/models/group.py:22
 #: assets/models/label.py:25 common/db/models.py:72 common/mixins/models.py:50
@@ -875,19 +876,14 @@ msgstr "协议重复: {}"
 msgid "Domain name"
 msgstr "网域名称"
 
-#: assets/serializers/asset.py:69
+#: assets/serializers/asset.py:70
 msgid "Nodes name"
 msgstr "节点名称"
 
-#: assets/serializers/asset.py:103
+#: assets/serializers/asset.py:104
 msgid "Hardware info"
 msgstr "硬件信息"
 
-#: assets/serializers/asset.py:104 assets/serializers/system_user.py:276
-#: orgs/mixins/serializers.py:26
-msgid "Org name"
-msgstr "组织名称"
-
 #: assets/serializers/asset.py:105
 msgid "Admin user display"
 msgstr "特权用户名称"
@@ -988,6 +984,10 @@ msgstr "仅允许自动登录的系统用户"
 msgid "System user name"
 msgstr "系统用户名称"
 
+#: assets/serializers/system_user.py:276 orgs/mixins/serializers.py:26
+msgid "Org name"
+msgstr "组织名称"
+
 #: assets/serializers/system_user.py:285
 msgid "Asset hostname"
 msgstr "资产主机名"
@@ -1501,7 +1501,7 @@ msgstr "{ApplicationPermission} 添加 {SystemUser}"
 msgid "{ApplicationPermission} REMOVE {SystemUser}"
 msgstr "{ApplicationPermission} 移除 {SystemUser}"
 
-#: authentication/api/connection_token.py:239
+#: authentication/api/connection_token.py:248
 msgid "Invalid token"
 msgstr "无效的令牌"
 
@@ -1880,6 +1880,7 @@ msgstr "代码错误"
 #: authentication/templates/authentication/_msg_different_city.html:3
 #: authentication/templates/authentication/_msg_reset_password.html:3
 #: authentication/templates/authentication/_msg_rest_password_success.html:2
+#: authentication/templates/authentication/_msg_rest_public_key_success.html:2
 #: jumpserver/conf.py:269
 #: perms/templates/perms/_msg_item_permissions_expire.html:3
 #: perms/templates/perms/_msg_permed_items_expire.html:3
@@ -1929,6 +1930,7 @@ msgid "Your password has just been successfully updated"
 msgstr "你的密码刚刚成功更新"
 
 #: authentication/templates/authentication/_msg_rest_password_success.html:9
+#: authentication/templates/authentication/_msg_rest_public_key_success.html:9
 msgid "Browser"
 msgstr "浏览器"
 
@@ -1939,9 +1941,20 @@ msgid ""
 msgstr "如果这次密码更新不是由你发起的,那么你的账号可能存在安全问题"
 
 #: authentication/templates/authentication/_msg_rest_password_success.html:13
+#: authentication/templates/authentication/_msg_rest_public_key_success.html:13
 msgid "If you have any questions, you can contact the administrator"
 msgstr "如果有疑问或需求,请联系系统管理员"
 
+#: authentication/templates/authentication/_msg_rest_public_key_success.html:5
+msgid "Your public key has just been successfully updated"
+msgstr "你的公钥刚刚成功更新"
+
+#: authentication/templates/authentication/_msg_rest_public_key_success.html:12
+msgid ""
+"If the public key update was not initiated by you, your account may have "
+"security issues"
+msgstr "如果这次公钥更新不是由你发起的,那么你的账号可能存在安全问题"
+
 #: authentication/templates/authentication/login.html:143
 msgid "Welcome back, please enter username and password to login"
 msgstr "欢迎回来,请输入用户名和密码登录"
@@ -3262,7 +3275,9 @@ msgstr "显示未分组节点"
 
 #: settings/serializers/other.py:30
 msgid "Perm single to ungroup node"
-msgstr "放置单独授权的资产到未分组节点, 避免能看到资产所在节点,但该节点未被授权的问题"
+msgstr ""
+"放置单独授权的资产到未分组节点, 避免能看到资产所在节点,但该节点未被授权的问"
+"题"
 
 #: settings/serializers/other.py:34
 msgid "Help Docs URL"
@@ -3280,14 +3295,6 @@ msgstr "支持链接"
 msgid "default: http://www.jumpserver.org/support/"
 msgstr "默认: http://www.jumpserver.org/support/"
 
-#: settings/serializers/other.py:44
-msgid "Help Website URL"
-msgstr "官网链接"
-
-#: settings/serializers/other.py:45
-msgid "default: http://www.jumpserver.org"
-msgstr "如: http://dev.jumpserver.org:8080"
-
 #: settings/serializers/security.py:8
 msgid "Password minimum length"
 msgstr "密码最小长度"
@@ -5064,19 +5071,23 @@ msgstr "重置密码"
 msgid "Reset password success"
 msgstr "重置密码成功"
 
-#: users/notifications.py:111
+#: users/notifications.py:117
+msgid "Reset public key success"
+msgstr "重置公钥成功"
+
+#: users/notifications.py:143
 msgid "Password is about expire"
 msgstr "密码即将过期"
 
-#: users/notifications.py:139
+#: users/notifications.py:171
 msgid "Account is about expire"
 msgstr "账号即将过期"
 
-#: users/notifications.py:161
+#: users/notifications.py:193
 msgid "Reset SSH Key"
 msgstr "重置 SSH 密钥"
 
-#: users/notifications.py:182
+#: users/notifications.py:214
 msgid "Reset MFA"
 msgstr "重置 MFA"
 
@@ -5523,8 +5534,8 @@ msgstr "* 新密码不能是最近 {} 次的密码"
 msgid "Reset password success, return to login page"
 msgstr "重置密码成功,返回到登录页面"
 
-#: xpack/plugins/change_auth_plan/api/app.py:113
-#: xpack/plugins/change_auth_plan/api/asset.py:100
+#: xpack/plugins/change_auth_plan/api/app.py:114
+#: xpack/plugins/change_auth_plan/api/asset.py:101
 msgid "The parameter 'action' must be [{}]"
 msgstr "参数 'action' 必须是 [{}]"
 
@@ -5655,15 +5666,15 @@ msgstr "* 请输入正确的密码长度"
 msgid "* Password length range 6-30 bits"
 msgstr "* 密码长度范围 6-30 位"
 
-#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:248
+#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:249
 msgid "Invalid/incorrect password"
 msgstr "无效/错误 密码"
 
-#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:250
+#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:251
 msgid "Failed to connect to the host"
 msgstr "连接主机失败"
 
-#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:252
+#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:253
 msgid "Data could not be sent to remote"
 msgstr "无法将数据发送到远程"
 
@@ -6021,7 +6032,7 @@ msgstr "执行次数"
 msgid "Instance count"
 msgstr "实例个数"
 
-#: xpack/plugins/cloud/utils.py:65
+#: xpack/plugins/cloud/utils.py:68
 msgid "Account unavailable"
 msgstr "账户无效"
 
@@ -6109,6 +6120,12 @@ msgstr "旗舰版"
 msgid "Community edition"
 msgstr "社区版"
 
+#~ msgid "Help Website URL"
+#~ msgstr "官网链接"
+
+#~ msgid "default: http://www.jumpserver.org"
+#~ msgstr "如: http://dev.jumpserver.org:8080"
+
 #~ msgid "One-time password invalid, or ntp sync server time"
 #~ msgstr "MFA 验证码不正确,或者服务器端时间不对"
 
diff --git a/apps/users/api/profile.py b/apps/users/api/profile.py
index 372ed5809..14aa1d8f6 100644
--- a/apps/users/api/profile.py
+++ b/apps/users/api/profile.py
@@ -5,7 +5,10 @@ from rest_framework import generics
 from common.permissions import IsOrgAdmin
 from rest_framework.permissions import IsAuthenticated
 
-from users.notifications import ResetPasswordMsg, ResetPasswordSuccessMsg, ResetSSHKeyMsg
+from users.notifications import (
+    ResetPasswordMsg, ResetPasswordSuccessMsg, ResetSSHKeyMsg,
+    ResetPublicKeySuccessMsg,
+)
 from common.permissions import (
     IsCurrentUserOrReadOnly
 )
@@ -87,4 +90,4 @@ class UserPublicKeyApi(generics.RetrieveUpdateAPIView):
 
     def perform_update(self, serializer):
         super().perform_update(serializer)
-        ResetPasswordSuccessMsg(self.get_object(), self.request).publish_async()
+        ResetPublicKeySuccessMsg(self.get_object(), self.request).publish_async()
diff --git a/apps/users/notifications.py b/apps/users/notifications.py
index d1fbad546..c9e5a0521 100644
--- a/apps/users/notifications.py
+++ b/apps/users/notifications.py
@@ -105,6 +105,38 @@ class ResetPasswordSuccessMsg(UserMessage):
         return cls(user, request)
 
 
+class ResetPublicKeySuccessMsg(UserMessage):
+    def __init__(self, user, request):
+        super().__init__(user)
+        self.ip_address = get_request_ip_or_data(request)
+        self.browser = get_request_user_agent(request)
+
+    def get_html_msg(self) -> dict:
+        user = self.user
+
+        subject = _('Reset public key success')
+        context = {
+            'name': user.name,
+            'ip_address': self.ip_address,
+            'browser': self.browser,
+        }
+        message = render_to_string('authentication/_msg_rest_public_key_success.html', context)
+        return {
+            'subject': subject,
+            'message': message
+        }
+
+    @classmethod
+    def gen_test_msg(cls):
+        from users.models import User
+        from rest_framework.test import APIRequestFactory
+        from rest_framework.request import Request
+        factory = APIRequestFactory()
+        request = Request(factory.get('/notes/'))
+        user = User.objects.first()
+        return cls(user, request)
+
+
 class PasswordExpirationReminderMsg(UserMessage):
     def get_html_msg(self) -> dict:
         user = self.user

From 7286b1b09ea23cdfb5e55fad5fa9ce62bc9f65ac Mon Sep 17 00:00:00 2001
From: Michael Bai <baijiangjie@gmail.com>
Date: Fri, 12 Nov 2021 11:04:38 +0800
Subject: [PATCH 06/25] =?UTF-8?q?fix:=20=E7=A7=BB=E5=8A=A8=E3=80=90?=
 =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E8=AE=BE=E7=BD=AE=E4=BF=9D=E7=95=99=E6=97=B6?=
 =?UTF-8?q?=E9=97=B4=E3=80=91=E8=87=B3=E5=AE=9A=E6=9C=9F=E6=B8=85=E7=90=86?=
 =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E9=A1=B5=E9=9D=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/settings/serializers/cleaning.py | 5 +++++
 apps/settings/serializers/terminal.py | 4 ----
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/apps/settings/serializers/cleaning.py b/apps/settings/serializers/cleaning.py
index 39aae4a80..68e8092cb 100644
--- a/apps/settings/serializers/cleaning.py
+++ b/apps/settings/serializers/cleaning.py
@@ -25,3 +25,8 @@ class CleaningSerializer(serializers.Serializer):
         min_value=1, max_value=9999,
         label=_("Cloud sync record keep days"), help_text=_("Unit: day")
     )
+    TERMINAL_SESSION_KEEP_DURATION = serializers.IntegerField(
+        min_value=1, max_value=99999, required=True, label=_('Session keep duration'),
+        help_text=_('Unit: days, Session, record, command will be delete if more than duration, only in database')
+    )
+
diff --git a/apps/settings/serializers/terminal.py b/apps/settings/serializers/terminal.py
index 0410a7517..12858913d 100644
--- a/apps/settings/serializers/terminal.py
+++ b/apps/settings/serializers/terminal.py
@@ -25,10 +25,6 @@ class TerminalSettingSerializer(serializers.Serializer):
     TERMINAL_ASSET_LIST_SORT_BY = serializers.ChoiceField(SORT_BY_CHOICES, required=False, label=_('List sort by'))
     TERMINAL_ASSET_LIST_PAGE_SIZE = serializers.ChoiceField(PAGE_SIZE_CHOICES, required=False,
                                                             label=_('List page size'))
-    TERMINAL_SESSION_KEEP_DURATION = serializers.IntegerField(
-        min_value=1, max_value=99999, required=True, label=_('Session keep duration'),
-        help_text=_('Unit: days, Session, record, command will be delete if more than duration, only in database')
-    )
     TERMINAL_TELNET_REGEX = serializers.CharField(
         allow_blank=True, max_length=1024, required=False, label=_('Telnet login regex'),
         help_text=_("The login success message varies with devices. "

From 4260fe14249af90fc968802c113925e36e780855 Mon Sep 17 00:00:00 2001
From: xinwen <coderWen@126.com>
Date: Fri, 12 Nov 2021 15:54:09 +0800
Subject: [PATCH 07/25] =?UTF-8?q?fix:=20=E8=B5=84=E4=BA=A7=20cpu=5Finfo=20?=
 =?UTF-8?q?=E7=BF=BB=E8=AF=91=E4=B8=8D=E5=AF=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/assets/serializers/asset.py     |   1 +
 apps/locale/zh/LC_MESSAGES/django.po | 168 ++++++++++++++-------------
 2 files changed, 88 insertions(+), 81 deletions(-)

diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py
index b13eda715..0182dfc94 100644
--- a/apps/assets/serializers/asset.py
+++ b/apps/assets/serializers/asset.py
@@ -103,6 +103,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
             'port': {'write_only': True},
             'hardware_info': {'label': _('Hardware info'), 'read_only': True},
             'admin_user_display': {'label': _('Admin user display'), 'read_only': True},
+            'cpu_info': {'label': _('CPU Info')},
         }
 
     def get_fields(self):
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index 0b4d4b4be..57dae93a7 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: JumpServer 0.3.3\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-11-12 15:28+0800\n"
+"POT-Creation-Date: 2021-11-12 16:51+0800\n"
 "PO-Revision-Date: 2021-05-20 10:54+0800\n"
 "Last-Translator: ibuler <ibuler@qq.com>\n"
 "Language-Team: JumpServer team<ibuler@qq.com>\n"
@@ -171,16 +171,6 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. "
 msgid "Username"
 msgstr "用户名"
 
-msgid "IP Black List"
-msgstr "IP 黑名单"
-
-msgid ""
-"Format for comma-delimited string. Such as: "
-"192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64"
-msgstr ""
-"格式为逗号分隔的字符串。例如: 192.168.10.1, 192.168.1.0/24, "
-"10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)"
-
 #: acls/serializers/login_asset_acl.py:24
 msgid ""
 "Format for comma-delimited string, with * indicating a match all. Such as: "
@@ -235,7 +225,7 @@ msgstr "组织 `{}` 不存在"
 msgid "None of the reviewers belong to Organization `{}`"
 msgstr "所有复核人都不属于组织 `{}`"
 
-#: acls/serializers/rules/rules.py:20
+#: acls/serializers/rules/rules.py:20 settings/serializers/security.py:35
 #: xpack/plugins/cloud/serializers/task.py:23
 msgid "IP address invalid: `{}`"
 msgstr "IP 地址无效: `{}`"
@@ -455,7 +445,7 @@ msgstr "基础"
 msgid "Charset"
 msgstr "编码"
 
-#: assets/models/asset.py:142 assets/serializers/asset.py:174
+#: assets/models/asset.py:142 assets/serializers/asset.py:175
 #: tickets/models/ticket.py:50
 msgid "Meta"
 msgstr "元数据"
@@ -888,6 +878,10 @@ msgstr "硬件信息"
 msgid "Admin user display"
 msgstr "特权用户名称"
 
+#: assets/serializers/asset.py:106
+msgid "CPU Info"
+msgstr "CPU信息"
+
 #: assets/serializers/base.py:41
 msgid "private key invalid"
 msgstr "密钥不合法"
@@ -1308,12 +1302,12 @@ msgstr ""
 msgid "Auth Token"
 msgstr "认证令牌"
 
-#: audits/signals_handler.py:68 authentication/views/login.py:169
+#: audits/signals_handler.py:68 authentication/views/login.py:170
 #: notifications/backends/__init__.py:11 users/models/user.py:596
 msgid "WeCom"
 msgstr "企业微信"
 
-#: audits/signals_handler.py:69 authentication/views/login.py:175
+#: audits/signals_handler.py:69 authentication/views/login.py:176
 #: notifications/backends/__init__.py:12 users/models/user.py:597
 msgid "DingTalk"
 msgstr "钉钉"
@@ -1662,47 +1656,47 @@ msgstr "等待登录复核处理"
 msgid "Login confirm ticket was {}"
 msgstr "登录复核 {}"
 
-#: authentication/errors.py:243
+#: authentication/errors.py:187 authentication/errors.py:251
 msgid "IP is not allowed"
 msgstr "来源 IP 不被允许登录"
 
-#: authentication/errors.py:250
+#: authentication/errors.py:258
 msgid "Time Period is not allowed"
 msgstr "该 时间段 不被允许登录"
 
-#: authentication/errors.py:283
+#: authentication/errors.py:291
 msgid "SSO auth closed"
 msgstr "SSO 认证关闭了"
 
-#: authentication/errors.py:288 authentication/mixins.py:344
+#: authentication/errors.py:296 authentication/mixins.py:350
 msgid "Your password is too simple, please change it for security"
 msgstr "你的密码过于简单,为了安全,请修改"
 
-#: authentication/errors.py:297 authentication/mixins.py:351
+#: authentication/errors.py:305 authentication/mixins.py:357
 msgid "You should to change your password before login"
 msgstr "登录完成前,请先修改密码"
 
-#: authentication/errors.py:306 authentication/mixins.py:358
+#: authentication/errors.py:314 authentication/mixins.py:364
 msgid "Your password has expired, please reset before logging in"
 msgstr "您的密码已过期,先修改再登录"
 
-#: authentication/errors.py:340
+#: authentication/errors.py:348
 msgid "Your password is invalid"
 msgstr "您的密码无效"
 
-#: authentication/errors.py:346
+#: authentication/errors.py:354
 msgid "No upload or download permission"
 msgstr "没有上传下载权限"
 
-#: authentication/errors.py:358
+#: authentication/errors.py:366
 msgid "Please enter MFA code"
 msgstr "请输入6位动态安全码"
 
-#: authentication/errors.py:362
+#: authentication/errors.py:370
 msgid "Please enter SMS code"
 msgstr "请输入短信验证码"
 
-#: authentication/errors.py:366 users/exceptions.py:15
+#: authentication/errors.py:374 users/exceptions.py:15
 msgid "Phone not set"
 msgstr "手机号没有设置"
 
@@ -1774,11 +1768,11 @@ msgstr "设置手机号码启用"
 msgid "Clear phone number to disable"
 msgstr "清空手机号码禁用"
 
-#: authentication/mixins.py:305
+#: authentication/mixins.py:311
 msgid "The MFA type({}) is not supported"
 msgstr "该 MFA 方法 ({}) 不被支持"
 
-#: authentication/mixins.py:334
+#: authentication/mixins.py:340
 msgid "Please change your password"
 msgstr "请修改密码"
 
@@ -1827,7 +1821,7 @@ msgid "Show"
 msgstr "显示"
 
 #: authentication/templates/authentication/_access_key_modal.html:66
-#: settings/serializers/security.py:25 users/models/user.py:458
+#: settings/serializers/security.py:42 users/models/user.py:458
 #: users/serializers/profile.py:99 users/templates/users/mfa_setting.html:60
 #: users/templates/users/user_verify_mfa.html:36
 msgid "Disable"
@@ -2097,12 +2091,12 @@ msgstr "正在跳转到 {} 认证"
 msgid "Please enable cookies and try again."
 msgstr "设置你的浏览器支持cookie"
 
-#: authentication/views/login.py:181 notifications/backends/__init__.py:14
+#: authentication/views/login.py:182 notifications/backends/__init__.py:14
 #: users/models/user.py:598
 msgid "FeiShu"
 msgstr "飞书"
 
-#: authentication/views/login.py:270
+#: authentication/views/login.py:271
 msgid ""
 "Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>\n"
 "                  Don't close this page"
@@ -2110,15 +2104,15 @@ msgstr ""
 "等待 <b>{}</b> 确认, 你也可以复制链接发给他/她 <br/>\n"
 "                  不要关闭本页面"
 
-#: authentication/views/login.py:275
+#: authentication/views/login.py:276
 msgid "No ticket found"
 msgstr "没有发现工单"
 
-#: authentication/views/login.py:307
+#: authentication/views/login.py:308
 msgid "Logout success"
 msgstr "退出登录成功"
 
-#: authentication/views/login.py:308
+#: authentication/views/login.py:309
 msgid "Logout success, return login page"
 msgstr "退出登录成功,返回到登录页面"
 
@@ -3295,61 +3289,73 @@ msgstr "支持链接"
 msgid "default: http://www.jumpserver.org/support/"
 msgstr "默认: http://www.jumpserver.org/support/"
 
-#: settings/serializers/security.py:8
+#: settings/serializers/security.py:10
 msgid "Password minimum length"
 msgstr "密码最小长度"
 
-#: settings/serializers/security.py:12
+#: settings/serializers/security.py:14
 msgid "Admin user password minimum length"
 msgstr "管理员密码最小长度"
 
-#: settings/serializers/security.py:15
+#: settings/serializers/security.py:17
 msgid "Must contain capital"
 msgstr "必须包含大写字符"
 
-#: settings/serializers/security.py:17
+#: settings/serializers/security.py:20
 msgid "Must contain lowercase"
 msgstr "必须包含小写字符"
 
-#: settings/serializers/security.py:18
+#: settings/serializers/security.py:23
 msgid "Must contain numeric"
 msgstr "必须包含数字"
 
-#: settings/serializers/security.py:19
+#: settings/serializers/security.py:26
 msgid "Must contain special"
 msgstr "必须包含特殊字符"
 
-#: settings/serializers/security.py:26
+#: settings/serializers/security.py:43
 msgid "All users"
 msgstr "所有用户"
 
-#: settings/serializers/security.py:27
+#: settings/serializers/security.py:44
 msgid "Only admin users"
 msgstr "仅管理员"
 
-#: settings/serializers/security.py:29
+#: settings/serializers/security.py:46
 msgid "Global MFA auth"
 msgstr "全局启用 MFA 认证"
 
-#: settings/serializers/security.py:33
+#: settings/serializers/security.py:50
 msgid "Limit the number of login failures"
 msgstr "限制登录失败次数"
 
-#: settings/serializers/security.py:37
+#: settings/serializers/security.py:54
 msgid "Block logon interval"
 msgstr "禁止登录时间间隔"
 
-#: settings/serializers/security.py:39
+#: settings/serializers/security.py:56
 msgid ""
 "Unit: minute, If the user has failed to log in for a limited number of "
 "times, no login is allowed during this time interval."
 msgstr "单位:分, 当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录"
 
-#: settings/serializers/security.py:45
+#: settings/serializers/security.py:61
+msgid "IP Black List"
+msgstr "IP 黑名单"
+
+#: settings/serializers/security.py:64
+msgid ""
+"Format for comma-delimited string. Such as: 192.168.10.1, 192.168.1.0/24, "
+"10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64"
+msgstr ""
+"格式为逗号分隔的字符串。例如: 192.168.10.1, 192.168.1.0/24, "
+"10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)"
+
+#: settings/serializers/security.py:70
 msgid "User password expiration"
 msgstr "用户密码过期时间"
 
-#: settings/serializers/security.py:47
+#: settings/serializers/security.py:72
 msgid ""
 "Unit: day, If the user does not update the password during the time, the "
 "user password will expire failure;The password expiration reminder mail will "
@@ -3359,55 +3365,55 @@ msgstr ""
 "单位:天, 如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期提醒邮件"
 "将在密码过期前5天内由系统(每天)自动发送给用户"
 
-#: settings/serializers/security.py:54
+#: settings/serializers/security.py:79
 msgid "Number of repeated historical passwords"
 msgstr "不能设置近几次密码"
 
-#: settings/serializers/security.py:56
+#: settings/serializers/security.py:81
 msgid ""
 "Tip: When the user resets the password, it cannot be the previous n "
 "historical passwords of the user"
 msgstr "提示:用户重置密码时,不能为该用户前几次使用过的密码"
 
-#: settings/serializers/security.py:61
+#: settings/serializers/security.py:86
 msgid "Only single device login"
 msgstr "仅一台设备登录"
 
-#: settings/serializers/security.py:62
+#: settings/serializers/security.py:87
 msgid "Next device login, pre login will be logout"
 msgstr "下个设备登录,上次登录会被顶掉"
 
-#: settings/serializers/security.py:65
+#: settings/serializers/security.py:90
 msgid "Only exist user login"
 msgstr "仅已存在用户登录"
 
-#: settings/serializers/security.py:66
+#: settings/serializers/security.py:91
 msgid "If enable, CAS、OIDC auth will be failed, if user not exist yet"
 msgstr "开启后,如果系统中不存在该用户,CAS、OIDC 登录将会失败"
 
-#: settings/serializers/security.py:69
+#: settings/serializers/security.py:94
 msgid "Only from source login"
 msgstr "仅从用户来源登录"
 
-#: settings/serializers/security.py:70
+#: settings/serializers/security.py:95
 msgid "Only log in from the user source property"
 msgstr "开启后,如果用户来源为本地,CAS、OIDC 登录将会失败"
 
-#: settings/serializers/security.py:74
+#: settings/serializers/security.py:99
 msgid "MFA verify TTL"
 msgstr "MFA 校验有效期"
 
-#: settings/serializers/security.py:75
+#: settings/serializers/security.py:101
 msgid ""
 "Unit: second, The verification MFA takes effect only when you view the "
 "account password"
 msgstr "单位: 秒, 目前仅在查看账号密码校验 MFA 时生效"
 
-#: settings/serializers/security.py:79
+#: settings/serializers/security.py:106
 msgid "Enable Login dynamic code"
 msgstr "启用登录附加码"
 
-#: settings/serializers/security.py:80
+#: settings/serializers/security.py:107
 msgid ""
 "The password and additional code are sent to a third party authentication "
 "system for verification"
@@ -3415,89 +3421,89 @@ msgstr ""
 "密码和附加码一并发送给第三方认证系统进行校验, 如:有的第三方认证系统,需要 密"
 "码+6位数字 完成认证"
 
-#: settings/serializers/security.py:85
+#: settings/serializers/security.py:112
 msgid "MFA in login page"
 msgstr "MFA 在登录页面"
 
-#: settings/serializers/security.py:86
+#: settings/serializers/security.py:113
 msgid "Eu security regulations(GDPR) require MFA to be on the login page"
 msgstr "欧盟数据安全法规(GDPR) 要求 MFA 在登录页面,来确保系统登录安全"
 
-#: settings/serializers/security.py:89
+#: settings/serializers/security.py:116
 msgid "Enable Login captcha"
 msgstr "启用登录验证码"
 
-#: settings/serializers/security.py:90
+#: settings/serializers/security.py:117
 msgid "Enable captcha to prevent robot authentication"
 msgstr "开启验证码,防止机器人登录"
 
-#: settings/serializers/security.py:110
+#: settings/serializers/security.py:137
 msgid "Enable terminal register"
 msgstr "终端注册"
 
-#: settings/serializers/security.py:111
+#: settings/serializers/security.py:139
 msgid ""
 "Allow terminal register, after all terminal setup, you should disable this "
 "for security"
 msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭"
 
-#: settings/serializers/security.py:114
+#: settings/serializers/security.py:143
 msgid "Enable watermark"
 msgstr "开启水印"
 
-#: settings/serializers/security.py:115
+#: settings/serializers/security.py:144
 msgid "Enabled, the web session and replay contains watermark information"
 msgstr "启用后,Web 会话和录像将包含水印信息"
 
-#: settings/serializers/security.py:119
+#: settings/serializers/security.py:148
 msgid "Connection max idle time"
 msgstr "连接最大空闲时间"
 
-#: settings/serializers/security.py:120
+#: settings/serializers/security.py:149
 msgid "If idle time more than it, disconnect connection Unit: minute"
 msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)"
 
-#: settings/serializers/security.py:123
+#: settings/serializers/security.py:152
 msgid "Remember manual auth"
 msgstr "保存手动输入密码"
 
-#: settings/serializers/security.py:126
+#: settings/serializers/security.py:155
 msgid "Enable change auth secure mode"
 msgstr "启用改密安全模式"
 
-#: settings/serializers/security.py:129
+#: settings/serializers/security.py:158
 msgid "Insecure command alert"
 msgstr "危险命令告警"
 
-#: settings/serializers/security.py:132
+#: settings/serializers/security.py:161
 msgid "Email recipient"
 msgstr "邮件收件人"
 
-#: settings/serializers/security.py:133
+#: settings/serializers/security.py:162
 msgid "Multiple user using , split"
 msgstr "多个用户,使用 , 分割"
 
-#: settings/serializers/security.py:136
+#: settings/serializers/security.py:165
 msgid "Batch command execution"
 msgstr "批量命令执行"
 
-#: settings/serializers/security.py:137
+#: settings/serializers/security.py:166
 msgid "Allow user run batch command or not using ansible"
 msgstr "是否允许用户使用 ansible 执行批量命令"
 
-#: settings/serializers/security.py:140
+#: settings/serializers/security.py:169
 msgid "Session share"
 msgstr "会话分享"
 
-#: settings/serializers/security.py:141
+#: settings/serializers/security.py:170
 msgid "Enabled, Allows user active session to be shared with other users"
 msgstr "开启后允许用户分享已连接的资产会话给它人,协同工作"
 
-#: settings/serializers/security.py:144
+#: settings/serializers/security.py:173
 msgid "Remote Login Protection"
 msgstr "异地登录保护"
 
-#: settings/serializers/security.py:145
+#: settings/serializers/security.py:175
 msgid ""
 "The system determines whether the login IP address belongs to a common login "
 "city. If the account is logged in from a common login city, the system sends "

From 187977c04a25d169b80bcfd9339fe01c14dd9c72 Mon Sep 17 00:00:00 2001
From: xinwen <coderWen@126.com>
Date: Fri, 12 Nov 2021 16:59:56 +0800
Subject: [PATCH 08/25] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=20CPU=20info=20?=
 =?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/assets/serializers/asset.py     | 2 +-
 apps/locale/zh/LC_MESSAGES/django.po | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py
index 0182dfc94..1bb5afe24 100644
--- a/apps/assets/serializers/asset.py
+++ b/apps/assets/serializers/asset.py
@@ -103,7 +103,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
             'port': {'write_only': True},
             'hardware_info': {'label': _('Hardware info'), 'read_only': True},
             'admin_user_display': {'label': _('Admin user display'), 'read_only': True},
-            'cpu_info': {'label': _('CPU Info')},
+            'cpu_info': {'label': _('CPU info')},
         }
 
     def get_fields(self):
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index 57dae93a7..9c34f0e40 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -879,7 +879,7 @@ msgid "Admin user display"
 msgstr "特权用户名称"
 
 #: assets/serializers/asset.py:106
-msgid "CPU Info"
+msgid "CPU info"
 msgstr "CPU信息"
 
 #: assets/serializers/base.py:41

From 4a786baf4edb688477f701e8d9b12b5d12c96166 Mon Sep 17 00:00:00 2001
From: feng626 <1304903146@qq.com>
Date: Fri, 12 Nov 2021 17:32:22 +0800
Subject: [PATCH 09/25] =?UTF-8?q?perf:=20=E5=B7=A5=E5=8D=95=E6=8F=90?=
 =?UTF-8?q?=E7=A4=BA=E4=BF=A1=E6=81=AF=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../ticket/meta/ticket_type/apply_application.py      | 11 ++++++++---
 .../ticket/meta/ticket_type/apply_asset.py            |  7 +++++--
 2 files changed, 13 insertions(+), 5 deletions(-)

diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py b/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py
index c3f75ed6d..98e303b1c 100644
--- a/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py
+++ b/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py
@@ -77,14 +77,19 @@ class ApplySerializer(serializers.Serializer):
         type = self.root.initial_data['meta'].get('apply_type')
         org_id = self.root.initial_data.get('org_id')
         with tmp_to_org(org_id):
-            applications = Application.objects.filter(id__in=apply_applications, type=type).values_list('id', flat=True)
+            applications = Application.objects.filter(
+                id__in=apply_applications, type=type
+            ).values_list('id', flat=True)
         return list(applications)
 
     def validate(self, attrs):
-        apply_date_start = attrs['apply_date_start']
-        apply_date_expired = attrs['apply_date_expired']
+        apply_date_start = attrs['apply_date_start'].strftime('%Y-%m-%d %H:%M:%S')
+        apply_date_expired = attrs['apply_date_expired'].strftime('%Y-%m-%d %H:%M:%S')
 
         if apply_date_expired <= apply_date_start:
             error = _('The expiration date should be greater than the start date')
             raise serializers.ValidationError({'apply_date_expired': error})
+
+        attrs['apply_date_start'] = apply_date_start
+        attrs['apply_date_expired'] = apply_date_expired
         return attrs
diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py b/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py
index a4b78e7d5..19da551ec 100644
--- a/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py
+++ b/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py
@@ -64,10 +64,13 @@ class ApplySerializer(serializers.Serializer):
         ))
 
     def validate(self, attrs):
-        apply_date_start = attrs['apply_date_start']
-        apply_date_expired = attrs['apply_date_expired']
+        apply_date_start = attrs['apply_date_start'].strftime('%Y-%m-%d %H:%M:%S')
+        apply_date_expired = attrs['apply_date_expired'].strftime('%Y-%m-%d %H:%M:%S')
 
         if apply_date_expired <= apply_date_start:
             error = _('The expiration date should be greater than the start date')
             raise serializers.ValidationError({'apply_date_expired': error})
+
+        attrs['apply_date_start'] = apply_date_start
+        attrs['apply_date_expired'] = apply_date_expired
         return attrs

From 454d3cba969fe2ed15fabcdcd26b71a8750bec49 Mon Sep 17 00:00:00 2001
From: ibuler <ibuler@qq.com>
Date: Mon, 15 Nov 2021 16:00:11 +0800
Subject: [PATCH 10/25] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=99=BB?=
 =?UTF-8?q?=E5=BD=95mfa=E6=97=B6=EF=BC=8C=E9=80=89=E6=8B=A9=E4=BA=86?=
 =?UTF-8?q?=E7=A6=81=E7=94=A8=E7=9A=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/locale/zh/LC_MESSAGES/django.po |  2 +-
 apps/templates/_mfa_login_field.html | 11 +++++++++--
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index 9c34f0e40..1ad622d52 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -1690,7 +1690,7 @@ msgstr "没有上传下载权限"
 
 #: authentication/errors.py:366
 msgid "Please enter MFA code"
-msgstr "请输入6位动态安全码"
+msgstr "请输入验证码"
 
 #: authentication/errors.py:370
 msgid "Please enter SMS code"
diff --git a/apps/templates/_mfa_login_field.html b/apps/templates/_mfa_login_field.html
index 6164e0a4c..8a9a1eb71 100644
--- a/apps/templates/_mfa_login_field.html
+++ b/apps/templates/_mfa_login_field.html
@@ -54,9 +54,13 @@
     $(document).ready(function () {
         const mfaSelectRef = document.getElementById('mfa-select');
         const preferMFA = localStorage.getItem(preferMFAKey);
-        if (preferMFA) {
+        const valueSelector = "value=" + preferMFA
+        const preferMFADisabled = $(`#mfa-select option[${valueSelector}]`).attr('disabled')
+
+        if (preferMFA && !preferMFADisabled) {
             mfaSelectRef.value = preferMFA;
         }
+
         const mfaSelect = mfaSelectRef.value;
         if (mfaSelect !== null) {
             selectChange(mfaSelect, true);
@@ -73,7 +77,10 @@
         $('.input-style').each(function (i, ele){
             $(ele).attr('name', '').attr('required', false)
         })
-        $('#mfa-' + name + ' .input-style').attr('name', 'code').attr('required', true)
+        $('#mfa-' + name + ' .input-style')
+            .attr('name', 'code')
+            .attr('required', true)
+            .focus()
     }
 
     function sendChallengeCode(currentBtn) {

From a315e8888b6478cfea0c8fb0639c7dce073a6985 Mon Sep 17 00:00:00 2001
From: ibuler <ibuler@qq.com>
Date: Mon, 15 Nov 2021 14:51:49 +0800
Subject: [PATCH 11/25] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E4=B8=80?=
 =?UTF-8?q?=E4=B8=8B=20win2016=20=E4=B8=8D=E5=86=8D=E5=86=85=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/assets/migrations/0079_auto_20211102_1922.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/apps/assets/migrations/0079_auto_20211102_1922.py b/apps/assets/migrations/0079_auto_20211102_1922.py
index f0a05dc06..592df0259 100644
--- a/apps/assets/migrations/0079_auto_20211102_1922.py
+++ b/apps/assets/migrations/0079_auto_20211102_1922.py
@@ -16,6 +16,11 @@ def create_internal_platform(apps, schema_editor):
             name=name, defaults=defaults
         )
 
+    win2016 = model.objects.filter(name='Windows2016').first()
+    if win2016:
+        win2016.internal = False
+        win2016.save(update_fields=['internal'])
+
 
 class Migration(migrations.Migration):
 

From 12a00969635f31fe75eaa6d9b06fccd0e2097807 Mon Sep 17 00:00:00 2001
From: ibuler <ibuler@qq.com>
Date: Mon, 15 Nov 2021 14:27:11 +0800
Subject: [PATCH 12/25] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=87=8D?=
 =?UTF-8?q?=E7=BD=AEmfa=E7=9A=84bug?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/users/api/user.py      | 13 +++++++------
 apps/users/urls/api_urls.py |  4 ++--
 2 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/apps/users/api/user.py b/apps/users/api/user.py
index 2499e7b78..fe902e8c5 100644
--- a/apps/users/api/user.py
+++ b/apps/users/api/user.py
@@ -28,7 +28,7 @@ from ..filters import OrgRoleUserFilterBackend, UserFilter
 logger = get_logger(__name__)
 __all__ = [
     'UserViewSet', 'UserChangePasswordApi',
-    'UserUnblockPKApi', 'UserResetOTPApi',
+    'UserUnblockPKApi', 'UserResetMFAApi',
 ]
 
 
@@ -199,7 +199,7 @@ class UserUnblockPKApi(UserQuerysetMixin, generics.UpdateAPIView):
         MFABlockUtils.unblock_user(username)
 
 
-class UserResetOTPApi(UserQuerysetMixin, generics.RetrieveAPIView):
+class UserResetMFAApi(UserQuerysetMixin, generics.RetrieveAPIView):
     permission_classes = (IsOrgAdmin,)
     serializer_class = serializers.ResetOTPSerializer
 
@@ -209,9 +209,10 @@ class UserResetOTPApi(UserQuerysetMixin, generics.RetrieveAPIView):
             msg = _("Could not reset self otp, use profile reset instead")
             return Response({"error": msg}, status=401)
 
-        if user.mfa_enabled:
-            user.reset_mfa()
-            user.save()
+        backends = user.active_mfa_backends_mapper
+        for backend in backends:
+            if backend.can_disable():
+                backend.disable()
 
-            ResetMFAMsg(user).publish_async()
+        ResetMFAMsg(user).publish_async()
         return Response({"msg": "success"})
diff --git a/apps/users/urls/api_urls.py b/apps/users/urls/api_urls.py
index 8b9c538bd..af24fc147 100644
--- a/apps/users/urls/api_urls.py
+++ b/apps/users/urls/api_urls.py
@@ -23,8 +23,8 @@ urlpatterns = [
     path('profile/', api.UserProfileApi.as_view(), name='user-profile'),
     path('profile/password/', api.UserPasswordApi.as_view(), name='user-password'),
     path('profile/public-key/', api.UserPublicKeyApi.as_view(), name='user-public-key'),
-    path('otp/reset/', api.UserResetOTPApi.as_view(), name='my-otp-reset'),
-    path('users/<uuid:pk>/otp/reset/', api.UserResetOTPApi.as_view(), name='user-reset-otp'),
+    path('profile/mfa/reset/', api.UserResetMFAApi.as_view(), name='my-mfa-reset'),
+    path('users/<uuid:pk>/mfa/reset/', api.UserResetMFAApi.as_view(), name='user-reset-mfa'),
     path('users/<uuid:pk>/password/', api.UserChangePasswordApi.as_view(), name='change-user-password'),
     path('users/<uuid:pk>/password/reset/', api.UserResetPasswordApi.as_view(), name='user-reset-password'),
     path('users/<uuid:pk>/pubkey/reset/', api.UserResetPKApi.as_view(), name='user-public-key-reset'),

From cb1c906db4ce58055422e737724bd1b46845a512 Mon Sep 17 00:00:00 2001
From: ibuler <ibuler@qq.com>
Date: Mon, 15 Nov 2021 14:00:52 +0800
Subject: [PATCH 13/25] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=99=BB?=
 =?UTF-8?q?=E5=BD=95=E6=97=B6=E6=B2=A1=E6=9C=89=E7=BB=91=E5=AE=9Amfa?=
 =?UTF-8?q?=EF=BC=8C=E6=B2=A1=E6=9C=89=E8=B7=B3=E8=BD=AC=E7=9A=84=E9=97=AE?=
 =?UTF-8?q?=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

fix: 首页登录如果没有则后续登录
---
 apps/authentication/errors.py      | 16 ++++------------
 apps/authentication/mixins.py      | 15 +++++++++++----
 apps/authentication/views/login.py |  4 ++--
 3 files changed, 17 insertions(+), 18 deletions(-)

diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py
index 1e9000e67..f8bf7f32d 100644
--- a/apps/authentication/errors.py
+++ b/apps/authentication/errors.py
@@ -66,7 +66,6 @@ mfa_error_msg = _(
 )
 mfa_required_msg = _("MFA required")
 mfa_unset_msg = _("MFA not set, please set it first")
-otp_unset_msg = _("OTP not set, please set it first")
 login_confirm_required_msg = _("Login confirm required")
 login_confirm_wait_msg = _("Wait login confirm ticket for accept")
 login_confirm_error_msg = _("Login confirm ticket was {}")
@@ -162,13 +161,11 @@ class BlockMFAError(AuthFailedNeedLogMixin, AuthFailedError):
         super().__init__(username=username, request=request, ip=ip)
 
 
-class MFAUnsetError(AuthFailedNeedLogMixin, AuthFailedError):
+class MFAUnsetError(Exception):
     error = reason_mfa_unset
     msg = mfa_unset_msg
 
     def __init__(self, user, request, url):
-        super().__init__(username=user.username, request=request)
-        self.user = user
         self.url = url
 
 
@@ -354,21 +351,16 @@ class NotHaveUpDownLoadPerm(JMSException):
     default_detail = _('No upload or download permission')
 
 
-class OTPBindRequiredError(JMSException):
-    default_detail = otp_unset_msg
-
-    def __init__(self, url, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.url = url
-
-
 class MFACodeRequiredError(AuthFailedError):
+    error = 'mfa_code_required'
     msg = _("Please enter MFA code")
 
 
 class SMSCodeRequiredError(AuthFailedError):
+    error = 'sms_code_required'
     msg = _("Please enter SMS code")
 
 
 class UserPhoneNotSet(AuthFailedError):
+    error = 'phone_not_set'
     msg = _('Phone not set')
diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py
index 69daf6330..8ec2fc391 100644
--- a/apps/authentication/mixins.py
+++ b/apps/authentication/mixins.py
@@ -248,16 +248,24 @@ class MFAMixin:
     get_user_from_session: Callable
     get_request_ip: Callable
 
+    def _check_if_no_active_mfa(self, user):
+        active_mfa_mapper = user.active_mfa_backends_mapper
+        if not active_mfa_mapper:
+            url = reverse('authentication:user-otp-enable-start')
+            raise errors.MFAUnsetError(user, self.request, url)
+
     def _check_login_page_mfa_if_need(self, user):
         if not settings.SECURITY_MFA_IN_LOGIN_PAGE:
             return
+        self._check_if_no_active_mfa(user)
 
         request = self.request
         data = request.data if hasattr(request, 'data') else request.POST
         code = data.get('code')
         mfa_type = data.get('mfa_type', 'otp')
+
         if not code:
-            raise errors.MFACodeRequiredError
+            return
         self._do_check_user_mfa(code, mfa_type, user=user)
 
     def check_user_mfa_if_need(self, user):
@@ -266,10 +274,9 @@ class MFAMixin:
         if not user.mfa_enabled:
             return
 
+        self._check_if_no_active_mfa(user)
+
         active_mfa_mapper = user.active_mfa_backends_mapper
-        if not active_mfa_mapper:
-            url = reverse('authentication:user-otp-enable-start')
-            raise errors.MFAUnsetError(user, self.request, url)
         raise errors.MFARequiredError(mfa_types=tuple(active_mfa_mapper.keys()))
 
     def mark_mfa_ok(self, mfa_type):
diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py
index 79b6839b4..f8c760b43 100644
--- a/apps/authentication/views/login.py
+++ b/apps/authentication/views/login.py
@@ -122,10 +122,10 @@ class UserLoginView(mixins.AuthMixin, FormView):
             self.request.session.set_test_cookie()
             return self.render_to_response(context)
         except (
+                errors.MFAUnsetError,
                 errors.PasswordTooSimple,
                 errors.PasswordRequireResetError,
-                errors.PasswordNeedUpdate,
-                errors.OTPBindRequiredError
+                errors.PasswordNeedUpdate
         ) as e:
             return redirect(e.url)
         except (

From 9bfbdea508b2047e8de42bdd2a0b4c27e96afe65 Mon Sep 17 00:00:00 2001
From: xinwen <coderWen@126.com>
Date: Mon, 15 Nov 2021 16:29:22 +0800
Subject: [PATCH 14/25] =?UTF-8?q?fix:=20=E7=BB=88=E7=AB=AF=E5=88=A0?=
 =?UTF-8?q?=E9=99=A4=E5=90=8E=EF=BC=8C=E5=BD=95=E5=83=8F=E5=AD=98=E5=82=A8?=
 =?UTF-8?q?=E4=B8=8D=E8=83=BD=E5=88=A0=E9=99=A4=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/terminal/models/storage.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apps/terminal/models/storage.py b/apps/terminal/models/storage.py
index 7d4efcfbc..bb2495c7e 100644
--- a/apps/terminal/models/storage.py
+++ b/apps/terminal/models/storage.py
@@ -164,4 +164,4 @@ class ReplayStorage(CommonStorageModelMixin, CommonModelMixin):
         return storage.is_valid(src, target)
 
     def is_use(self):
-        return Terminal.objects.filter(replay_storage=self.name).exists()
+        return Terminal.objects.filter(replay_storage=self.name, is_deleted=False).exists()

From d6395b64fab812112fbfcd904cebbce8f447a2b1 Mon Sep 17 00:00:00 2001
From: fit2bot <68588906+fit2bot@users.noreply.github.com>
Date: Mon, 15 Nov 2021 16:51:05 +0800
Subject: [PATCH 15/25] merge: with dev (#7195)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix: 修复登录mfa时,选择了禁用的

* fix: mfa focus

Co-authored-by: ibuler <ibuler@qq.com>
---
 apps/templates/_mfa_login_field.html | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/apps/templates/_mfa_login_field.html b/apps/templates/_mfa_login_field.html
index 8a9a1eb71..31d59ec24 100644
--- a/apps/templates/_mfa_login_field.html
+++ b/apps/templates/_mfa_login_field.html
@@ -77,6 +77,14 @@
         $('.input-style').each(function (i, ele){
             $(ele).attr('name', '').attr('required', false)
         })
+
+        const currentMFAInput = $('#mfa-' + name + ' .input-style')
+        currentMFAInput.attr('name', 'code').attr('required', true)
+
+        // 登录页时,不应该默认focus
+        if ($('input[name="username"]').length == 0) {
+            currentMFAInput.focus()
+        }
         $('#mfa-' + name + ' .input-style')
             .attr('name', 'code')
             .attr('required', true)

From 7d9da9ff66991aaaed5c3c59769e923340577bfc Mon Sep 17 00:00:00 2001
From: ibuler <ibuler@qq.com>
Date: Mon, 15 Nov 2021 16:53:35 +0800
Subject: [PATCH 16/25] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=E5=90=8D?=
 =?UTF-8?q?=E7=A7=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/templates/_mfa_login_field.html | 14 ++++++--------
 1 file changed, 6 insertions(+), 8 deletions(-)

diff --git a/apps/templates/_mfa_login_field.html b/apps/templates/_mfa_login_field.html
index 31d59ec24..f4811a11a 100644
--- a/apps/templates/_mfa_login_field.html
+++ b/apps/templates/_mfa_login_field.html
@@ -78,17 +78,15 @@
             $(ele).attr('name', '').attr('required', false)
         })
 
-        const currentMFAInput = $('#mfa-' + name + ' .input-style')
-        currentMFAInput.attr('name', 'code').attr('required', true)
+        const currentMFAInputRef = $('#mfa-' + name + ' .input-style')
+        currentMFAInputRef.attr('name', 'code').attr('required', true)
 
         // 登录页时,不应该默认focus
-        if ($('input[name="username"]').length == 0) {
-            currentMFAInput.focus()
+        const usernameRef = $('input[name="username"]')
+        if (!usernameRef || usernameRef.length === 0) {
+            currentMFAInputRef.focus()
         }
-        $('#mfa-' + name + ' .input-style')
-            .attr('name', 'code')
-            .attr('required', true)
-            .focus()
+        currentMFAInputRef.attr('name', 'code').attr('required', true)
     }
 
     function sendChallengeCode(currentBtn) {

From c5ff0d972bdf5602da5fffb64cf5fe29079c03e6 Mon Sep 17 00:00:00 2001
From: ibuler <ibuler@qq.com>
Date: Mon, 15 Nov 2021 16:56:56 +0800
Subject: [PATCH 17/25] =?UTF-8?q?perf:=20=E5=8E=BB=E6=8E=89mfa=20input=20?=
 =?UTF-8?q?=E7=9A=84required=EF=BC=8C=E4=BB=A5=E4=B8=BA=E7=94=A8=E6=88=B7?=
 =?UTF-8?q?=E5=8F=AF=E8=83=BD=E6=B2=A1=E6=9C=89=E8=AE=BE=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/templates/_mfa_login_field.html | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/apps/templates/_mfa_login_field.html b/apps/templates/_mfa_login_field.html
index f4811a11a..52918acfd 100644
--- a/apps/templates/_mfa_login_field.html
+++ b/apps/templates/_mfa_login_field.html
@@ -75,7 +75,7 @@
         }
 
         $('.input-style').each(function (i, ele){
-            $(ele).attr('name', '').attr('required', false)
+            $(ele).attr('name', '')
         })
 
         const currentMFAInputRef = $('#mfa-' + name + ' .input-style')
@@ -86,7 +86,7 @@
         if (!usernameRef || usernameRef.length === 0) {
             currentMFAInputRef.focus()
         }
-        currentMFAInputRef.attr('name', 'code').attr('required', true)
+        currentMFAInputRef.attr('name', 'code')
     }
 
     function sendChallengeCode(currentBtn) {

From ed01f2f1fb3d7ca277805e296a416c30517971af Mon Sep 17 00:00:00 2001
From: fit2bot <68588906+fit2bot@users.noreply.github.com>
Date: Tue, 16 Nov 2021 11:32:25 +0800
Subject: [PATCH 18/25] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dxrdp=E8=BF=9E?=
 =?UTF-8?q?=E6=8E=A5=E6=97=B6=E6=8A=A5=E9=94=99=20(#7202)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix: 修复xrdp连接时报错

perf: 添加注释

* perf: 去掉import

Co-authored-by: ibuler <ibuler@qq.com>
---
 apps/authentication/api/connection_token.py | 33 +++++++++++++--------
 apps/authentication/errors.py               |  6 ----
 2 files changed, 21 insertions(+), 18 deletions(-)

diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py
index 2ff4fa71c..4083edb90 100644
--- a/apps/authentication/api/connection_token.py
+++ b/apps/authentication/api/connection_token.py
@@ -26,7 +26,6 @@ from orgs.mixins.api import RootOrgViewMixin
 from common.http import is_true
 from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user
 from perms.models.asset_permission import Action
-from authentication.errors import NotHaveUpDownLoadPerm
 
 from ..serializers import (
     ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
@@ -96,22 +95,26 @@ class ClientProtocolMixin:
         drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
         token = self.create_token(user, asset, application, system_user)
 
+        # 设置磁盘挂载
         if drives_redirect and asset:
             systemuser_actions_mapper = get_asset_system_user_ids_with_actions_by_user(user, asset)
-            actions = systemuser_actions_mapper.get(system_user.id, [])
+            actions = systemuser_actions_mapper.get(system_user.id, 0)
             if actions & Action.UPDOWNLOAD:
                 options['drivestoredirect:s'] = '*'
-            else:
-                raise NotHaveUpDownLoadPerm
 
+        # 全屏
         options['screen mode id:i'] = '2' if full_screen else '1'
+
+        # RDP Server 地址
         address = settings.TERMINAL_RDP_ADDR
         if not address or address == 'localhost:3389':
             address = self.request.get_host().split(':')[0] + ':3389'
         options['full address:s'] = address
+        # 用户名
         options['username:s'] = '{}|{}'.format(user.username, token)
         if system_user.ad_domain:
             options['domain:s'] = system_user.ad_domain
+        # 宽高
         if width and height:
             options['desktopwidth:i'] = width
             options['desktopheight:i'] = height
@@ -160,13 +163,16 @@ class ClientProtocolMixin:
         asset, application, system_user, user = self.get_request_resource(serializer)
         protocol = system_user.protocol
         username = user.username
-        name = ''
+
         if protocol == 'rdp':
             name, config = self.get_rdp_file_content(serializer)
-        elif protocol == 'vnc':
-            raise HttpResponse(status=404, data={"error": "VNC not support"})
-        else:
+        elif protocol == 'ssh':
+            # Todo:
+            name = ''
             config = 'ssh://system_user@asset@user@jumpserver-ssh'
+        else:
+            raise ValueError('Protocol not support: {}'.format(protocol))
+
         filename = "{}-{}-jumpserver".format(username, name)
         data = {
             "filename": filename,
@@ -179,8 +185,13 @@ class ClientProtocolMixin:
     @action(methods=['POST', 'GET'], detail=False, url_path='client-url', permission_classes=[IsValidUser])
     def get_client_protocol_url(self, request, *args, **kwargs):
         serializer = self.get_valid_serializer()
-        protocol_data = self.get_client_protocol_data(serializer)
-        protocol_data = base64.b64encode(json.dumps(protocol_data).encode()).decode()
+        try:
+            protocol_data = self.get_client_protocol_data(serializer)
+        except ValueError as e:
+            return Response({'error': str(e)}, status=401)
+
+        protocol_data = json.dumps(protocol_data).encode()
+        protocol_data = base64.b64encode(protocol_data).decode()
         data = {
             'url': 'jms://{}'.format(protocol_data),
         }
@@ -348,14 +359,12 @@ class UserConnectionTokenViewSet(
             raise serializers.ValidationError("User not valid, disabled or expired")
 
         system_user = get_object_or_404(SystemUser, id=value.get('system_user'))
-
         asset = None
         app = None
         if value.get('type') == 'asset':
             asset = get_object_or_404(Asset, id=value.get('asset'))
             if not asset.is_active:
                 raise serializers.ValidationError("Asset disabled")
-
             has_perm, expired_at = asset_validate_permission(user, asset, system_user, 'connect')
         else:
             app = get_object_or_404(Application, id=value.get('application'))
diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py
index f8bf7f32d..c17a1bccb 100644
--- a/apps/authentication/errors.py
+++ b/apps/authentication/errors.py
@@ -345,12 +345,6 @@ class PasswordInvalid(JMSException):
     default_detail = _('Your password is invalid')
 
 
-class NotHaveUpDownLoadPerm(JMSException):
-    status_code = status.HTTP_403_FORBIDDEN
-    code = 'not_have_up_down_load_perm'
-    default_detail = _('No upload or download permission')
-
-
 class MFACodeRequiredError(AuthFailedError):
     error = 'mfa_code_required'
     msg = _("Please enter MFA code")

From 22c9dfc0f21ee70dcff673fc43a5971f14d8fe02 Mon Sep 17 00:00:00 2001
From: xinwen <coderWen@126.com>
Date: Tue, 16 Nov 2021 11:12:59 +0800
Subject: [PATCH 19/25] =?UTF-8?q?fix:=20=E7=B3=BB=E7=BB=9F=E7=94=A8?=
 =?UTF-8?q?=E6=88=B7=20actions?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/perms/utils/asset/permission.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apps/perms/utils/asset/permission.py b/apps/perms/utils/asset/permission.py
index 949ee6873..349040b99 100644
--- a/apps/perms/utils/asset/permission.py
+++ b/apps/perms/utils/asset/permission.py
@@ -86,7 +86,7 @@ def get_asset_system_user_ids_with_actions_by_user(user: User, asset: Asset):
 
 def has_asset_system_permission(user: User, asset: Asset, system_user: SystemUser):
     systemuser_actions_mapper = get_asset_system_user_ids_with_actions_by_user(user, asset)
-    actions = systemuser_actions_mapper.get(system_user.id, [])
+    actions = systemuser_actions_mapper.get(system_user.id, 0)
     if actions:
         return True
     return False

From cc2d47e6dc66ab33144aca04a63ea2e6717cd33e Mon Sep 17 00:00:00 2001
From: ibuler <ibuler@qq.com>
Date: Mon, 15 Nov 2021 18:36:22 +0800
Subject: [PATCH 20/25] =?UTF-8?q?perf:=20=E4=BF=AE=E5=A4=8D=E9=A6=96?=
 =?UTF-8?q?=E9=A1=B5=E7=99=BB=E5=BD=95mfa=E9=94=99=E8=AF=AF=E6=8F=90?=
 =?UTF-8?q?=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/authentication/api/mfa.py                |   2 +-
 apps/authentication/errors.py                 |   2 +-
 apps/authentication/mfa/otp.py                |   1 +
 apps/authentication/mfa/radius.py             |   3 +-
 apps/authentication/mfa/sms.py                |   8 +-
 apps/authentication/mixins.py                 |   9 +-
 .../templates/authentication/login.html       |   2 +-
 .../templates/authentication/login_mfa.html   |   2 +-
 apps/locale/zh/LC_MESSAGES/django.mo          |   4 +-
 apps/locale/zh/LC_MESSAGES/django.po          | 163 +++++++++---------
 apps/users/models/user.py                     |   8 +-
 11 files changed, 113 insertions(+), 91 deletions(-)

diff --git a/apps/authentication/api/mfa.py b/apps/authentication/api/mfa.py
index 751231819..1387de8fe 100644
--- a/apps/authentication/api/mfa.py
+++ b/apps/authentication/api/mfa.py
@@ -44,7 +44,7 @@ class MFASendCodeApi(AuthMixin, CreateAPIView):
         else:
             user = get_object_or_404(User, username=username)
 
-        mfa_backend = user.get_mfa_backend_by_type(mfa_type)
+        mfa_backend = user.get_active_mfa_backend_by_type(mfa_type)
         if not mfa_backend or not mfa_backend.challenge_required:
             raise ValidationError('MFA type not support: {} {}'.format(mfa_type, mfa_backend))
         mfa_backend.send_challenge()
diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py
index c17a1bccb..015ca19ad 100644
--- a/apps/authentication/errors.py
+++ b/apps/authentication/errors.py
@@ -60,7 +60,7 @@ block_mfa_msg = _(
     "(please contact admin to unlock it or try again after {} minutes)"
 )
 mfa_error_msg = _(
-    "{error},"
+    "{error}, "
     "You can also try {times_try} times "
     "(The account will be temporarily locked for {block_time} minutes)"
 )
diff --git a/apps/authentication/mfa/otp.py b/apps/authentication/mfa/otp.py
index 9d67c4ae2..912978d25 100644
--- a/apps/authentication/mfa/otp.py
+++ b/apps/authentication/mfa/otp.py
@@ -10,6 +10,7 @@ otp_failed_msg = _("OTP code invalid, or server time error")
 class MFAOtp(BaseMFA):
     name = 'otp'
     display_name = _('OTP')
+    placeholder = _('OTP verification code')
 
     def check_code(self, code):
         from users.utils import check_otp_code
diff --git a/apps/authentication/mfa/radius.py b/apps/authentication/mfa/radius.py
index ad20456c1..9f1250234 100644
--- a/apps/authentication/mfa/radius.py
+++ b/apps/authentication/mfa/radius.py
@@ -9,7 +9,8 @@ mfa_failed_msg = _("Radius verify code invalid")
 
 class MFARadius(BaseMFA):
     name = 'otp_radius'
-    display_name = _('Radius MFA')
+    display_name = 'Radius'
+    placeholder = _("Radius verification code")
 
     def check_code(self, code):
         assert self.is_authenticated()
diff --git a/apps/authentication/mfa/sms.py b/apps/authentication/mfa/sms.py
index cc2855cfd..670e49378 100644
--- a/apps/authentication/mfa/sms.py
+++ b/apps/authentication/mfa/sms.py
@@ -19,8 +19,12 @@ class MFASms(BaseMFA):
 
     def check_code(self, code):
         assert self.is_authenticated()
-        ok = self.sms.verify(code)
-        msg = '' if ok else sms_failed_msg
+        ok = False
+        msg = ''
+        try:
+            ok = self.sms.verify(code)
+        except Exception as e:
+            msg = str(e)
         return ok, msg
 
     def is_active(self):
diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py
index 8ec2fc391..4d70439f4 100644
--- a/apps/authentication/mixins.py
+++ b/apps/authentication/mixins.py
@@ -312,10 +312,13 @@ class MFAMixin:
 
         ok = False
         mfa_backend = user.get_mfa_backend_by_type(mfa_type)
-        if mfa_backend:
-            ok, msg = mfa_backend.check_code(code)
+        backend_error = _('The MFA type ({}) is not enabled')
+        if not mfa_backend:
+            msg = backend_error.format(mfa_type)
+        elif not mfa_backend.is_active():
+            msg = backend_error.format(mfa_backend.display_name)
         else:
-            msg = _('The MFA type({}) is not supported'.format(mfa_type))
+            ok, msg = mfa_backend.check_code(code)
 
         if ok:
             self.mark_mfa_ok(mfa_type)
diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html
index a6550e725..6e053b877 100644
--- a/apps/authentication/templates/authentication/login.html
+++ b/apps/authentication/templates/authentication/login.html
@@ -109,7 +109,7 @@
         }
 
         .select-con {
-            width: 22%;
+            width: 30%;
         }
 
         .mfa-div {
diff --git a/apps/authentication/templates/authentication/login_mfa.html b/apps/authentication/templates/authentication/login_mfa.html
index cde6e367a..4e33af224 100644
--- a/apps/authentication/templates/authentication/login_mfa.html
+++ b/apps/authentication/templates/authentication/login_mfa.html
@@ -3,7 +3,7 @@
 {% load i18n %}
 
 {% block title %}
-    {% trans 'MFA' %}
+    {% trans 'MFA Auth' %}
 {% endblock %}
 
 {% block content %}
diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo
index 2aec9545b..1fcdb6691 100644
--- a/apps/locale/zh/LC_MESSAGES/django.mo
+++ b/apps/locale/zh/LC_MESSAGES/django.mo
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:fd23c3a1a8f833e99937d38db94b5e32b308c9c1912f8b258ff009d96f9bf2bb
-size 92755
+oid sha256:0cbfc40871a5626f0798771004e92949986e1b5f845c03783590baf38aa43c2e
+size 93462
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index 1ad622d52..14e729bb1 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: JumpServer 0.3.3\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-11-12 16:51+0800\n"
+"POT-Creation-Date: 2021-11-15 17:52+0800\n"
 "PO-Revision-Date: 2021-05-20 10:54+0800\n"
 "Last-Translator: ibuler <ibuler@qq.com>\n"
 "Language-Team: JumpServer team<ibuler@qq.com>\n"
@@ -1223,11 +1223,10 @@ msgstr "用户代理"
 
 #: audits/models.py:110
 #: authentication/templates/authentication/_mfa_confirm_modal.html:14
-#: authentication/templates/authentication/login_mfa.html:6
 #: users/forms/profile.py:64 users/models/user.py:563
 #: users/serializers/profile.py:102
 msgid "MFA"
-msgstr "多因子认证"
+msgstr "MFA"
 
 #: audits/models.py:111 audits/serializers.py:45 terminal/models/sharing.py:88
 #: xpack/plugins/change_auth_plan/models/base.py:187
@@ -1258,7 +1257,7 @@ msgstr "状态名称"
 
 #: audits/serializers.py:31
 msgid "MFA display"
-msgstr "多因子认证"
+msgstr "MFA名称"
 
 #: audits/serializers.py:76 audits/serializers.py:91 ops/models/adhoc.py:248
 #: terminal/serializers/session.py:35
@@ -1500,8 +1499,6 @@ msgid "Invalid token"
 msgstr "无效的令牌"
 
 #: authentication/api/mfa.py:103
-#, fuzzy
-#| msgid "Code is invalid, "
 msgid "Code is invalid"
 msgstr "验证码无效"
 
@@ -1566,11 +1563,11 @@ msgstr "密码解密失败"
 
 #: authentication/errors.py:28
 msgid "MFA failed"
-msgstr "多因子认证失败"
+msgstr "MFA 校验失败"
 
 #: authentication/errors.py:29
 msgid "MFA unset"
-msgstr "多因子认证没有设定"
+msgstr "MFA 没有设定"
 
 #: authentication/errors.py:30
 msgid "Username does not exist"
@@ -1627,76 +1624,72 @@ msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)"
 #: authentication/errors.py:63
 #, python-brace-format
 msgid ""
-"{error},You can also try {times_try} times (The account will be temporarily "
+"{error}, You can also try {times_try} times (The account will be temporarily "
 "locked for {block_time} minutes)"
 msgstr ""
-"{error},您还可以尝试 {times_try} 次(账号将被临时锁定 {block_time} 分钟)"
+"{error},您还可以尝试 {times_try} 次(账号将被临时锁定 {block_time} 分钟)"
 
 #: authentication/errors.py:67
 msgid "MFA required"
-msgstr "需要多因子认证"
+msgstr "需要 MFA 认证"
 
 #: authentication/errors.py:68
 msgid "MFA not set, please set it first"
-msgstr "多因子认证没有设置,请先完成设置"
+msgstr "MFA 没有设置,请先完成设置"
 
 #: authentication/errors.py:69
-msgid "OTP not set, please set it first"
-msgstr "OTP认证没有设置,请先完成设置"
-
-#: authentication/errors.py:70
 msgid "Login confirm required"
 msgstr "需要登录复核"
 
-#: authentication/errors.py:71
+#: authentication/errors.py:70
 msgid "Wait login confirm ticket for accept"
 msgstr "等待登录复核处理"
 
-#: authentication/errors.py:72
+#: authentication/errors.py:71
 msgid "Login confirm ticket was {}"
 msgstr "登录复核 {}"
 
-#: authentication/errors.py:187 authentication/errors.py:251
+#: authentication/errors.py:184 authentication/errors.py:248
 msgid "IP is not allowed"
 msgstr "来源 IP 不被允许登录"
 
-#: authentication/errors.py:258
+#: authentication/errors.py:255
 msgid "Time Period is not allowed"
 msgstr "该 时间段 不被允许登录"
 
-#: authentication/errors.py:291
+#: authentication/errors.py:288
 msgid "SSO auth closed"
 msgstr "SSO 认证关闭了"
 
-#: authentication/errors.py:296 authentication/mixins.py:350
+#: authentication/errors.py:293 authentication/mixins.py:359
 msgid "Your password is too simple, please change it for security"
 msgstr "你的密码过于简单,为了安全,请修改"
 
-#: authentication/errors.py:305 authentication/mixins.py:357
+#: authentication/errors.py:302 authentication/mixins.py:366
 msgid "You should to change your password before login"
 msgstr "登录完成前,请先修改密码"
 
-#: authentication/errors.py:314 authentication/mixins.py:364
+#: authentication/errors.py:311 authentication/mixins.py:373
 msgid "Your password has expired, please reset before logging in"
 msgstr "您的密码已过期,先修改再登录"
 
-#: authentication/errors.py:348
+#: authentication/errors.py:345
 msgid "Your password is invalid"
 msgstr "您的密码无效"
 
-#: authentication/errors.py:354
+#: authentication/errors.py:351
 msgid "No upload or download permission"
 msgstr "没有上传下载权限"
 
-#: authentication/errors.py:366
+#: authentication/errors.py:356
 msgid "Please enter MFA code"
-msgstr "请输入验证码"
+msgstr "请输入 MFA 验证码"
 
-#: authentication/errors.py:370
+#: authentication/errors.py:361
 msgid "Please enter SMS code"
 msgstr "请输入短信验证码"
 
-#: authentication/errors.py:374 users/exceptions.py:15
+#: authentication/errors.py:366 users/exceptions.py:15
 msgid "Phone not set"
 msgstr "手机号没有设置"
 
@@ -1714,7 +1707,7 @@ msgstr "MFA 类型"
 
 #: authentication/forms.py:60 users/forms/profile.py:27
 msgid "MFA code"
-msgstr "多因子认证验证码"
+msgstr "MFA 验证码"
 
 #: authentication/forms.py:62
 msgid "Dynamic code"
@@ -1722,29 +1715,33 @@ msgstr "动态码"
 
 #: authentication/mfa/base.py:7
 msgid "Please input security code"
-msgstr "请输入 6 位动态安全码"
+msgstr "请输入动态安全码"
 
 #: authentication/mfa/otp.py:7
 msgid "OTP code invalid, or server time error"
-msgstr "MFA (OTP) 验证码错误,或者服务器端时间不对"
+msgstr "虚拟 MFA 验证码错误,或者服务器端时间不对"
 
 #: authentication/mfa/otp.py:12
 msgid "OTP"
-msgstr "MFA (OTP)"
+msgstr "MFA"
 
-#: authentication/mfa/otp.py:47
+#: authentication/mfa/otp.py:13
+msgid "OTP verification code"
+msgstr "虚拟 MFA 验证码"
+
+#: authentication/mfa/otp.py:48
 msgid "Virtual OTP based MFA"
-msgstr "虚拟 MFA (OTP)"
+msgstr "虚拟 MFA(OTP)"
 
 #: authentication/mfa/radius.py:7
 msgid "Radius verify code invalid"
 msgstr "Radius 校验失败"
 
-#: authentication/mfa/radius.py:12
-msgid "Radius MFA"
-msgstr "Radius MFA"
+#: authentication/mfa/radius.py:13
+msgid "Radius verification code"
+msgstr "Radius 动态安全码"
 
-#: authentication/mfa/radius.py:43
+#: authentication/mfa/radius.py:44
 msgid "Radius global enabled, cannot disable"
 msgstr "Radius MFA 全局开启,无法被禁用"
 
@@ -1760,19 +1757,19 @@ msgstr "短信"
 msgid "SMS verification code"
 msgstr "短信验证码"
 
-#: authentication/mfa/sms.py:53
+#: authentication/mfa/sms.py:57
 msgid "Set phone number to enable"
 msgstr "设置手机号码启用"
 
-#: authentication/mfa/sms.py:57
+#: authentication/mfa/sms.py:61
 msgid "Clear phone number to disable"
 msgstr "清空手机号码禁用"
 
-#: authentication/mixins.py:311
-msgid "The MFA type({}) is not supported"
-msgstr "该 MFA 方法 ({}) 不被支持"
+#: authentication/mixins.py:316 authentication/mixins.py:318
+msgid "The MFA type ({}) is not enabled"
+msgstr "该 MFA ({}) 方式没有启用"
 
-#: authentication/mixins.py:340
+#: authentication/mixins.py:349
 msgid "Please change your password"
 msgstr "请修改密码"
 
@@ -1855,11 +1852,11 @@ msgstr "验证码"
 
 #: authentication/templates/authentication/_mfa_confirm_modal.html:5
 msgid "MFA confirm"
-msgstr "多因子认证校验"
+msgstr "MFA 认证校验"
 
 #: authentication/templates/authentication/_mfa_confirm_modal.html:17
 msgid "Need MFA for view auth"
-msgstr "需要多因子认证来查看账号信息"
+msgstr "需要 MFA 认证来查看账号信息"
 
 #: authentication/templates/authentication/_mfa_confirm_modal.html:20
 #: templates/_modal.html:23 templates/flash_message_standalone.html:34
@@ -1968,6 +1965,10 @@ msgstr "登录"
 msgid "More login options"
 msgstr "更多登录方式"
 
+#: authentication/templates/authentication/login_mfa.html:6
+msgid "MFA Auth"
+msgstr "MFA 多因子认证"
+
 #: authentication/templates/authentication/login_mfa.html:19
 #: users/templates/users/user_otp_check_password.html:18
 #: users/templates/users/user_otp_enable_bind.html:24
@@ -1978,7 +1979,7 @@ msgstr "下一步"
 
 #: authentication/templates/authentication/login_mfa.html:22
 msgid "Can't provide security? Please contact the administrator!"
-msgstr "如果不能提供多因子认证验证码,请联系管理员!"
+msgstr "如果不能提供 MFA 验证码,请联系管理员!"
 
 #: authentication/templates/authentication/login_wait_confirm.html:41
 msgid "Refresh"
@@ -3140,6 +3141,18 @@ msgstr "上传下载"
 msgid "Cloud sync record keep days"
 msgstr "云同步记录"
 
+#: settings/serializers/cleaning.py:29
+msgid "Session keep duration"
+msgstr "会话日志保存时间"
+
+#: settings/serializers/cleaning.py:30
+msgid ""
+"Unit: days, Session, record, command will be delete if more than duration, "
+"only in database"
+msgstr ""
+"单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不"
+"受影响)"
+
 #: settings/serializers/email.py:18
 msgid "SMTP host"
 msgstr "SMTP 主机"
@@ -3423,7 +3436,7 @@ msgstr ""
 
 #: settings/serializers/security.py:112
 msgid "MFA in login page"
-msgstr "MFA 在登录页面"
+msgstr "MFA 在登录页面输入"
 
 #: settings/serializers/security.py:113
 msgid "Eu security regulations(GDPR) require MFA to be on the login page"
@@ -3545,36 +3558,24 @@ msgid "List page size"
 msgstr "资产列表每页数量"
 
 #: settings/serializers/terminal.py:29
-msgid "Session keep duration"
-msgstr "会话日志保存时间"
-
-#: settings/serializers/terminal.py:30
-msgid ""
-"Unit: days, Session, record, command will be delete if more than duration, "
-"only in database"
-msgstr ""
-"单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不"
-"受影响)"
-
-#: settings/serializers/terminal.py:33
 msgid "Telnet login regex"
 msgstr "Telnet 成功正则表达式"
 
-#: settings/serializers/terminal.py:34
+#: settings/serializers/terminal.py:30
 msgid ""
 "The login success message varies with devices. if you cannot log in to the "
 "device through Telnet, set this parameter"
 msgstr "不同设备登录成功提示不一样,所以如果 telnet 不能正常登录,可以这里设置"
 
-#: settings/serializers/terminal.py:38
+#: settings/serializers/terminal.py:34
 msgid "RDP address"
 msgstr "RDP 地址"
 
-#: settings/serializers/terminal.py:39
+#: settings/serializers/terminal.py:35
 msgid "RDP visit address, eg: dev.jumpserver.org:3389"
 msgstr "RDP 访问地址, 如: dev.jumpserver.org:3389"
 
-#: settings/serializers/terminal.py:42
+#: settings/serializers/terminal.py:38
 msgid "Enable XRDP"
 msgstr "启用 XRDP 服务"
 
@@ -3826,11 +3827,11 @@ msgstr ""
 msgid "Send verification code"
 msgstr "发送验证码"
 
-#: templates/_mfa_login_field.html:92
+#: templates/_mfa_login_field.html:107
 msgid "Wait: "
 msgstr "等待:"
 
-#: templates/_mfa_login_field.html:102
+#: templates/_mfa_login_field.html:117
 msgid "The verification code has been sent"
 msgstr "验证码已发送"
 
@@ -4828,7 +4829,7 @@ msgstr "批准的系统用户名称"
 msgid "Permission named `{}` already exists"
 msgstr "授权名称 `{}` 已存在"
 
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:88
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:90
 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:71
 msgid "The expiration date should be greater than the start date"
 msgstr "过期时间要大于开始时间"
@@ -4924,7 +4925,7 @@ msgstr "点击查看"
 
 #: users/api/user.py:209
 msgid "Could not reset self otp, use profile reset instead"
-msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置"
+msgstr "不能在该页面重置 MFA 多因子认证, 请去个人信息页面重置"
 
 #: users/const.py:10 users/models/user.py:167
 msgid "System administrator"
@@ -4944,11 +4945,11 @@ msgstr "设置密码"
 
 #: users/exceptions.py:10
 msgid "MFA not enabled"
-msgstr "MFA没有开启"
+msgstr "MFA 多因子认证没有开启"
 
 #: users/exceptions.py:20
 msgid "MFA method not support"
-msgstr "MFA 方法不支持"
+msgstr "不支持该 MFA 方式"
 
 #: users/forms/profile.py:49
 msgid ""
@@ -4957,11 +4958,11 @@ msgid ""
 "modification -> change MFA Settings\"!"
 msgstr ""
 "启用之后您将会在下次登录时进入多因子认证绑定流程;您也可以在(个人信息->快速"
-"修改->更改多因子设置)中直接绑定!"
+"修改->设置 MFA 多因子认证)中直接绑定!"
 
 #: users/forms/profile.py:60
 msgid "* Enable MFA to make the account more secure."
-msgstr "* 启用多因子认证,使账号更加安全。"
+msgstr "* 启用 MFA 多因子认证,使账号更加安全。"
 
 #: users/forms/profile.py:69
 msgid ""
@@ -4970,7 +4971,7 @@ msgid ""
 "password, enabling MFA)"
 msgstr ""
 "为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:"
-"设置复杂密码,并启用多因子认证)"
+"设置复杂密码,并启用 MFA 多因子认证)"
 
 #: users/forms/profile.py:76
 msgid "Finish"
@@ -5189,7 +5190,7 @@ msgstr "是否绑定了飞书"
 
 #: users/serializers/user.py:92
 msgid "Is OTP bound"
-msgstr "是否绑定了虚拟MFA"
+msgstr "是否绑定了虚拟 MFA"
 
 #: users/serializers/user.py:116
 msgid "Role limit to {}"
@@ -5463,7 +5464,7 @@ msgstr "账号保护已开启,请根据提示完成以下操作"
 
 #: users/templates/users/user_verify_mfa.html:17
 msgid "Open MFA Authenticator and enter the 6-bit dynamic code"
-msgstr "请打开MFA验证器,输入6位动态码"
+msgstr "请打开 MFA 验证器,输入 6 位动态码"
 
 #: users/views/profile/otp.py:80
 msgid "Already bound"
@@ -5483,7 +5484,7 @@ msgstr "MFA (OTP) 启用成功,返回到登录页面"
 
 #: users/views/profile/otp.py:151
 msgid "Disable OTP"
-msgstr "禁用 MFA (OTP)"
+msgstr "禁用虚拟 MFA(OTP)"
 
 #: users/views/profile/otp.py:157
 msgid "OTP disable success"
@@ -6126,6 +6127,12 @@ msgstr "旗舰版"
 msgid "Community edition"
 msgstr "社区版"
 
+#~ msgid "OTP not set, please set it first"
+#~ msgstr "OTP认证没有设置,请先完成设置"
+
+#~ msgid "Radius MFA"
+#~ msgstr "Radius MFA"
+
 #~ msgid "Help Website URL"
 #~ msgstr "官网链接"
 
diff --git a/apps/users/models/user.py b/apps/users/models/user.py
index 0611d67cb..59c5b9a03 100644
--- a/apps/users/models/user.py
+++ b/apps/users/models/user.py
@@ -507,8 +507,14 @@ class MFAMixin:
         backends = [cls(user) for cls in MFA_BACKENDS if cls.global_enabled()]
         return backends
 
+    def get_active_mfa_backend_by_type(self, mfa_type):
+        backend = self.get_mfa_backend_by_type(mfa_type)
+        if not backend or not backend.is_active():
+            return None
+        return backend
+
     def get_mfa_backend_by_type(self, mfa_type):
-        mfa_mapper = self.active_mfa_backends_mapper
+        mfa_mapper = {b.name: b for b in self.get_user_mfa_backends(self)}
         backend = mfa_mapper.get(mfa_type)
         if not backend:
             return None

From 2a9f0f8dcf7632f806dfbaebed6da8b55408e193 Mon Sep 17 00:00:00 2001
From: feng626 <1304903146@qq.com>
Date: Wed, 17 Nov 2021 15:40:00 +0800
Subject: [PATCH 21/25] =?UTF-8?q?perf:=20help=E4=BF=A1=E6=81=AF=E8=BF=81?=
 =?UTF-8?q?=E7=A7=BB=E5=88=B0public=E4=B8=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/jumpserver/settings/custom.py | 8 +++++---
 apps/settings/api/public.py        | 2 ++
 2 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py
index a4be80f5b..d5088ea48 100644
--- a/apps/jumpserver/settings/custom.py
+++ b/apps/jumpserver/settings/custom.py
@@ -37,7 +37,7 @@ SECURITY_LOGIN_LIMIT_COUNT = CONFIG.SECURITY_LOGIN_LIMIT_COUNT
 SECURITY_LOGIN_IP_BLACK_LIST = CONFIG.SECURITY_LOGIN_IP_BLACK_LIST
 SECURITY_LOGIN_LIMIT_TIME = CONFIG.SECURITY_LOGIN_LIMIT_TIME  # Unit: minute
 SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME  # Unit: minute
-SECURITY_PASSWORD_EXPIRATION_TIME = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day
+SECURITY_PASSWORD_EXPIRATION_TIME = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME  # Unit: day
 SECURITY_PASSWORD_MIN_LENGTH = CONFIG.SECURITY_PASSWORD_MIN_LENGTH  # Unit: bit
 SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH = CONFIG.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH  # Unit: bit
 OLD_PASSWORD_HISTORY_LIMIT_COUNT = CONFIG.OLD_PASSWORD_HISTORY_LIMIT_COUNT
@@ -119,7 +119,6 @@ REFERER_CHECK_ENABLED = CONFIG.REFERER_CHECK_ENABLED
 CONNECTION_TOKEN_ENABLED = CONFIG.CONNECTION_TOKEN_ENABLED
 FORGOT_PASSWORD_URL = CONFIG.FORGOT_PASSWORD_URL
 
-
 # 自定义默认组织名
 GLOBAL_ORG_DISPLAY_NAME = CONFIG.GLOBAL_ORG_DISPLAY_NAME
 HEALTH_CHECK_TOKEN = CONFIG.HEALTH_CHECK_TOKEN
@@ -136,7 +135,6 @@ CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = CONFIG.CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS
 
 XRDP_ENABLED = CONFIG.XRDP_ENABLED
 
-
 # SMS enabled
 SMS_ENABLED = CONFIG.SMS_ENABLED
 SMS_BACKEND = CONFIG.SMS_BACKEND
@@ -156,3 +154,7 @@ TENCENT_SMS_SIGN_AND_TEMPLATES = CONFIG.TENCENT_SMS_SIGN_AND_TEMPLATES
 # 公告
 ANNOUNCEMENT_ENABLED = CONFIG.ANNOUNCEMENT_ENABLED
 ANNOUNCEMENT = CONFIG.ANNOUNCEMENT
+
+# help
+HELP_DOCUMENT_URL = CONFIG.HELP_DOCUMENT_URL
+HELP_SUPPORT_URL = CONFIG.HELP_SUPPORT_URL
diff --git a/apps/settings/api/public.py b/apps/settings/api/public.py
index ad9c1fbe6..48542a1ea 100644
--- a/apps/settings/api/public.py
+++ b/apps/settings/api/public.py
@@ -59,6 +59,8 @@ class PublicSettingApi(generics.RetrieveAPIView):
                 "XRDP_ENABLED": settings.XRDP_ENABLED,
                 "ANNOUNCEMENT_ENABLED": settings.ANNOUNCEMENT_ENABLED,
                 "ANNOUNCEMENT": settings.ANNOUNCEMENT,
+                "HELP_DOCUMENT_URL": settings.HELP_DOCUMENT_URL,
+                "HELP_SUPPORT_URL": settings.HELP_SUPPORT_URL,
             }
         }
         return instance

From 8af88cd2c6c93433ce72c4818202d6342af19e00 Mon Sep 17 00:00:00 2001
From: fit2bot <68588906+fit2bot@users.noreply.github.com>
Date: Wed, 17 Nov 2021 16:43:04 +0800
Subject: [PATCH 22/25] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dotp=20verify=20m?=
 =?UTF-8?q?sg=E5=BC=95=E8=B5=B7=E7=9A=84500=20(#7210)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: ibuler <ibuler@qq.com>
---
 apps/authentication/api/mfa.py       |  2 +-
 apps/locale/zh/LC_MESSAGES/django.po | 87 ++++++++++++++--------------
 2 files changed, 44 insertions(+), 45 deletions(-)

diff --git a/apps/authentication/api/mfa.py b/apps/authentication/api/mfa.py
index 1387de8fe..52795c630 100644
--- a/apps/authentication/api/mfa.py
+++ b/apps/authentication/api/mfa.py
@@ -100,7 +100,7 @@ class UserOtpVerifyApi(CreateAPIView):
             request.session["MFA_VERIFY_TIME"] = int(time.time())
             return Response({"ok": "1"})
         else:
-            return Response({"error": _("Code is invalid") + ", " + error}, status=400)
+            return Response({"error": _("Code is invalid, {}").format(error)}, status=400)
 
     def get_permissions(self):
         if self.request.method.lower() == 'get' \
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index 14e729bb1..74a9084aa 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: JumpServer 0.3.3\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-11-15 17:52+0800\n"
+"POT-Creation-Date: 2021-11-17 16:39+0800\n"
 "PO-Revision-Date: 2021-05-20 10:54+0800\n"
 "Last-Translator: ibuler <ibuler@qq.com>\n"
 "Language-Team: JumpServer team<ibuler@qq.com>\n"
@@ -25,7 +25,7 @@ msgstr ""
 #: orgs/models.py:24 perms/models/base.py:44 settings/models.py:29
 #: settings/serializers/sms.py:6 terminal/models/storage.py:23
 #: terminal/models/task.py:16 terminal/models/terminal.py:100
-#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:541
+#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:547
 #: users/templates/users/_select_user_modal.html:13
 #: users/templates/users/user_asset_permission.html:37
 #: users/templates/users/user_asset_permission.html:154
@@ -60,7 +60,7 @@ msgstr "激活中"
 #: orgs/models.py:27 perms/models/base.py:53 settings/models.py:34
 #: terminal/models/storage.py:26 terminal/models/terminal.py:114
 #: tickets/models/ticket.py:71 users/models/group.py:16
-#: users/models/user.py:574 xpack/plugins/change_auth_plan/models/base.py:41
+#: users/models/user.py:580 xpack/plugins/change_auth_plan/models/base.py:41
 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:113
 #: xpack/plugins/gathered_user/models.py:26
 msgid "Comment"
@@ -87,7 +87,7 @@ msgstr "登录复核"
 #: terminal/backends/command/serializers.py:12 terminal/models/session.py:38
 #: terminal/notifications.py:90 terminal/notifications.py:138
 #: tickets/models/comment.py:17 users/const.py:14 users/models/user.py:169
-#: users/models/user.py:745 users/models/user.py:771
+#: users/models/user.py:751 users/models/user.py:777
 #: users/serializers/group.py:19
 #: users/templates/users/user_asset_permission.html:38
 #: users/templates/users/user_asset_permission.html:64
@@ -162,7 +162,7 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. "
 #: assets/models/base.py:176 assets/models/gathered_user.py:15
 #: audits/models.py:105 authentication/forms.py:15 authentication/forms.py:17
 #: authentication/templates/authentication/_msg_different_city.html:9
-#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:539
+#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:545
 #: users/templates/users/_msg_user_created.html:12
 #: users/templates/users/_select_user_modal.html:14
 #: xpack/plugins/change_auth_plan/models/asset.py:35
@@ -554,7 +554,7 @@ msgstr "标签管理"
 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:26
 #: assets/models/cmd_filter.py:67 assets/models/group.py:21
 #: common/db/models.py:70 common/mixins/models.py:49 orgs/models.py:25
-#: orgs/models.py:437 perms/models/base.py:51 users/models/user.py:582
+#: orgs/models.py:437 perms/models/base.py:51 users/models/user.py:588
 #: users/serializers/group.py:33
 #: xpack/plugins/change_auth_plan/models/base.py:45
 #: xpack/plugins/cloud/models.py:119 xpack/plugins/gathered_user/models.py:30
@@ -567,7 +567,7 @@ msgstr "创建者"
 #: assets/models/label.py:25 common/db/models.py:72 common/mixins/models.py:50
 #: ops/models/adhoc.py:38 ops/models/command.py:29 orgs/models.py:26
 #: orgs/models.py:435 perms/models/base.py:52 users/models/group.py:18
-#: users/models/user.py:772 xpack/plugins/cloud/models.py:122
+#: users/models/user.py:778 xpack/plugins/cloud/models.py:122
 msgid "Date created"
 msgstr "创建日期"
 
@@ -622,7 +622,7 @@ msgstr "带宽"
 msgid "Contact"
 msgstr "联系人"
 
-#: assets/models/cluster.py:22 users/models/user.py:560
+#: assets/models/cluster.py:22 users/models/user.py:566
 msgid "Phone"
 msgstr "手机"
 
@@ -648,7 +648,7 @@ msgid "Default"
 msgstr "默认"
 
 #: assets/models/cluster.py:36 assets/models/label.py:14
-#: users/models/user.py:757
+#: users/models/user.py:763
 msgid "System"
 msgstr "系统"
 
@@ -1223,7 +1223,7 @@ msgstr "用户代理"
 
 #: audits/models.py:110
 #: authentication/templates/authentication/_mfa_confirm_modal.html:14
-#: users/forms/profile.py:64 users/models/user.py:563
+#: users/forms/profile.py:64 users/models/user.py:569
 #: users/serializers/profile.py:102
 msgid "MFA"
 msgstr "MFA"
@@ -1302,12 +1302,12 @@ msgid "Auth Token"
 msgstr "认证令牌"
 
 #: audits/signals_handler.py:68 authentication/views/login.py:170
-#: notifications/backends/__init__.py:11 users/models/user.py:596
+#: notifications/backends/__init__.py:11 users/models/user.py:602
 msgid "WeCom"
 msgstr "企业微信"
 
 #: audits/signals_handler.py:69 authentication/views/login.py:176
-#: notifications/backends/__init__.py:12 users/models/user.py:597
+#: notifications/backends/__init__.py:12 users/models/user.py:603
 msgid "DingTalk"
 msgstr "钉钉"
 
@@ -1494,13 +1494,13 @@ msgstr "{ApplicationPermission} 添加 {SystemUser}"
 msgid "{ApplicationPermission} REMOVE {SystemUser}"
 msgstr "{ApplicationPermission} 移除 {SystemUser}"
 
-#: authentication/api/connection_token.py:248
+#: authentication/api/connection_token.py:259
 msgid "Invalid token"
 msgstr "无效的令牌"
 
 #: authentication/api/mfa.py:103
-msgid "Code is invalid"
-msgstr "验证码无效"
+msgid "Code is invalid, {}"
+msgstr "验证码无效: {}"
 
 #: authentication/backends/api.py:67
 msgid "Invalid signature header. No credentials provided."
@@ -1661,15 +1661,15 @@ msgstr "该 时间段 不被允许登录"
 msgid "SSO auth closed"
 msgstr "SSO 认证关闭了"
 
-#: authentication/errors.py:293 authentication/mixins.py:359
+#: authentication/errors.py:293 authentication/mixins.py:360
 msgid "Your password is too simple, please change it for security"
 msgstr "你的密码过于简单,为了安全,请修改"
 
-#: authentication/errors.py:302 authentication/mixins.py:366
+#: authentication/errors.py:302 authentication/mixins.py:367
 msgid "You should to change your password before login"
 msgstr "登录完成前,请先修改密码"
 
-#: authentication/errors.py:311 authentication/mixins.py:373
+#: authentication/errors.py:311 authentication/mixins.py:374
 msgid "Your password has expired, please reset before logging in"
 msgstr "您的密码已过期,先修改再登录"
 
@@ -1677,19 +1677,15 @@ msgstr "您的密码已过期,先修改再登录"
 msgid "Your password is invalid"
 msgstr "您的密码无效"
 
-#: authentication/errors.py:351
-msgid "No upload or download permission"
-msgstr "没有上传下载权限"
-
-#: authentication/errors.py:356
+#: authentication/errors.py:350
 msgid "Please enter MFA code"
 msgstr "请输入 MFA 验证码"
 
-#: authentication/errors.py:361
+#: authentication/errors.py:355
 msgid "Please enter SMS code"
 msgstr "请输入短信验证码"
 
-#: authentication/errors.py:366 users/exceptions.py:15
+#: authentication/errors.py:360 users/exceptions.py:15
 msgid "Phone not set"
 msgstr "手机号没有设置"
 
@@ -1765,11 +1761,11 @@ msgstr "设置手机号码启用"
 msgid "Clear phone number to disable"
 msgstr "清空手机号码禁用"
 
-#: authentication/mixins.py:316 authentication/mixins.py:318
+#: authentication/mixins.py:315
 msgid "The MFA type ({}) is not enabled"
 msgstr "该 MFA ({}) 方式没有启用"
 
-#: authentication/mixins.py:349
+#: authentication/mixins.py:350
 msgid "Please change your password"
 msgstr "请修改密码"
 
@@ -2093,7 +2089,7 @@ msgid "Please enable cookies and try again."
 msgstr "设置你的浏览器支持cookie"
 
 #: authentication/views/login.py:182 notifications/backends/__init__.py:14
-#: users/models/user.py:598
+#: users/models/user.py:604
 msgid "FeiShu"
 msgstr "飞书"
 
@@ -2352,7 +2348,7 @@ msgstr ""
 "div>"
 
 #: notifications/backends/__init__.py:10 users/forms/profile.py:101
-#: users/models/user.py:543
+#: users/models/user.py:549
 msgid "Email"
 msgstr "邮件"
 
@@ -2567,7 +2563,7 @@ msgstr "组织审计员"
 msgid "GLOBAL"
 msgstr "全局组织"
 
-#: orgs/models.py:434 users/models/user.py:551 users/serializers/user.py:37
+#: orgs/models.py:434 users/models/user.py:557 users/serializers/user.py:37
 #: users/templates/users/_select_user_modal.html:15
 msgid "Role"
 msgstr "角色"
@@ -2628,7 +2624,7 @@ msgid "Favorite"
 msgstr "收藏夹"
 
 #: perms/models/base.py:47 templates/_nav.html:21 users/models/group.py:31
-#: users/models/user.py:547 users/templates/users/_select_user_modal.html:16
+#: users/models/user.py:553 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
@@ -2639,7 +2635,7 @@ msgstr "用户组"
 #: perms/models/base.py:50
 #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:60
 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:50
-#: users/models/user.py:579
+#: users/models/user.py:585
 msgid "Date expired"
 msgstr "失效日期"
 
@@ -3827,11 +3823,11 @@ msgstr ""
 msgid "Send verification code"
 msgstr "发送验证码"
 
-#: templates/_mfa_login_field.html:107
+#: templates/_mfa_login_field.html:105
 msgid "Wait: "
 msgstr "等待:"
 
-#: templates/_mfa_login_field.html:117
+#: templates/_mfa_login_field.html:115
 msgid "The verification code has been sent"
 msgstr "验证码已发送"
 
@@ -5022,7 +5018,7 @@ msgstr "不能和原来的密钥相同"
 msgid "Not a valid ssh public key"
 msgstr "SSH密钥不合法"
 
-#: users/forms/profile.py:160 users/models/user.py:571
+#: users/forms/profile.py:160 users/models/user.py:577
 #: users/templates/users/user_password_update.html:48
 msgid "Public key"
 msgstr "SSH公钥"
@@ -5031,39 +5027,39 @@ msgstr "SSH公钥"
 msgid "Force enable"
 msgstr "强制启用"
 
-#: users/models/user.py:520
+#: users/models/user.py:526
 msgid "Local"
 msgstr "数据库"
 
-#: users/models/user.py:554
+#: users/models/user.py:560
 msgid "Avatar"
 msgstr "头像"
 
-#: users/models/user.py:557
+#: users/models/user.py:563
 msgid "Wechat"
 msgstr "微信"
 
-#: users/models/user.py:568
+#: users/models/user.py:574
 msgid "Private key"
 msgstr "ssh私钥"
 
-#: users/models/user.py:587
+#: users/models/user.py:593
 msgid "Source"
 msgstr "来源"
 
-#: users/models/user.py:591
+#: users/models/user.py:597
 msgid "Date password last updated"
 msgstr "最后更新密码日期"
 
-#: users/models/user.py:594
+#: users/models/user.py:600
 msgid "Need update password"
 msgstr "需要更新密码"
 
-#: users/models/user.py:753
+#: users/models/user.py:759
 msgid "Administrator"
 msgstr "管理员"
 
-#: users/models/user.py:756
+#: users/models/user.py:762
 msgid "Administrator is the super user of system"
 msgstr "Administrator是初始的超级管理员"
 
@@ -6127,6 +6123,9 @@ msgstr "旗舰版"
 msgid "Community edition"
 msgstr "社区版"
 
+#~ msgid "No upload or download permission"
+#~ msgstr "没有上传下载权限"
+
 #~ msgid "OTP not set, please set it first"
 #~ msgstr "OTP认证没有设置,请先完成设置"
 

From 0ed0f69be0b63c8f1e5b69bef176ccb552fec6e1 Mon Sep 17 00:00:00 2001
From: fit2bot <68588906+fit2bot@users.noreply.github.com>
Date: Wed, 17 Nov 2021 19:28:58 +0800
Subject: [PATCH 23/25] =?UTF-8?q?fix:=20=E7=99=BB=E5=BD=95=E6=97=A5?=
 =?UTF-8?q?=E5=BF=97=E5=AD=97=E6=AE=B5=E7=BF=BB=E8=AF=91=20(#7211)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: xinwen <coderWen@126.com>
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
---
 apps/audits/serializers.py           | 2 +-
 apps/locale/zh/LC_MESSAGES/django.po | 6 +++++-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py
index 32e8ad20d..2c7911f14 100644
--- a/apps/audits/serializers.py
+++ b/apps/audits/serializers.py
@@ -42,7 +42,7 @@ class UserLoginLogSerializer(serializers.ModelSerializer):
         fields = fields_small
         extra_kwargs = {
             "user_agent": {'label': _('User agent')},
-            "reason_display": {'label': _('Reason')}
+            "reason_display": {'label': _('Reason display')}
         }
 
 
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index 74a9084aa..d72d23fac 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -1228,7 +1228,7 @@ msgstr "用户代理"
 msgid "MFA"
 msgstr "MFA"
 
-#: audits/models.py:111 audits/serializers.py:45 terminal/models/sharing.py:88
+#: audits/models.py:111 terminal/models/sharing.py:88
 #: xpack/plugins/change_auth_plan/models/base.py:187
 #: xpack/plugins/cloud/models.py:176
 msgid "Reason"
@@ -1259,6 +1259,10 @@ msgstr "状态名称"
 msgid "MFA display"
 msgstr "MFA名称"
 
+#: audits/serializers.py:45
+msgid "Reason display"
+msgstr "原因显示"
+
 #: audits/serializers.py:76 audits/serializers.py:91 ops/models/adhoc.py:248
 #: terminal/serializers/session.py:35
 msgid "Is success"

From 096b2cf8b845b04775569f02c395b53ade68c0eb Mon Sep 17 00:00:00 2001
From: Michael Bai <baijiangjie@gmail.com>
Date: Wed, 17 Nov 2021 19:31:15 +0800
Subject: [PATCH 24/25] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=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 | 4 ++--
 apps/locale/zh/LC_MESSAGES/django.po | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo
index 1fcdb6691..2b95a8070 100644
--- a/apps/locale/zh/LC_MESSAGES/django.mo
+++ b/apps/locale/zh/LC_MESSAGES/django.mo
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:0cbfc40871a5626f0798771004e92949986e1b5f845c03783590baf38aa43c2e
-size 93462
+oid sha256:4fea2cdf5a5477757cb95ff36016ed754fd65f839c12adbac9247ebdcca138ef
+size 93440
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index d72d23fac..59c2bf499 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -1261,7 +1261,7 @@ msgstr "MFA名称"
 
 #: audits/serializers.py:45
 msgid "Reason display"
-msgstr "原因显示"
+msgstr "原因描述"
 
 #: audits/serializers.py:76 audits/serializers.py:91 ops/models/adhoc.py:248
 #: terminal/serializers/session.py:35

From dac45f234a60e34e29ad6f4795ad804277bebd0f Mon Sep 17 00:00:00 2001
From: Michael Bai <baijiangjie@gmail.com>
Date: Wed, 17 Nov 2021 18:54:32 +0800
Subject: [PATCH 25/25] =?UTF-8?q?fix:=20=E7=BB=88=E7=AB=AF=E5=88=97?=
 =?UTF-8?q?=E8=A1=A8=E6=8C=87=E6=A0=87=E6=95=B0=E5=80=BC=E4=BF=9D=E7=95=99?=
 =?UTF-8?q?=E4=B8=80=E4=BD=8D=E5=B0=8F=E6=95=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/terminal/models/status.py | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/apps/terminal/models/status.py b/apps/terminal/models/status.py
index cece6b5ce..bc195d857 100644
--- a/apps/terminal/models/status.py
+++ b/apps/terminal/models/status.py
@@ -55,8 +55,18 @@ class Status(models.Model):
         stat = cls(**data)
         stat.terminal = terminal
         stat.is_alive = terminal.is_alive
+        stat.keep_one_decimal_place()
         return stat
 
+    def keep_one_decimal_place(self):
+        keys = ['cpu_load', 'memory_used', 'disk_used']
+        for key in keys:
+            value = getattr(self, key, 0)
+            if not isinstance(value, (int, float)):
+                continue
+            value = '%.1f' % value
+            setattr(self, key, float(value))
+
     def save(self, force_insert=False, force_update=False, using=None,
              update_fields=None):
         self.terminal.set_alive(ttl=120)