From 189bc9d74a75b3e04c78dd701238d3218b591738 Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 17 May 2021 14:46:40 +0800 Subject: [PATCH 01/15] =?UTF-8?q?perf:=20=E6=B7=BB=E5=8A=A0lion=E7=BB=88?= =?UTF-8?q?=E7=AB=AF=E7=B1=BB=E5=9E=8B;=20=E4=BF=AE=E6=94=B9=E5=8A=A0?= =?UTF-8?q?=E5=85=A5=E4=BC=9A=E8=AF=9D=E6=A0=A1=E9=AA=8C=E9=80=BB=E8=BE=91?= =?UTF-8?q?(vnc/rdp)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/settings/base.py | 2 +- apps/terminal/api/session.py | 8 ++------ apps/terminal/api/status.py | 5 ----- apps/terminal/const.py | 1 + apps/terminal/models/session.py | 4 +++- 5 files changed, 7 insertions(+), 13 deletions(-) diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index d4a8ce27a..4a2e59062 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -16,7 +16,7 @@ PROJECT_DIR = const.PROJECT_DIR # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = CONFIG.SECRET_KEY -# SECURITY WARNING: keep the token secret, remove it if all coco, guacamole ok +# SECURITY WARNING: keep the token secret, remove it if all koko, lion ok BOOTSTRAP_TOKEN = CONFIG.BOOTSTRAP_TOKEN # SECURITY WARNING: don't run with debug turned on in production! diff --git a/apps/terminal/api/session.py b/apps/terminal/api/session.py index 2a749aac5..359ecffc2 100644 --- a/apps/terminal/api/session.py +++ b/apps/terminal/api/session.py @@ -90,7 +90,7 @@ class SessionViewSet(OrgBulkModelViewSet): def filter_queryset(self, queryset): queryset = super().filter_queryset(queryset) - # 解决guacamole更新session时并发导致幽灵会话的问题 + # 解决guacamole更新session时并发导致幽灵会话的问题,暂不处理 if self.request.method in ('PATCH',): queryset = queryset.select_for_update() return queryset @@ -98,11 +98,6 @@ class SessionViewSet(OrgBulkModelViewSet): def perform_create(self, serializer): if hasattr(self.request.user, 'terminal'): serializer.validated_data["terminal"] = self.request.user.terminal - sid = serializer.validated_data["system_user"] - # guacamole提交的是id - if is_uuid(sid): - _system_user = get_object_or_404(SystemUser, id=sid) - serializer.validated_data["system_user"] = _system_user.name return super().perform_create(serializer) def get_permissions(self): @@ -140,6 +135,7 @@ class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet): def get_replay_data(session, url): tp = 'json' if session.protocol in ('rdp', 'vnc'): + # 需要考虑录像播放和离线播放器的约定,暂时不处理 tp = 'guacamole' download_url = reverse('api-terminal:session-replay-download', kwargs={'pk': session.id}) diff --git a/apps/terminal/api/status.py b/apps/terminal/api/status.py index b39e13ba7..fdf3ef1f1 100644 --- a/apps/terminal/api/status.py +++ b/apps/terminal/api/status.py @@ -39,11 +39,6 @@ class StatusViewSet(viewsets.ModelViewSet): def handle_sessions(self): session_ids = self.request.data.get('sessions', []) - # guacamole 上报的 session 是字符串 - # "[53cd3e47-210f-41d8-b3c6-a184f3, 53cd3e47-210f-41d8-b3c6-a184f4]" - if isinstance(session_ids, str): - session_ids = session_ids[1:-1].split(',') - session_ids = [sid.strip() for sid in session_ids if sid.strip()] Session.set_sessions_active(session_ids) def get_queryset(self): diff --git a/apps/terminal/const.py b/apps/terminal/const.py index 830913e28..ff638325d 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -43,6 +43,7 @@ class TerminalTypeChoices(TextChoices): guacamole = 'guacamole', 'Guacamole' omnidb = 'omnidb', 'OmniDB' xrdp = 'xrdp', 'Xrdp' + lion = 'lion', 'Lion' @classmethod def types(cls): diff --git a/apps/terminal/models/session.py b/apps/terminal/models/session.py index 6d85759af..86843433e 100644 --- a/apps/terminal/models/session.py +++ b/apps/terminal/models/session.py @@ -109,7 +109,9 @@ class Session(OrgModelMixin): _PROTOCOL = self.PROTOCOL if self.is_finished: return False - if self.protocol in [_PROTOCOL.SSH, _PROTOCOL.TELNET, _PROTOCOL.K8S]: + if self.protocol in [ + _PROTOCOL.SSH, _PROTOCOL.VNC, _PROTOCOL.RDP, _PROTOCOL.TELNET, _PROTOCOL.K8S + ]: return True else: return False From 8b951ce12c7b2d95cfa704ea8ae6f17e92aba647 Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 17 May 2021 14:49:03 +0800 Subject: [PATCH 02/15] =?UTF-8?q?perf:=20=E6=B7=BB=E5=8A=A0=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E6=96=87=E4=BB=B6(lion)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0035_auto_20210517_1448.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 apps/terminal/migrations/0035_auto_20210517_1448.py diff --git a/apps/terminal/migrations/0035_auto_20210517_1448.py b/apps/terminal/migrations/0035_auto_20210517_1448.py new file mode 100644 index 000000000..2c8592700 --- /dev/null +++ b/apps/terminal/migrations/0035_auto_20210517_1448.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.6 on 2021-05-17 06:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0034_auto_20210406_1434'), + ] + + operations = [ + migrations.AlterField( + model_name='terminal', + name='type', + field=models.CharField(choices=[('koko', 'KoKo'), ('guacamole', 'Guacamole'), ('omnidb', 'OmniDB'), ('xrdp', 'Xrdp'), ('lion', 'Lion')], default='koko', max_length=64, verbose_name='type'), + ), + ] From 726fd94f652b099dfcbb2d6126a19d4be5d9356b Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 17 May 2021 14:35:04 +0800 Subject: [PATCH 03/15] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20xslx=20?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E6=95=B0=E5=AD=97=E7=B1=BB=E5=9E=8B=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/common/drf/parsers/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/common/drf/parsers/base.py b/apps/common/drf/parsers/base.py index f228960f0..acffcfef8 100644 --- a/apps/common/drf/parsers/base.py +++ b/apps/common/drf/parsers/base.py @@ -57,6 +57,8 @@ class BaseFileParser(BaseParser): @staticmethod def _replace_chinese_quote(s): + if not isinstance(s, str): + return s trans_table = str.maketrans({ '“': '"', '”': '"', From 70055b8af276a7ca5367370fdbe7e90f1f1b57e2 Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 17 May 2021 16:10:19 +0800 Subject: [PATCH 04/15] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dremoteapp?= =?UTF-8?q?=E8=8E=B7=E5=8F=96asset=5Finfo=E5=A4=B1=E8=B4=A5=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../serializers/attrs/application_category/remote_app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/applications/serializers/attrs/application_category/remote_app.py b/apps/applications/serializers/attrs/application_category/remote_app.py index 9fa691976..3cda4ac1e 100644 --- a/apps/applications/serializers/attrs/application_category/remote_app.py +++ b/apps/applications/serializers/attrs/application_category/remote_app.py @@ -39,14 +39,14 @@ class RemoteAppSerializer(serializers.Serializer): @staticmethod def get_asset_info(obj): asset_id = obj.get('asset') - if not asset_id or is_uuid(asset_id): + if not asset_id or not is_uuid(asset_id): return {} try: - asset = Asset.objects.filter(id=str(asset_id)).values_list('id', 'hostname') + asset = Asset.objects.get(id=str(asset_id)) except ObjectDoesNotExist as e: logger.error(e) return {} if not asset: return {} - asset_info = {'id': str(asset[0]), 'hostname': asset[1]} + asset_info = {'id': str(asset.id), 'hostname': asset.hostname} return asset_info From 374376102414e37154db606ac666a4aa59cbbd38 Mon Sep 17 00:00:00 2001 From: xinwen Date: Mon, 17 May 2021 14:20:51 +0800 Subject: [PATCH 05/15] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=BB=91?= =?UTF-8?q?=E5=AE=9A=E4=BC=81=E4=B8=9A=E5=BE=AE=E4=BF=A1&=E9=92=89?= =?UTF-8?q?=E9=92=89=E7=9A=84=E4=B8=80=E4=BA=9B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/views/dingtalk.py | 27 ++++- apps/authentication/views/wecom.py | 27 ++++- apps/common/message/backends/exceptions.py | 5 - apps/common/message/backends/mixin.py | 4 +- apps/common/message/backends/utils.py | 2 +- .../common/message/backends/wecom/__init__.py | 5 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 74959 -> 74979 bytes apps/locale/zh/LC_MESSAGES/django.po | 100 ++++++++++-------- apps/settings/api/dingtalk.py | 33 +++--- apps/settings/api/wecom.py | 33 +++--- apps/settings/serializers/settings.py | 12 +-- 11 files changed, 150 insertions(+), 98 deletions(-) diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py index c2e139467..24861b979 100644 --- a/apps/authentication/views/dingtalk.py +++ b/apps/authentication/views/dingtalk.py @@ -8,7 +8,9 @@ from django.views.generic import TemplateView from django.views import View from django.conf import settings from django.http.request import HttpRequest +from django.db.utils import IntegrityError from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.exceptions import APIException from users.views import UserVerifyPasswordView from users.utils import is_auth_password_time_valid @@ -29,6 +31,20 @@ DINGTALK_STATE_SESSION_KEY = '_dingtalk_state' class DingTalkQRMixin(PermissionsMixin, View): + def dispatch(self, request, *args, **kwargs): + try: + return super().dispatch(request, *args, **kwargs) + except APIException as e: + try: + msg = e.detail['errmsg'] + except Exception: + msg = _('DingTalk Error, Please contact your system administrator') + return self.get_failed_reponse( + '/', + _('DingTalk Error'), + msg + ) + def verify_state(self): state = self.request.GET.get('state') session_state = self.request.session.get(DINGTALK_STATE_SESSION_KEY) @@ -130,8 +146,15 @@ class DingTalkQRBindCallbackView(DingTalkQRMixin, View): response = self.get_failed_reponse(redirect_url, msg, msg) return response - user.dingtalk_id = userid - user.save() + try: + user.dingtalk_id = userid + user.save() + except IntegrityError as e: + if e.args[0] == 1062: + msg = _('The DingTalk is already bound to another user') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + raise e msg = _('Binding DingTalk successfully') response = self.get_success_reponse(redirect_url, msg, msg) diff --git a/apps/authentication/views/wecom.py b/apps/authentication/views/wecom.py index 097df8e95..981c12508 100644 --- a/apps/authentication/views/wecom.py +++ b/apps/authentication/views/wecom.py @@ -8,7 +8,9 @@ from django.views.generic import TemplateView from django.views import View from django.conf import settings from django.http.request import HttpRequest +from django.db.utils import IntegrityError from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.exceptions import APIException from users.views import UserVerifyPasswordView from users.utils import is_auth_password_time_valid @@ -29,6 +31,20 @@ WECOM_STATE_SESSION_KEY = '_wecom_state' class WeComQRMixin(PermissionsMixin, View): + def dispatch(self, request, *args, **kwargs): + try: + return super().dispatch(request, *args, **kwargs) + except APIException as e: + try: + msg = e.detail['errmsg'] + except Exception: + msg = _('WeCom Error, Please contact your system administrator') + return self.get_failed_reponse( + '/', + _('WeCom Error'), + msg + ) + def verify_state(self): state = self.request.GET.get('state') session_state = self.request.session.get(WECOM_STATE_SESSION_KEY) @@ -128,8 +144,15 @@ class WeComQRBindCallbackView(WeComQRMixin, View): response = self.get_failed_reponse(redirect_url, msg, msg) return response - user.wecom_id = wecom_userid - user.save() + try: + user.wecom_id = wecom_userid + user.save() + except IntegrityError as e: + if e.args[0] == 1062: + msg = _('The WeCom is already bound to another user') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + raise e msg = _('Binding WeCom successfully') response = self.get_success_reponse(redirect_url, msg, msg) diff --git a/apps/common/message/backends/exceptions.py b/apps/common/message/backends/exceptions.py index f72e8694d..e28a80811 100644 --- a/apps/common/message/backends/exceptions.py +++ b/apps/common/message/backends/exceptions.py @@ -21,8 +21,3 @@ class ResponseDataKeyError(APIException): class NetError(APIException): default_code = 'net_error' default_detail = _('Network error, please contact system administrator') - - -class AccessTokenError(APIException): - default_code = 'access_token_error' - default_detail = 'Access token error, check config' diff --git a/apps/common/message/backends/mixin.py b/apps/common/message/backends/mixin.py index 3beb60272..5652a1520 100644 --- a/apps/common/message/backends/mixin.py +++ b/apps/common/message/backends/mixin.py @@ -22,7 +22,7 @@ class RequestMixin: logger.error(f'Response 200 but errcode is not 0: ' f'errcode={errcode} ' f'errmsg={errmsg} ') - raise exce.ErrCodeNot0(detail=str(data.raw_data)) + raise exce.ErrCodeNot0(detail=data.raw_data) def check_http_is_200(self, response): if response.status_code != 200: @@ -31,7 +31,7 @@ class RequestMixin: f'status_code={response.status_code} ' f'url={response.url}' f'\ncontent={response.content}') - raise exce.HTTPNot200 + raise exce.HTTPNot200(detail=response.json()) class BaseRequest(RequestMixin): diff --git a/apps/common/message/backends/utils.py b/apps/common/message/backends/utils.py index 6c6f2b593..5a2f90355 100644 --- a/apps/common/message/backends/utils.py +++ b/apps/common/message/backends/utils.py @@ -39,7 +39,7 @@ class DictWrapper: except KeyError as e: msg = f'Response 200 but get field from json error: error={e} data={self.raw_data}' logger.error(msg) - raise exce.ResponseDataKeyError(detail=msg) + raise exce.ResponseDataKeyError(detail=self.raw_data) def __getattr__(self, item): return getattr(self.raw_data, item) diff --git a/apps/common/message/backends/wecom/__init__.py b/apps/common/message/backends/wecom/__init__.py index 257da47a0..dd3e34c8a 100644 --- a/apps/common/message/backends/wecom/__init__.py +++ b/apps/common/message/backends/wecom/__init__.py @@ -33,6 +33,9 @@ class ErrorCode: # https://open.work.weixin.qq.com/api/doc/90000/90139/90313#%E9%94%99%E8%AF%AF%E7%A0%81%EF%BC%9A81013 RECIPIENTS_INVALID = 81013 # UserID、部门ID、标签ID全部非法或无权限。 + # https: // open.work.weixin.qq.com / devtool / query?e = 82001 + RECIPIENTS_EMPTY = 82001 # 指定的成员/部门/标签全部为空 + # https://open.work.weixin.qq.com/api/doc/90000/90135/91437 INVALID_CODE = 40029 @@ -141,7 +144,7 @@ class WeCom(RequestMixin): data = self._requests.post(URL.SEND_MESSAGE, json=body, check_errcode_is_0=False) errcode = data['errcode'] - if errcode == ErrorCode.RECIPIENTS_INVALID: + if errcode in (ErrorCode.RECIPIENTS_INVALID, ErrorCode.RECIPIENTS_EMPTY): # 全部接收人无权限或不存在 return users self.check_errcode_is_0(data) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 304516e3cddf64bcee3e4a11330bb169eedc3e2e..159bcda3ef65fb12268270a1b138fa7fb060d68d 100644 GIT binary patch delta 22071 zcmZwP1$b3Q+wSp&1xc_32u^~#yL+L5}3dP;st$48F(xRmVhaxS--JujH z-~Zn8a8A#6oprs_-^?>UYptCfXx~%EV%|I!(|0{M=5&wibqvo-jX$UJysu+WdG%;lk48b1*ERCIG1TvD?0G}5 zQWMX+f`{=W^-G#k=XpM_TXWA#LdP5}JTHm{Wm|b(QcT?1^8zpn#>d5#= zW)4Nx?k&X>cnD+SHOz;9VJ=M4#`PtK!5E5RsBwy7T&#p@UlS8yee@+E)1Hi0KFAz}+R;?hfJ-qOZbI$& z3UP#ejOdL~L+ya8(C9XfOV>M)oY1Up(cEf+ELJVu3s3cz5r^1FD>30wV^(!_M=g^WIk$~t*G{gePn{j{E6E6W7LXc zbaD481hvvssMj+uCcx^bi5sI9-V@bsB5HzVsPVR<#yerks`n486h`PrYF%DkC_;?2c@fBu7ubX?;GGQ*g|9Q#iUN=RZ zM0-qx5vZqqBx<1XsQS66fmWdgK8fmo8Fl10k&hGaHLCyLsFV7H>YuE;D`&tAdjE@) zDMUq6EP&InI37jqJVp<9GHFp0Wx#}(3-e+r)I?n{9!8)RJOp)u6Hu?+GF1PysFU7? z@%8@iBBO`n7;2y!<~>x$rWxCg4;095~RR$hQQkqxNV?KmdITc`#9 z6T$gwpqRbgJqs-N$@RRr~Q?|l-~Yn2PN@?xj~E2BEruyPaB5w=0K>x%m19Eb&Q0qP{qpgy23 zquRelo#ZE{&r8wI-ID^Sqp6B5u@0ud?WmodMh*Bo>V$5fj`%rh;7?}U{`};l9D?ds z5VfIFs1vM;$*>8=)cfCyjCRlu)nTNSC!!{phnjE=>I8P87J3X*;yEinMgK#E8ZYJm zH(p}Y38qAilODB@{1~A3zcLvOTphDwW6XggFeh$8y~o#43%rRb@Db{14jAYbk{UHp z5!3?8nAK4I8lom{iCSoH^eHohjCM2zbq}ZGm$)6(G5R34fI!qONQ64lP%{InT^?+J zMNua=8B^mzERVab{0YlaPCc0OSH~uU-Rsj9vr+DhI;t6{30I(Aw{@te_8{u1K8wlm zHfrbpqQ=v)%7m!5DmiMr5~#Q0D@=t=ePl9|>5u7fG3LVqs9W$7b!6#=xQ+!-3#g15 zus-V6wKIF8CK!QQ&@5E_BGiI6pyCHm8$0GBqm@RXDlVfcZeSX`hhZ2n)RnWK+E+r| z;~G|OgzDECHBKih_q6){s9QE13*tl!N8it6G|(N?j$Wd69AlWfXQ5b+^0&AGH{mX9 zFx<6I^1Zt?8ByaDLp@8CQ49JSwV(#5lW2|_zbkT*K5sY~b(n^FXcnS6tV2EJTT$=p zDbxb*pziq_)Q%F4a8Gk8RR2s^1Ph=h?1V9~zv)A@`yNy4{hwkL8&Ge<7Ax;XO>hXc zvr`y`S5dd%BWi+xk*+=|>K>=ZSXdCX;Nlkl5;aa^jE)^7^Lw4iXh&U91CK%tG#Rzh zrI;Q!ndh(+<+qp)3;p1Zv@Qlv?tp2r8?M2LsAsA8D7zJ?1vW=tY%*;v&;|2Q?u9jR z8Ro@Ts9TU}v^$ZK7>9Bd)CqizT4;0B``!ih7K}x;TVSpI8hKiD#htEl2I-DCz?x3bpe`R{sgrKK(d%@AIMBmqy)! z%0A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 delta 22092 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\n" "Language-Team: JumpServer team\n" @@ -1547,44 +1547,48 @@ msgstr "返回" msgid "Copy success" msgstr "复制成功" -#: authentication/views/dingtalk.py:40 authentication/views/wecom.py:40 +#: authentication/views/dingtalk.py:41 authentication/views/wecom.py:41 msgid "You've been hacked" msgstr "你被攻击了" -#: authentication/views/dingtalk.py:76 +#: authentication/views/dingtalk.py:77 msgid "DingTalk is already bound" msgstr "钉钉已经绑定" -#: authentication/views/dingtalk.py:89 authentication/views/wecom.py:88 +#: authentication/views/dingtalk.py:90 authentication/views/wecom.py:89 msgid "Please verify your password first" msgstr "请检查密码" -#: authentication/views/dingtalk.py:113 authentication/views/wecom.py:112 +#: authentication/views/dingtalk.py:114 authentication/views/wecom.py:113 msgid "Invalid user_id" msgstr "无效的 user_id" -#: authentication/views/dingtalk.py:129 +#: authentication/views/dingtalk.py:130 msgid "DingTalk query user failed" msgstr "钉钉查询用户失败" -#: authentication/views/dingtalk.py:136 authentication/views/dingtalk.py:219 -#: authentication/views/dingtalk.py:220 +#: authentication/views/dingtalk.py:139 +msgid "The DingTalk is already bound to another user" +msgstr "该钉钉已经绑定其他用户" + +#: authentication/views/dingtalk.py:144 authentication/views/dingtalk.py:227 +#: authentication/views/dingtalk.py:228 msgid "Binding DingTalk successfully" msgstr "绑定 钉钉 成功" -#: authentication/views/dingtalk.py:188 +#: authentication/views/dingtalk.py:196 msgid "Failed to get user from DingTalk" msgstr "从钉钉获取用户失败" -#: authentication/views/dingtalk.py:194 +#: authentication/views/dingtalk.py:202 msgid "DingTalk is not bound" msgstr "钉钉没有绑定" -#: authentication/views/dingtalk.py:195 authentication/views/wecom.py:193 +#: authentication/views/dingtalk.py:203 authentication/views/wecom.py:201 msgid "Please login with a password and then bind the WeCom" msgstr "请使用密码登录,然后绑定企业微信" -#: authentication/views/dingtalk.py:237 authentication/views/dingtalk.py:238 +#: authentication/views/dingtalk.py:245 authentication/views/dingtalk.py:246 msgid "Binding DingTalk failed" msgstr "绑定钉钉失败" @@ -1620,28 +1624,32 @@ msgstr "退出登录成功" msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/wecom.py:75 +#: authentication/views/wecom.py:76 msgid "WeCom is already bound" msgstr "企业微信已经绑定" -#: authentication/views/wecom.py:127 +#: authentication/views/wecom.py:128 msgid "WeCom query user failed" msgstr "企业微信查询用户失败" -#: authentication/views/wecom.py:134 authentication/views/wecom.py:217 -#: authentication/views/wecom.py:218 +#: authentication/views/wecom.py:137 +msgid "The WeCom is already bound to another user" +msgstr "该企业微信已经绑定其他用户" + +#: authentication/views/wecom.py:142 authentication/views/wecom.py:225 +#: authentication/views/wecom.py:226 msgid "Binding WeCom successfully" msgstr "绑定 企业微信 成功" -#: authentication/views/wecom.py:186 +#: authentication/views/wecom.py:194 msgid "Failed to get user from WeCom" msgstr "从企业微信获取用户失败" -#: authentication/views/wecom.py:192 +#: authentication/views/wecom.py:200 msgid "WeCom is not bound" msgstr "没有绑定企业微信" -#: authentication/views/wecom.py:235 authentication/views/wecom.py:236 +#: authentication/views/wecom.py:243 authentication/views/wecom.py:244 msgid "Binding WeCom failed" msgstr "绑定企业微信失败" @@ -2123,7 +2131,11 @@ msgstr "邮件已经发送{}, 请检查" msgid "Welcome to the JumpServer open source Bastion Host" msgstr "欢迎使用JumpServer开源堡垒机" -#: settings/api/dingtalk.py:36 settings/api/wecom.py:36 +#: settings/api/dingtalk.py:29 +msgid "AppSecret is required" +msgstr "AppSecret 是必须的" + +#: settings/api/dingtalk.py:35 settings/api/wecom.py:35 msgid "OK" msgstr "" @@ -2135,6 +2147,10 @@ msgstr "获取 LDAP 用户为 None" msgid "Imported {} users successfully" msgstr "导入 {} 个用户成功" +#: settings/api/wecom.py:29 +msgid "Secret is required" +msgstr "Secret 是必须的" + #: settings/models.py:123 users/templates/users/reset_password.html:29 msgid "Setting" msgstr "设置" @@ -2462,34 +2478,10 @@ msgstr "邮件收件人" msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" -#: settings/serializers/settings.py:193 -msgid "Corporation ID(corpid)" -msgstr "企业 ID(CorpId)" - -#: settings/serializers/settings.py:194 -msgid "Agent ID(agentid)" -msgstr "应用 ID(AgentId)" - -#: settings/serializers/settings.py:195 -msgid "Secret(secret)" -msgstr "秘钥(secret)" - #: settings/serializers/settings.py:196 msgid "Enable WeCom Auth" msgstr "启用企业微信认证" -#: settings/serializers/settings.py:200 -msgid "AgentId" -msgstr "应用 ID(AgentId)" - -#: settings/serializers/settings.py:201 -msgid "AppKey" -msgstr "应用 Key(AppKey)" - -#: settings/serializers/settings.py:202 -msgid "AppSecret" -msgstr "应用密文(AppSecret)" - #: settings/serializers/settings.py:203 msgid "Enable DingTalk Auth" msgstr "启用钉钉认证" @@ -4953,7 +4945,7 @@ msgstr "实例个数" msgid "Periodic display" msgstr "定时执行" -#: xpack/plugins/cloud/utils.py:65 +#: xpack/plugins/cloud/utils.py:64 msgid "Account unavailable" msgstr "账户无效" @@ -5041,5 +5033,23 @@ msgstr "旗舰版" msgid "Community edition" msgstr "社区版" +#~ msgid "Corporation ID(corpid)" +#~ msgstr "企业 ID(CorpId)" + +#~ msgid "Agent ID(agentid)" +#~ msgstr "应用 ID(AgentId)" + +#~ msgid "Secret(secret)" +#~ msgstr "秘钥(secret)" + +#~ msgid "AgentId" +#~ msgstr "应用 ID(AgentId)" + +#~ msgid "AppKey" +#~ msgstr "应用 Key(AppKey)" + +#~ msgid "AppSecret" +#~ msgstr "应用密文(AppSecret)" + #~ msgid "No" #~ msgstr "无" diff --git a/apps/settings/api/dingtalk.py b/apps/settings/api/dingtalk.py index e560f8626..4e4a73bf7 100644 --- a/apps/settings/api/dingtalk.py +++ b/apps/settings/api/dingtalk.py @@ -1,11 +1,12 @@ -import requests - from rest_framework.views import Response from rest_framework.generics import GenericAPIView +from rest_framework.exceptions import APIException +from rest_framework import status from django.utils.translation import gettext_lazy as _ +from settings.models import Setting from common.permissions import IsSuperUser -from common.message.backends.dingtalk import URL +from common.message.backends.dingtalk import DingTalk from .. import serializers @@ -20,19 +21,17 @@ class DingTalkTestingAPI(GenericAPIView): dingtalk_appkey = serializer.validated_data['DINGTALK_APPKEY'] dingtalk_agentid = serializer.validated_data['DINGTALK_AGENTID'] - dingtalk_appsecret = serializer.validated_data['DINGTALK_APPSECRET'] + dingtalk_appsecret = serializer.validated_data.get('DINGTALK_APPSECRET') + + if not dingtalk_appsecret: + secret = Setting.objects.filter(name='DINGTALK_APPSECRET').first() + if not secret: + return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': _('AppSecret is required')}) + dingtalk_appsecret = secret.cleaned_value try: - params = {'appkey': dingtalk_appkey, 'appsecret': dingtalk_appsecret} - resp = requests.get(url=URL.GET_TOKEN, params=params) - if resp.status_code != 200: - return Response(status=400, data={'error': resp.json()}) - - data = resp.json() - errcode = data['errcode'] - if errcode != 0: - return Response(status=400, data={'error': data['errmsg']}) - - return Response(status=200, data={'msg': _('OK')}) - except Exception as e: - return Response(status=400, data={'error': str(e)}) + dingtalk = DingTalk(appid=dingtalk_appkey, appsecret=dingtalk_appsecret, agentid=dingtalk_agentid) + dingtalk.send_text(['test'], 'test') + return Response(status=status.HTTP_200_OK, data={'msg': _('OK')}) + except APIException as e: + return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': e.detail}) diff --git a/apps/settings/api/wecom.py b/apps/settings/api/wecom.py index 3087efd1f..5059b7647 100644 --- a/apps/settings/api/wecom.py +++ b/apps/settings/api/wecom.py @@ -1,11 +1,12 @@ -import requests - from rest_framework.views import Response from rest_framework.generics import GenericAPIView +from rest_framework.exceptions import APIException +from rest_framework import status from django.utils.translation import gettext_lazy as _ +from settings.models import Setting from common.permissions import IsSuperUser -from common.message.backends.wecom import URL +from common.message.backends.wecom import WeCom from .. import serializers @@ -20,19 +21,17 @@ class WeComTestingAPI(GenericAPIView): wecom_corpid = serializer.validated_data['WECOM_CORPID'] wecom_agentid = serializer.validated_data['WECOM_AGENTID'] - wecom_corpsecret = serializer.validated_data['WECOM_SECRET'] + wecom_corpsecret = serializer.validated_data.get('WECOM_SECRET') + + if not wecom_corpsecret: + secret = Setting.objects.filter(name='WECOM_SECRET').first() + if not secret: + return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': _('Secret is required')}) + wecom_corpsecret = secret.cleaned_value try: - params = {'corpid': wecom_corpid, 'corpsecret': wecom_corpsecret} - resp = requests.get(url=URL.GET_TOKEN, params=params) - if resp.status_code != 200: - return Response(status=400, data={'error': resp.json()}) - - data = resp.json() - errcode = data['errcode'] - if errcode != 0: - return Response(status=400, data={'error': data['errmsg']}) - - return Response(status=200, data={'msg': _('OK')}) - except Exception as e: - return Response(status=400, data={'error': str(e)}) + wecom = WeCom(corpid=wecom_corpid, corpsecret=wecom_corpsecret, agentid=wecom_agentid) + wecom.send_text(['test'], 'test') + return Response(status=status.HTTP_200_OK, data={'msg': _('OK')}) + except APIException as e: + return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': e.detail}) diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index 3aca30c10..28ddf1cd7 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -190,16 +190,16 @@ class SecuritySettingSerializer(serializers.Serializer): class WeComSettingSerializer(serializers.Serializer): - WECOM_CORPID = serializers.CharField(max_length=256, required=True, label=_('Corporation ID(corpid)')) - WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label=_("Agent ID(agentid)")) - WECOM_SECRET = serializers.CharField(max_length=256, required=True, label=_("Secret(secret)"), write_only=True) + WECOM_CORPID = serializers.CharField(max_length=256, required=True, label='corpid') + WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label='agentid') + WECOM_SECRET = serializers.CharField(max_length=256, required=False, label='secret', write_only=True) AUTH_WECOM = serializers.BooleanField(default=False, label=_('Enable WeCom Auth')) class DingTalkSettingSerializer(serializers.Serializer): - DINGTALK_AGENTID = serializers.CharField(max_length=256, required=True, label=_("AgentId")) - DINGTALK_APPKEY = serializers.CharField(max_length=256, required=True, label=_("AppKey")) - DINGTALK_APPSECRET = serializers.CharField(max_length=256, required=False, label=_("AppSecret"), write_only=True) + DINGTALK_AGENTID = serializers.CharField(max_length=256, required=True, label='AgentId') + DINGTALK_APPKEY = serializers.CharField(max_length=256, required=True, label='AppKey') + DINGTALK_APPSECRET = serializers.CharField(max_length=256, required=False, label='AppSecret', write_only=True) AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('Enable DingTalk Auth')) From ba3b5a40274a3f8fdf2c6f6963405a032d7dac58 Mon Sep 17 00:00:00 2001 From: xinwen Date: Mon, 17 May 2021 17:19:29 +0800 Subject: [PATCH 06/15] =?UTF-8?q?fix:=20=E5=88=9B=E5=BB=BA=20Es=20?= =?UTF-8?q?=E6=97=B6=E5=A4=B1=E8=B4=A5=E7=9A=84=E6=8F=90=E7=A4=BA=E7=BF=BB?= =?UTF-8?q?=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/locale/zh/LC_MESSAGES/django.mo | Bin 74979 -> 75055 bytes apps/locale/zh/LC_MESSAGES/django.po | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 159bcda3ef65fb12268270a1b138fa7fb060d68d..70c15aafc0ad5797565364e167bab1d948d1fb7a 100644 GIT binary patch delta 21148 zcmY-11(;P;`}grZ%n-xS0}L?44Ba&}4^o4)bcl2#Eg^jn5TxT!D$*bg0@6d5NJyth zr--D20{`#tteflg-Pg6A&sz8Dwf8<}2JpH0Q^?vQA-=1rLS}eeFM~ZVBOcA-dG+FZ z-jMQ&dfx7ao|h}0=k39dDc^7Gc@wd66VDrjCoz4H=hbTJd5vk;kw!@<$9>{?esjQyltN6^M<$fywr3o+s5;Lp+Vhto|gur+k0LDEQv|+0}R7j zm=>F2H1;&7B5U_HV0t`_A@~T3;8QGs**my?^)V0gdwt1dreZGU#W*aA_pl&Fc68Rj z3zP@qRjl91^HO1v&YqVXV=*x{#3b0t>}C!`ZD2Hp<1|do{N7SB330nM*oPY6gn0$k z;Xdj_-e58e>Eil@qvDaMlgf$_SQs@)<5SP$@$rUWZXAxea5-w>Cow+WMjjXMA?lV??#lUVfPP&)FC~sd z-TN6BiYqPtBZg5vWaZPS9o$Ae3olVSP1?;ZFaxT-0BR#2pq`13EZ!Nl@qyhqf9+@z zfk<3{dZ^;8!EdMuo}&f~>+W`x5!J6Ss=hL6f@T))i`vi_)Cqizx+SYo;~YS>Kj$Np zgv_6)oxee?IJ}3uSJ_Z2&4+qDD_}Bgftt7*YT+YM?dG8-*oYeM0BXF;R(^5dhzOE3QUZ} zv4GzH3S@Mzd!SBY0H(k%QBUQ|x$+JYMRS5*J|s3U)de4Kdkd%OOLP$!if z)jy||V=$ZE{~Bb9Q_%xsa50v_3#grkf96i6AZnr*OpfKSFxEybbQo%9U!oQ~1$D%8 zQLo)bRR0~QlRk(^_5S}%Mi0kD)Id+nzfm3EVJ1x2$L%}^s(lSqye;b13`Z?^hLzW$ zCOUwc_y}tJGpPQT(U*eEGcqYLq^~orncFOb;ndf{RM;A|vp%SMJr*^=G;@x*)Le&Z zw+(ed`>cGtFXykod8@dATF4_TjqgwkEZxtoyawt6rXK2K2B8KXfoeA%)qjqa*Pu>h z7wUDpglX_OYQc&7bN(7Ab$@qHGn)Ca5b^(^j;b4KfKiwX=VEGHjd~XLqMrU!s3X3B zdIo+&J#>#y^SnlFINs;3Us4|#Rb;}fn8PZnp?1;~HQ}eITQJb-Cz{`(cD@>Q5<5}j z9YWoz6R2@+So}F^ynj*sd|?Azhm5HAK0E5QDu-Hmb<}{3Q5{=axjX6z`=Q$TP@kL= zFb3D4PT~gY1L{7ief)v$B$GRRULG>KCzVl0^D(x?_O5|<2(`28r~x0Kj{FJgh=T^X zfs>o*F^YILRKHl%hH9fu@MBDi-7!S(|0pur!B|v>nO2^MnqU=b!fmJ%ID%T}Ma+P= zto#lG57l5dUTV~M(Wn#5iyEgWY9W;{f!_bdWHfLK%#GbJAI`x1xCiwf|A89t8K%d7 zQ74ynh+9a0)I?QL8>(YANA>H1n)ox+LPw)dnJHwnqi<06a0%AHL#U3ahPnkrqHaMH z>PT~$F{pOsu_;zVo!kP1t@s+M;y$Y431-5- zF$&Xu;mRdZ?Hi%)aZ4+AMfK~88fUPTM_TV5qUwZNCCd!FD+x1%hmhd3Xqe{n2{l~EH8#tinP4U0EJwd;oQaG+#+sFiNOthmR#h22HByScvi{tb-e|FoukEx1c!cL~3CoY>GPKHmHU6LcQ<9P;bGvsCH}2&E{^@yg!ZQ z{PU1GNkAR{K|KrU#<`=*hWdz&!BG4N6Jt}foV0nW&f%Fqx zeSTE?I;dx$C8~W_9~s??KITBw$`ud8uTb}PI%)ySFfnej@*dQJPM{{fgX$M=qMI-o z=A@hn)h`y+z7Fa{eI3bY!d?~_h-x?zwS!5R1Q(zl$~9KM+x!`|z;hUZ4^Sr-Z<1R; zDC&bI9qP!-peFtRnb+r4Ba??fJ)De#Q9B5upn)S$JIIGxP<_mS9Z>DYqZT|1ljB0v zLf50d9rs)O5=K#efZ9;l6fK1NpN>p20$EWjFJhKLO;8^70a6n+U|rNL>u7dEJwun{fLhQ>RQq*U z6c3?plVJvFAp{NsGi+YVWp*C>R%9s6|e=Y(q321`M)7?j7LDcI}1Oo%328y+K zP1M8ouxD{%Fovqx<9Eh551Zu~VF*UA0-Kt%v`Ocxn`_*R+?x6;LgGn*LOt+BKsMjjD zSpjoWZi*?;XHK#D1sFwqE9#-XgoW@v>SWT-a^^#wkgpsWeQ?x9ecg6J?QA4!g7M}Y zREO1=9pg~Xzzy>rY6E|uHt-kf1me$jwqZWihU7NFX1z(jigx06XrU_a_1 zxs2-Ycc1|e8B z!sn=!rTNw!Z6;KQY^Zo%)I>!s9*f#>P1HmUQ9EvjT4+zy#Dh@%Mxh?YnW%ABqEDIi zWHi85jKagHTW}wBk0a)|_c;@)T^-bkwZ#0`54DhmsH0zr`XtmiCLQ|nmDjHdU&nsqu(x{1Qp&B-@a$D3+x}YZPZRK&O9ZW-=#4L+{k2=w{sEOiG z3;qSQ(TAvcUIg^5Q77;<>S&i?Zrp;=cpWuy@Dleqkq`4y{uo=} zDDwt(py9$t*H7e%iS*~ zH83aT!KfWA#O%1+>ThE{%4t{d1&5U|0;ixJ(q$_+|9WJ8Bd`XGtaJ^oq2AxWQAd|* zm3s&apl(5N)X7yw-TQ{9kJio>ABuWLCShirZ}DBI{=Z-nyy_zpM&=gB)A-t<0jO?526-)%IeQ!3d*-IDZW5`U_Zi{+2x}i?wCrpVaQ4`)o-I~Xk07Ex8Q=o2D1nS6(Viv52d9gP};T+Tg z;xIAuduPdLplhg|-9=6G7is~)8=YaO1%_iA%!JyR4|M`FQ0>1l7h+<{D^Mr#BkCdE zhxstikIcjTUL!Jk-P)jb+6%S95vZs78;rp%sH6QIHSinMLKAIrpAV@~^|?^vl|n7J zDr&-dRvwDF#bYq={?8|qiog$;2=}8FaLl}o#VNl*9c|2J_srBsJrhk)3+jO7u@`Eb zbr>JFqE2`h>VyxXPU_rd&R;vYNOF3~#Vzbh)IhVbFwVuac*5%MUzdscE{YRdn^f>_UIf#Dc~u#Of#H;Mq3(TI)Oa;eCszmcY&Ej_PNV==oX#>cp8m;1b`sCeC%*eQA_K)z3x^ybyEYa`UA5%Ic%fuxRRYV+?+d znr|hBg!>0Y>nDbFDsA4G?e}3 za?DJ5*ICYAnX3fU;f*y&an3z-IWZ0KQszfydyFJL$jUR#Rpwr+zhLF(sEs8)?-rO1 zwV@cFWh$VKw6-;9h+0^CE046YAF~r*k6PGytAAv^F_T_!^_k2VvpVYNTcPIj^&yjv z%plW`ns}9!e@5-}mX*DWu6=sc07X%^t~BcCD_gk%MpJHq+NlrqOii%zk4~Sr*8(R{ z175N6P4iFGg5Fp;<0Us?9!y6(27|E{YNEQR1-G#BKyx%^AnwO}xCXQ8{l7$}Ab~fi z_c#A#w~%tE0b|X2sD-q&`k@vdg?jj=n)9uGt+~mJLyfl|^{ky#T<`xaGTOmI)KNah z2z-T_Fy$53p)jVVTp6|VrkDYHTX_QN1ec&DT7?>KE9zO^tuLeg6RKP2!m1er?28cm@ zG?zh5+`-~KQSJJh6Hq%|h}z*=bFX<8)&Cx9VNWgo>MG~24oQAQ|IcM>kR(^nqiT`Esc-P#QQK*lMIutg`p(d<>T39nweRp$^ zImVohTF3$`FGDTlM^tz7krvNwi1%1;zzCg5Y_$#YKQS}x?7hSHEuD~IAsI+_}gj=G&0+wcF@xtVeuKL0T)_%yLs3= zi<;nuc^~x_{fS!8TZ<>V^)WllydXr{RwwU8ZFe-^c)E9QNR zzeG*^#^OnDyLK7OTDoSc!ILtQ>L2Ei^k8CSKCY zovl0=bt0=U0dBj)`>&nvA)uAsw2Eh_9lth{-gP^ULhZCPCcqD^T-(acFbnZE7XJeE z3{Ar9I2-e09BP3N?sEPm$VA?A?_+&TOL-vbh^L_5`{k$w?J`f9w=t6V->8XF-**el zZ00jdp%z@ltc9VJ8~7~H0yRN<)PSF(PR5U+_#`go&}Qm3>%}@&r`7{iyNIn3qxg zZlD(O7(+49A8N<_PZh}UhYF~MQDyhqM8`I-*RKI-~ z509C@pq{DI7@zsQ`(!fWV@!rg9=l&QBh8vvpZH+Z5gs?Mqb7cin(!Z7h-v>DRBtuC!9&B2^XOju)^GI?nk}PC$0PvQ&A3n=H8mLsD7nT z-zilv7+a%Gsy#;GuxFgV-uHO~!f+F+<6hK&Cr}-3So~kq%0r*Ko#sZ}%c7|E)ld)F zCuU!ZPeh%}d~*eAgBzQY(FtrvO}rm9&`C^=w=DkL40_?}lcDOPQT>XTA7F0EHT_cy zg$Mp|*L)(HVTHBWVD3h3<(QR!N6q}o%1K_jk<()e;yKNd7O#eC-w1V6JEK(LZ&zOt)xHJlqpXj`zqRsa)WpA` z#(9itAN7ixtIyIBuiQ@RqYk~5+0o+NQ8#B0YDc3iJ`2@;vBg)KJI#~kEz|=3G84Xb z<3#w#=!o-TI;?KCwFZN&JPCCZzc)9cCff%i8D~`cc4ygAFBO1Owat@ zO)`-L-k^>s{TtUHk6FU3j2ft}*~H>4Q3H21hoQ!qYV`}uHK=>O9W~BL%%>0c3l<1@ z>ng%fw;~m4rTNTKR$m2mGPSJS*ldAn-`>i-Q41MheuY}-Y^z_1fxp~YPbMuDJFMa~ zYM@KzT~x>Cr~wncb0-#oD#xG}Tp87_IqC<4o~UuAVrHCcr+L^sh1%d1)Oas~d~Tq4@qz-MV4IuTX`hrr#uC-Vw}}qLyh+iweZkjX9^z~RYV}KgBNWT zWl=k=Waawi$EXFhwsLn=|GpLK+#l zarO03?{!mDyUD15=Ab@+)}qF_WcAlk6aHcGkoc}X3^j2oWE`KDk&KQoj|+I!Py;o> zz|(B;p5_o#`?07KT4ruRO?Vi!z^fL2h)F5`gZlmsP2fxu(8rNmpaAMysWfWfSky`z znk~)FsE713)Ivv_6D>X+wSZ-)4g83D=ni9Nyl&+h2^pXHy>`lAchrENqaLhVH5Du*bZF+Q2i^2hSTUk6A)pzgDR6yP^8^F(-ub{;Oh%HC%=IA#AIa<52ZS z%#*0MKdfAlp6wcwMefnTE*oFb(wM`0A@5~u|>L4B7DM4ikE%&zbMePjv}xQEj) zB9*(BE6h0a7u3<;M4iYZ)P%3CK5@9~mkAXwY~>25hqi&myQBIKm3sfjSzwmA7d*WU^fJIXWd42J3)V=MVCMfW0@i-h!c^Ov2LTTO64?vyddaQ-luqhUZ;QiNU z{b(|pa4+g9{s~j#71ZxsPt0KcR!=KWgKC%6%EeLrE1_;hb<`2Jvho1b2iGLjNB0KQ zGjJ*$@4voeUK7wx)AFAQ%P1U#c~Ju|K|Q3atUeC)6Vo9pUq!XMhdTPlsFQhTX3XFo z+R~_nHAnUDmx1?R6HXwYoy{~Cnd?xWgmLC!)I_JTGG0R6+su(JUJ5l{d9yld+yqu!F|sQ>E7OPtNEFb1`<3dk$!RYgtMGP~Q+NYswTpl;1nb1`a% zn^6nfk7{=YHQrU!Ex3((rtYKKy+NN=k~D|=ktiE7Z{W&FkKf|CfAaJl!QmQ#>qqkQ zur;Y2NjonDuLwt`MH)I(4%c@cs3S{0V>c3b`u&V(luMItN`#tbC20!L*)F z?1hbxoVqZp-|7FMLPVAFR#MMnv>Ib;nrvpo2bS0Wqk*mkj=(>`noNGNKWW7XUnxck zVT8L_(B{)mEEOp8=QV+A93%Wq?iQ&D`L!+__%{#}EdK@Z`=m*vjf@jZyftIAA?bQ< zV=KRiHa@k{huky@y7a*4ilIY7|JI7xg0lO6Wr5#O{?OW}eh3|F*q|!wI!E%8LTH!I zCi>pmoWeoYzX|z~r0LRP-;y6jIz%}YX}87C6631jEoHzZq_ddEAEf06yf`UQcGH{CX#@3gEEh~x1RhytVlXxJu?&ALHeGg zOMkH1$MOs5mz44!w10n1rS1<3m#xiW@_cc5yRE^@U_MS-QCLebJB{?s@!xAUgPym} z@8e5}b+p*`1b?URV5~=6*L+hSUGJ|kWbV=K9H}(vSNhk(Hu?uh*XhuW27J1BC9ShM z9-&cPoKJk7#lQ6j#fDe;oSM`YxnxWEoK{syTPUZrSR;Dt^F-H1V(Bcdav}f6v5~&5 zHcB0OPAA$>Ra|-h(^BODv|EPFFd0do+iP&HD!ImzABjn^7-=x$RUzprgN+z3iN+*9 z7_ZQVOFz*5zpE4#<7n^l^4Zcg29QcGe}DNiE*i2NL^ zN?ToH$nV4tssAYuasPOw0`&u_yMwt&L#fx#H+M)INjs?Hhj0J)RZ`}8OZ_>@@2|z= z^H@0>uB1Xgrmpi}uM+O-K%@kt6(#ZmwkAc9^lMyoMv5lYqRlIePh8(Ox{eZCLDCOF z`qn;4y{-;sBI>4*x{~7P*PD1Z)WvU+URmOgf&=%3AK9raMUWp=yeCxX>VmQUsUJqf zuSl+|fAxnEzKgWe_nEGPHqjLNl%$;3`UNWapTyKB!n>q%rR%2Jp>(!<*hTsha z@Hyr0Y0U3*flEIP@R8xw@~^HI;Y&-4GqhMn(pA%1b)wwg@``^+`6p|W5U(i6m7g~H zzvPlJ6_?8kYetK>`CAZq+4<^IZW zeMbBr+VuTTz2YZmQ(YOZebmh*9VeC?8_@nBN!NW+Lt^?rHmagWH;h6!g;>%jq%xEz z;3MnMiw;f57bPA+{08}c#8#2#=VR{}@&8_*Q@Bg~HI^kcAq}8hg?5FlZA1F{NyDpA z(SyuQD&p`n8rR2v@cp%yGQU^{u8!EA@=V%~B>$BSc+EeyMm}FYfs7}C@)e47r%pgX(+cR-^|)f4dw%DBEjx9 z=w0$_No`2FDmnsxWvOgD+V%5ysF|fjV{(5oOn>#pP|9D>Vg@OMd`r@YfFKh;Jiygp`K(|0o}_`pjlBVozy14A;}{ zQ+-X;N?Cd~S5hH@Ck)oe1AY%5@PFT~!FW|9U`en+3~Bwfi#h1CXA;Q&lPyI|@gDbKQY zgcEyTQuv$7+x}y9BGR;?dJ{=k1)_&Ycl@vFLUDI&lFm3BX*y(z}i*Cy|mws zJuTMB-=c1$ZwjR+^jJV@NxG(Cx%Lx(Oe)VPy5>_i1nW}Xj;koI!fLcxU~SHm*R`DT zE$ZK2$H_dU?gFtn792pnW-uR9F9|dtl_0Rl2Kol?*XD1qRFSEt}f_BQrnsG zVJnMn{)+V@(=VmwHwOEf+C1bdVQuW`?^i#YFFm=xNe8T7Qu6PwQRL6ib2))e)W()x z7H2VvuG`k$hs{X`iLanOihK(4)k&vGV~PDjtQU46Z6ICN=gxD2x+V~qOVYK44yB2= zCEpp}5YI!(Kt5rh1wRerXwn7RPQ%C4U&RM%N79v*x(4*o^^kO!_>w@0w#3`=N1K7G zHw|@tL$DK#{w41t_SibVqr8-~huB=wWow@c-`Zr!aUku>5j%xDN&i~^{??u$0@rxT zw@8~b|2Q)FXn35o#yZSku-ViduyRN8gK762SmhiV=haL6F3Xy=UwgMeUlcO>IR9YCT4&RqJPOwbiUyYPV)pY>rWz z+FKR%e}8_@*Y)pnUH5t2_xIlSGm^CDoOMSM-Z-4ly%L;on#1)nf#YPtU&9@zej>-| zRzXq6S>MocqT)Nw25d(8LSx7I9*Z_{oB_BOGsJV8vP~VQG3^@DC^_XdUpUTK>bEy{ zoS|5?h2vbr19*)3A6io9IIh#Pwd16rW8OB76GwxJ?Hng9rf%;z0T_iTumA>PSxkr3 zF%nyu!;rN*i!lT4$Aowp3*&Xnk7>U2`o&;w=6BkY$wI{h%!6yN2%g6R7}&ux8qZVi zj8`zGqvHf)WGBZ7!eW>Vt6*}hYc@AKqc+eNLvT2zVSZ;inZ)>`HQ0z6V7K`@s>21; zi9E!V_yW~0L1!->h&riY48;i4IHfQtRza^)x@pLCBBPZLHbsGYt?EihR(uRb$sBLz^;M0txhMQyxuH_l%jh7btD$*6~F zjWsxqn&2jC!ndd$CGYO_i$K*EMNLrC;_Xoz>W6AS26ap3p~l&aYJb2blbp;|)XpEG zR-E7~?_PzVRvLzSJquwDVqgK9%TF_G~yNS4w3WT9v zmx7oci=hUnjTx~WYJwqF@3Z>V7T<@u$LBB!Ud9x78w2qLW=E%|_pIf>{CfWjk5Z*C(cV$|G!Zu^#RpCT`#Yk4YTR} zk0w)`ik4Uur(!f7LhU@k*WSrwK~0nmgD^i9!g8pIzQSY}i(2qd)Co>Ny>?4b{a2$- zdJCq|`@e&X9*)DPfv%Z%Q5_#+W_*p>dD`Ay`)E|W0qWNDL@juvl^3EW+KifbJ8Jx2 zQS%)|Hx-#1WK!b`GoX)GPH*PH5bDcfFxErutR3oJe}kG}xH;aOZZ1T%TZKBIjaJ^( zhx1q9pjDhiE#xwm#>c1y=8pAN9*z3IR6?Ch7u3LCquLEb^&fBL`KS|FhkD(PU|PJ1 zTJS%yoWBN2*w?$K$<0hyka#}SQ8h;m&<9iE1WbeTP|w0osHcA)>WB}ao`K`2hwd_J zo_nYbKST9a>LgC0 zK2R4>?O&o!@`I=AWcbFrCq+?5Qv=&#L(G6%Q9C<<8t{Lp6S{^v;-{#AKbT1e@RN^n z2&!K()P~BTPOt{1!xorO?|&aM+QB!d4x_9*5jDYF)P$>0C$Js0(8HJ!Ph0sh`X4IP zcnJr3QQa;WE_gwg&ao?n6D* zr!YO#rL8%cGxAOmByheE}$x|VP?FG5g0JcD@UQ) zS4G|9I#&Jy)vrBjoUT^xZS@0Cw`>F!!-*J)?yqDt&~4O?o}+f0V7PbBLa{OB&v6-U zz#Z6hgx5aJx8AMEjvA*l>RGCWT2Ot|f|{aEqBUy#9>__$&ImH7v?wi<$KPPqB)1sJCI0m3N^g z*pJ%Tag4xAs9W$JH9^2AuRbm69%scwSPZq`Xp7fGjnf?CV`s_yPB${zQ4iF>qfrA* zMy+%)X2lKWX)H(iHHKsH@4O>zgaMR0V;1a*t8gOfS&ANQw*s}m*61cC)4>8?VL{4$ zur4maLihr83v!I{PNXa*p0ze2qQ<52D9o6F7hsCj=L!};eXvzLIL zf%~W^aE<*pHcnJpeDSAIq?aqU+DK<`>dFh za%t3jZeivI7Mk`M^$ukf&K``pWkp(qiHq<>UZB{@%L{(9*S9{cJJlvdv`k}Q6 z)&DSRBY&c9*)!Ga{ZBdByEoZT9iq*;7(}@}YQ=p}3mSsD_am?f&PE;SF;xGvR=$tg z@e5SHcvHOg0ho_+X><#d=|D#B?M&2>ZbrQ>dr%9wi5lPq>IB}P2L6C*pWMf7!BEtA z^-w1|9Q7KHL2Y1(mDgev<-Q+rbO}G*@-g>Lw;gZq7ahL)xpcZlm^;*3(L#BIgLtacpyn)#c z)vgal;CM`lYp@{xf;yQ8=D(;DN;SiK2;B%W`Y~G;wX?>k30j&xP#uP1cASKI1~!{J zP#f5b+Q1Ri37kjWqUTl)nCVRzi5jOIrq}!5mW&oK5cA_SEQb401N@EpkOa>1?qPn^ zQ5QiixB_YcpP&{{5B=W<)I-+=bpm}*?MI>7Pr!6~|7VcVL$Vgt;ixq@W92KTdv+If z63?*+CYbFFTmo}Zu8!f@3v~-7qsCi^y7#NByaBbq9T=ha{~#Hy>@MnPpP)LtviN({ z4ie4r;-RP=WI;_7g&ME~YN0WxiEE?!H9|*L#`^V?N5&Q45Jh9sLm0mvB7#cWiDzEod8Rf%`BW9-GVgYoeP3^nTt$J$#Q* z132@%dz~2dETlmV6ouMxVbm=tXYtyo1vRm9Yt*yS#mfCq3ms#R6ZdDu1jbCF4oQvAXDOCR}sD<7_ozx4|0unFsmt7}0 z88r+?HOy({;;5aJMNL@6$}La>e2F@VE*2kvI?~~&i6)^Iyco69U8s2una43u@Bdjc zdOfbA7VsK1P{0q~&eNdc5vbRxAZp-Bs84lsi}ytBe7N~NYN0bxw`Lydq*tL9v{7Z| zcXnEX1L!|G)IGa`+R07Sf?lHrNVM4d@tG1MD3>sspiZhkX2Qv+g|0OZqaMnes9P6* z3HQG^nGiBru?A|V-B3q65Od);jKobCg=aB8{)>4q-%|dB$0p`x>`XainfFB;j3p`0 z!{&GfW3b3_&R-8r>~e3Q)mVn|IqZ)iE4a@%9yQT3EQFa?dQWp5)Q(~?JNi)d+b}Qw zg|#v5D(|OcJJfswu|EE^it}GfCgG1>gN>Mh@=?^$-NMA^toCj}64cS9L*4r*)U#8@ z;`LC^NE^(8y)8Zkbqg0`a@>G{xXUG@365YwJc&v191g>4_!8T#;V)xs#%eY38_aei)O&u>Rq0YAr4SPHLUB6Kru@CJ-PJv_xQ6l-HT{1TJnAPmAW zsFlw`EqJNbufkN6x1x4>2-D(O)HC$}HSSwfyO5v!^7}s-?YKDV;i!N*k-3-}m!Kxx zg1R;PFaR%^H&D0g9_q*wZS)?x?5Jm^3PxZL)B+}9GF*WHdjB_)(ayG`COU#zz*+MO zYJs=04L(8btidMl1UjMGcQ<2E6AVV3#Awt*JRS4mJJdY6Hght1|BI2)PAj8U*a-Dh zcgLbQ4s~R^Q3J=J2Dpg9c*pAhK^=AA7H`2BQ4?mjay`^7{v0)4Z*(=lw`7vw4AcS^ zn%l5AB?x-Kr_56P|@Ssgu7fp z=!yqX?{U#>-p-n$2I`81uotGoAFTdo45j=#>S4TzTG(IaD=ber!Oz}8Vz4yj8dwBJ z{LJ}BlG#Fl2h%x^*>LQ3&$U>P@=4T5#oyt*?`bgytT8vi_y3kGvXCYi*GOu z2L0mwyX5Ss@@Fm?t-KRvipM=i{{nV;J70@Bp-rd(ccJdx1=Nw=!w~!jb?;N|^2W=I zI=QT_;jAiGz3)Lu%%d$fC8LHT zummnb7VVrzUUg?toOe`>PIwPr2TVbHusIHOl(Ve73iS|fGk-HrqTYr-t$bVRh@V@) zIq7vwfohl$_3%Vmxg_dYD36+;I_jZoWaY1{ejuv)k;Vo|Jl+M9R?hEN`ddW{#Hw)cMv0qtau zHN1dY&}}O}G2ffX&UgccVoKU&#~>_#&`GJfETxS(q%>AeZ#aa0=>X~?l=`rAc z-al9jM@^IswctWlu4OjEjKn)&UL1y5aSayG`yWR}@9ziHLQ-Ay1`IW`qZU#aRbS8I zO;8VCd$YII4>!k{lTZtsfqK@KSbQrc(EGoOjCQsUL-BXigf~$g5?u0rSfoSkJTGR% zDpqcVT3CP7L@sK)@u+8II%=HNs0Hsf|3LrW|CcTB0QF^iYbL$ybqGZbnA0p|Mw?a4 zI;e#-MLiR(%pRs|PBa%>=KL$ta2)}y^a*N!fGgg&JOygvQmA+gs$C7U6>7(^s2vVB zr={o2ZVDQ1Q1`j(^RI2cf?GneY>=f@(J&wQwJ5;AN-{tw)Ww z$I8d7{xY^;e&-GuJoRS)lb7L#1~k37pnas)DF+1ZrxMV z0u$fx#z|?0`E~g^S)e#-2Qg+Ni+4f|7;EJT<{WbcYJ$z?FQ~U@KWafIEPf5Ok-Hd) zFD3Ikp*OuiY1BYpm~BxD>TC`|Eo7qAuRsmB&iuvVhfx#9S^TQ`(EJCpQJ?r0=UQ1g3pHpb9yxs~ss7Wx_sVY1s^xeTgY2X!JY2H^Lo6PS9N`>&O5 zv5EtzfsUD1Q9FK)T43@!-T)a;`$T6~-N8`9r(PLjz?gA1td zz-!b`!QhA93iDuc%F&n%Ke2KHEJ?W)Y9}*L3tDcjMfKZ^TF5?3iWjZ^mS67w12Ss( z+;kp!4^s+Mhy1AelBkKQm<`c?0;t~~x?%<#gz7gPIfH^n@|%UL{0by{(yH;6O4P})h{wvnCr|fSc!H!(fyRn2QvEA z=+md(MEy`Z9*df2hLz`=Kca5YHY>-Wc6=Gt{*L*`e2IC8zr!RL_00P%yU;W4e;NWY z1k|w!YT}k=SF=CrDIRI%RTxZpE9z}IV(|y4XXp(kz%t!u(&?!9deje@ov0mNLLKpKRQq?Rhb!oXXD(E{5~_Vevn6VSolqyx12uks zmy9}$#2}n$6)VimR=>~c&shAH`3!T>?}L?dz4R7b9Q_MGP1q7OVMntMYGdv&3oJw} zWQ~>gn8z>`@&B3kE&dMGKItp(-ey8=;1kqq+5~knZLtIn#EJM5rp1`o{tuq(G$m7r zKp!vQEXAUf_geWSszd6(y)Rl6>PuPI%3VOwV0wISru)a6ry%OTnC8Tg z(F(sXJE10yMZGqIP~Y<9sD&QK5WHaJhvsWD-dnGIpjj02)BX$8TQmkW&RTS}!fj-< z^K+<)@0m|g3;Wwl^v=7N!I+bH4OIJ{sQ&#>?Z;vUoPlAu0d+#htp2ij?;Yo_4zCGl zpoH%|15xo{)WG3p5!66ct-g`j7In{ipvD=A`k;-s@-}lf>Q)>;jsNF+d;cF;gEy$7 zasKrNN@k`;bx3dJ9H@olGs~hDTFdHNp~mTe>9D88ze9~P!JOlgQO6ai0e7H|?5LIR zpjP}E)h^Ws?~9iWHBeQ|f(@+P3pLIVD^Ea;HwQJ&R@4c_S-pGCGB?eqs1E;HIinNL z-ysS$VF9xY>d32Gxg~1iuI510r+FNz-5S(GyC3y1pF>_(*ZDw30~dce@DJ2D*UH0cwY8 zH^J(sp(gyn;@iyKsEH4t#)(6n;AJbnLyeO(kvD#-MDe`$KN|scD1_=z4s}G$&2Fd( z2cgVF_oly_nAnb(Gto$!(g3!dCk*M+VqMo%PsBy};WHe!Q)VH}gYQSOEV79s3++^-W z4fuzZ&!B#(ypHOB*XrXX@!BOp?K}u|65*(y32q)TT1iuD@Fi-1SaSkuC(BXq^9HPd zr%?Sul6n(Fp!!9b6)oNj)vh(_SF-L_?&a0H&JZu-j6}UA9gPQ1(`PNJl=*82TSafQe979d?%E}p1c=tR8_04XNb#Xja!%J8i zv!(Q2&lae5y)hc+U@1I{`rrfwdFDkue4hmI{x=~LOJF!2!(v!3mAB%NsP}#?YQ?`= z`7}mQzK41!15^8dm^k@SC({!3Hub{-I2WhjQPiz$8SL3BnD<|SZwct=XP{1G5o*G< z)^L}3!s0ir`~vmRCJyo1N22-{Hp`oznoUu+_Dj@!-CQy~$n?eE@HY0r)oHwY8<{qq z|F7fnIEr|4td2KON1rd9ca$Bl7Uikf6tAJa_0gf;gndyP9gJyk5~fCXsb#jJR(=@O z@T`^pLaq1}>Q=l*9dQW%GEn7wsH2WSeY!iMo`F%Qd%hO6(=s9Ula_0(@P<4_OnL)5}jW%T;zMvYeywXvFJ6SF-g)BE4c zpWz#hn&>;Mj1y4z_N2uhpay(yzDEt5ILte-G&q)W6qdrDQ1$mw3;!E+vPt;&y!zya zVKDPM1bsK0^(ZDvM`kRL4B1lPQKess5;Mx@+|#Q2oBQ`q}0pa|L#zejRGwOyRu$ zT1oM6uVZ!8hoKRw!3c9M>fzdqn(#DgLFZ8uUdK=IF=}CDBE0WG4CbKR8}*%-g^O_q z>I>N_()FIw0g>KQJRNmZJ5UQdiu!b3LH#C_II9;gf<-A;MLp%;SbQ4ltyqETw+pqv zJE(=dK;BH}E$XcacC&jsDv8=r8Pvp8&8DaUx}p}=AJuL&YQQO|6Pbm2sOF*CZ9pw# zFX}g=bI82@D+e8}#OKwd>%6e@ zhW6tvKa}`g()XlI#H$c*#Tczgx}MwE%Ktzc9$Kf0zH*Z(=+YCTs|Xzee1|J#i`J}~WlOIevKshyOx5ZBr zJR^Bs!`b=OmBNz2tl z5|OS^=DBt@kl&A$NXM;bW@0-?%SgIXlD@Y5BKqmihKIEOcuk`25rzL*nDDe>QxT4&7+L=ff#soz?L$jq2e-;`1#&+n2gZNY%d7gjnQ)Eu|l=J|S(T zoXTR2=&cWmu8qV(Ev|9_U*{@e?lv2xEh%lG zZPF&v4(j;r+qbo9>fG zt~w({kUpi&-^d@V{vSA0IY)`DBvSR4p`7MRHwyJFA7d=V_;3R&@Pl6HTH|Ny@pcpTCm- zNJ@PYyhA!eK0fs?X{&1`EG}FdW{HWF6!)W4lN#`ua-!~ncJ`6IHG{u4y>6G6F zNJxA%`3Pc%$?u{~anc0xx<-3ATZyG5c96E`$$z}g6WdK;kMHMCq9UK#sQhu^tfq#4 zYvHuCSQW|@eetS?xbG|yPDEEB+;6du&uXxhzx&Tzm&qTYZClb+^7?gP9r+`ehg6sR z2vSk{X25a8bp2#wHTH^moLW>CA$Xm@2O2%6yd3%C&420F0X`Ydr@o!lL*2BrJf#*S zU7uR34wU;^Uh!`!AF?(9cu7I7e6$I%C6vW_q#jnzN4o;#n_zxYUs4Z}u5w-**I7j7 zCkEL^#jm)4beVh!8$^vCQ|_w_S1;oK(5Cl)>J|TkHZ_#t+DF|y(lKHw@iW>VAnE#x z^ck_2)K%4^n~Xv*h03JQNu?=|!w1%(CmouQFHAfg@oVH`iTy}EGx^_%|M%)g;WqJq zP=9JQA@!$Rg?0t4?Pv6zLK<3~itc2tQ}GLaP2>9b7C&BlDf1V#|LTD4C{LsP2=d?A zfPeaC)y(V0P`XFDNsEuyMas*q)f0ldET4?ty{z2{Y)5>X)v0e*@~dsu9+ZbsUaSf( z{s{H|e>1fw|GBl9lzu2^EaKCzkNC^seX=Y-SA_w>_{%sHt!O?vJ% zbohrk&!BvYq(9(w9i;uAv@1_+AnD^(p4eg1 zERwDZzIe4mlWw7PX;LCzWbH7w47nAglhhZZ-Anqv!WE>Vq(4YMP`8RUAFoqne8i{Y z9ojc1e~I+*dZIGzGySK}U*w1DpxUNtigGZ@fPx_Yp9_rWQ1Il-)uSI!kJYRI3beZE3`rW3P zL6eK5L&WY8d*ka~C)7PcDH9!jA`)sXY2>UW-;lI{l$|y|(neQ8@~^$D(~j62%JWGL z$WI_mBwZvWAvT`A8%Z-M=b&8O`iW!2xT5|4=}=kM%Q@fCp#b?EG-yM-mDkY8Lp+>( zXX-u>kMfnP8|v03n%8>1BD$Lv&G7~?{*9IYI%^{gprn7Y^MXFdiTz8OPWp!Ozx3%w z(iKE1NGuLhV}A^wU3}^@Ql4(@2q$yCrSO`{o4(6+L({gTdNWB^MWP2uw|q(Kg~co8 z%TX^p_e5H6Cw7;#(ArhQeYD?>UsH>m%3{Xym_b?1o9wO~K;H52fedPU$fQgH%{ZJ^nB z#|C>y{60xnEm98Z%Hx;R2NC~-d?5MyNg0>u#}oB3^tS6T;wZZE&R$ivOzXCJ-OGU z->hG9@*l4e58PT z0ey7cCmkZb)L-Iv7UFIB=T`o!Ck=JYBG{2e@5m1!_Q*PapuCKB6oe+-#CG(1LHV;!b3*i7nvwQ>jY186sw@;Fj% z$_;5#idb>d$15*&ldW)_`r4$L#OlyyB%UNaCOxx08R}5TPKR{(@oGzc@5Y@Cs}#&4c&De`{>Fp-D= diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 9e3ec9d74..789db6bfe 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -3086,7 +3086,7 @@ msgstr "测试失败: 账户无效" #: terminal/backends/command/es.py:27 msgid "Invalid elasticsearch config" -msgstr "" +msgstr "无效的 Elasticsearch 配置" #: terminal/backends/command/models.py:14 msgid "Ordinary" From d2dc2ab02c8baca3c1351223931f27e7f8d43db4 Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 17 May 2021 15:42:52 +0800 Subject: [PATCH 07/15] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/locale/zh/LC_MESSAGES/django.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 789db6bfe..0bd344d55 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -2888,7 +2888,7 @@ msgstr "取消" #: templates/flash_message_standalone.html:37 msgid "Go" -msgstr "" +msgstr "立即" #: templates/index.html:11 msgid "Total users" From 20dacea26091f64fd42b24a38491f8f98126f38c Mon Sep 17 00:00:00 2001 From: "fghbng@qq.com" Date: Mon, 17 May 2021 13:40:39 +0800 Subject: [PATCH 08/15] =?UTF-8?q?=E4=BB=AA=E8=A1=A8=E7=9B=98=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E7=BB=84=E7=BB=87=E6=8A=A5500=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/api.py | 2 ++ apps/orgs/caches.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index 9e9ceeda6..4bbba64e3 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -221,6 +221,8 @@ class IndexApi(DatesLoginMetricMixin, APIView): query_params = self.request.query_params caches = OrgResourceStatisticsCache(current_org) + if current_org.is_root(): + caches.refresh() _all = query_params.get('all') diff --git a/apps/orgs/caches.py b/apps/orgs/caches.py index c9d60bd18..8f73dc06e 100644 --- a/apps/orgs/caches.py +++ b/apps/orgs/caches.py @@ -87,6 +87,8 @@ class OrgResourceStatisticsCache(OrgRelatedCache): return users_amount def compute_assets_amount(self): + if self.org.is_root(): + return Asset.objects.all().count() node = Node.org_root() return node.assets_amount From c8d7d42f66d865851e1fc29873fe62e3d0e9dfb1 Mon Sep 17 00:00:00 2001 From: "fghbng@qq.com" Date: Mon, 17 May 2021 15:30:17 +0800 Subject: [PATCH 09/15] =?UTF-8?q?=E4=BB=AA=E8=A1=A8=E7=9B=98=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E7=BB=84=E7=BB=87=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/api.py | 2 -- apps/orgs/signals_handler/cache.py | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index 4bbba64e3..9e9ceeda6 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -221,8 +221,6 @@ class IndexApi(DatesLoginMetricMixin, APIView): query_params = self.request.query_params caches = OrgResourceStatisticsCache(current_org) - if current_org.is_root(): - caches.refresh() _all = query_params.get('all') diff --git a/apps/orgs/signals_handler/cache.py b/apps/orgs/signals_handler/cache.py index 5862d5edf..626975991 100644 --- a/apps/orgs/signals_handler/cache.py +++ b/apps/orgs/signals_handler/cache.py @@ -18,6 +18,7 @@ def refresh_user_amount_on_user_create_or_delete(user_id): for org in orgs: org_cache = OrgResourceStatisticsCache(org) org_cache.expire('users_amount') + OrgResourceStatisticsCache(Organization.root()).expire('users_amount') @receiver(post_save, sender=User) @@ -44,6 +45,7 @@ def on_org_user_changed_refresh_cache(sender, action, instance, reverse, pk_set, for org in orgs: org_cache = OrgResourceStatisticsCache(org) org_cache.expire('users_amount') + OrgResourceStatisticsCache(Organization.root()).expire('users_amount') class OrgResourceStatisticsRefreshUtil: @@ -67,6 +69,7 @@ class OrgResourceStatisticsRefreshUtil: if cache_field_name: org_cache = OrgResourceStatisticsCache(instance.org) org_cache.expire(*cache_field_name) + OrgResourceStatisticsCache(Organization.root()).expire(*cache_field_name) @receiver(pre_save) From ba35f5906b1864382fa67141d2ac5085c00e9ea5 Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 17 May 2021 18:26:43 +0800 Subject: [PATCH 10/15] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=94=B6?= =?UTF-8?q?=E9=9B=86=E7=94=A8=E6=88=B7interval=E7=AD=89=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E7=9A=84=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/ops/mixin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/ops/mixin.py b/apps/ops/mixin.py index 166c5db75..c6f1ce184 100644 --- a/apps/ops/mixin.py +++ b/apps/ops/mixin.py @@ -103,12 +103,14 @@ class PeriodTaskModelMixin(models.Model): class PeriodTaskSerializerMixin(serializers.Serializer): - is_periodic = serializers.BooleanField(default=False, label=_("Periodic perform")) + is_periodic = serializers.BooleanField(default=True, label=_("Periodic perform")) crontab = serializers.CharField( max_length=128, allow_blank=True, allow_null=True, required=False, label=_('Regularly perform') ) - interval = serializers.IntegerField(allow_null=True, required=False, label=_('Interval')) + interval = serializers.IntegerField( + default=24, allow_null=True, required=False, label=_('Interval') + ) INTERVAL_MAX = 65535 INTERVAL_MIN = 1 @@ -122,7 +124,7 @@ class PeriodTaskSerializerMixin(serializers.Serializer): return crontab def validate_interval(self, interval): - if not interval: + if not interval and not isinstance(interval, int): return interval msg = _("Range {} to {}").format(self.INTERVAL_MIN, self.INTERVAL_MAX) if interval > self.INTERVAL_MAX or interval < self.INTERVAL_MIN: From 6449f36c7ed007da4b2cfc2ce77282ff10447b79 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Mon, 17 May 2021 19:11:28 +0800 Subject: [PATCH 11/15] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=E6=96=87?= =?UTF-8?q?=E6=A1=88=20(#6129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: 修改i18n * perf: 修改文案 Co-authored-by: ibuler --- apps/locale/zh/LC_MESSAGES/django.po | 9 +++------ apps/settings/serializers/settings.py | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 0bd344d55..41403c0a4 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -2435,16 +2435,13 @@ msgstr "" #: settings/serializers/settings.py:172 msgid "Number of repeated historical passwords" -msgstr "历史密码可重复次数" +msgstr "不能设置近几次密码" #: settings/serializers/settings.py:173 msgid "" "Tip: When the user resets the password, it cannot be the previous n " -"historical passwords of the user (the value of n here is the value filled in " -"the input box)" -msgstr "" -"提示:用户重置密码时,不能为该用户前n次历史密码 (此处的n值即为输入框中填写的" -"值)" +"historical passwords of the user" +msgstr "提示:用户重置密码时,不能为该用户前几次使用过的密码" #: settings/serializers/settings.py:177 msgid "Password minimum length" diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index 28ddf1cd7..f26679c29 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -170,7 +170,7 @@ class SecuritySettingSerializer(serializers.Serializer): OLD_PASSWORD_HISTORY_LIMIT_COUNT = serializers.IntegerField( min_value=0, max_value=99999, required=True, label=_('Number of repeated historical passwords'), - help_text=_('Tip: When the user resets the password, it cannot be the previous n historical passwords of the user (the value of n here is the value filled in the input box)') + help_text=_('Tip: When the user resets the password, it cannot be the previous n historical passwords of the user') ) SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField( min_value=6, max_value=30, required=True, From bf53df46dc8ebdd8f80b4c1dbdc623936e99d05b Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Mon, 17 May 2021 19:11:55 +0800 Subject: [PATCH 12/15] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=8C=85?= =?UTF-8?q?=E5=90=AB=E7=BB=84=E7=BB=87=E7=AE=A1=E7=90=86=E5=91=98=E6=97=B6?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E5=88=A0=E9=99=A4=E7=BB=84=E7=BB=87=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20(#6130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bai --- apps/locale/zh/LC_MESSAGES/django.mo | Bin 75055 -> 74907 bytes apps/locale/zh/LC_MESSAGES/django.po | 37 +++++++++++++-------------- apps/orgs/api.py | 7 +++-- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 70c15aafc0ad5797565364e167bab1d948d1fb7a..1272a409d6bb7dce4f96f3e04df23de3c6b07b69 100644 GIT binary patch delta 22066 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 X+x+O=>+bDY{QqjVwQ}NsrXl|eJhf5w diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 41403c0a4..1434212a1 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-05-17 16:17+0800\n" +"POT-Creation-Date: 2021-05-17 18:56+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -1811,36 +1811,36 @@ msgstr "没有该主机 {} 权限" msgid "Operations" msgstr "运维" -#: ops/mixin.py:29 ops/mixin.py:92 ops/mixin.py:160 +#: ops/mixin.py:29 ops/mixin.py:92 ops/mixin.py:162 msgid "Cycle perform" msgstr "周期执行" -#: ops/mixin.py:33 ops/mixin.py:90 ops/mixin.py:109 ops/mixin.py:148 +#: ops/mixin.py:33 ops/mixin.py:90 ops/mixin.py:109 ops/mixin.py:150 msgid "Regularly perform" msgstr "定期执行" -#: ops/mixin.py:106 ops/mixin.py:145 +#: ops/mixin.py:106 ops/mixin.py:147 #: xpack/plugins/change_auth_plan/serializers.py:53 msgid "Periodic perform" msgstr "定时执行" -#: ops/mixin.py:111 +#: ops/mixin.py:112 msgid "Interval" msgstr "间隔" -#: ops/mixin.py:120 +#: ops/mixin.py:122 msgid "* Please enter a valid crontab expression" msgstr "* 请输入有效的 crontab 表达式" -#: ops/mixin.py:127 +#: ops/mixin.py:129 msgid "Range {} to {}" msgstr "输入在 {} - {} 范围之间" -#: ops/mixin.py:138 +#: ops/mixin.py:140 msgid "Require periodic or regularly perform setting" msgstr "需要周期或定期设置" -#: ops/mixin.py:149 +#: ops/mixin.py:151 msgid "" "eg: Every Sunday 03:05 run <5 3 * * 0>
Tips: Using 5 digits linux " "crontab expressions (在线工" "具
注意: 如果同时设置了定期执行和周期执行,优先使用定期执行" -#: ops/mixin.py:160 +#: ops/mixin.py:162 msgid "Tips: (Units: hour)" msgstr "提示:(单位: 时)" @@ -1962,12 +1962,11 @@ msgstr "更新任务内容: {}" msgid "Disk used more than 80%: {} => {}" msgstr "磁盘使用率超过 80%: {} => {}" -#: orgs/api.py:76 -#, python-brace-format -msgid "Have `{model._meta.verbose_name}` exists, Please delete" -msgstr "`{model._meta.verbose_name}` 存在数据, 请先删除" +#: orgs/api.py:79 +msgid "Have {} exists, Please delete" +msgstr "{} 存在数据, 请先删除" -#: orgs/api.py:80 +#: orgs/api.py:83 msgid "The current organization cannot be deleted" msgstr "当前组织不能被删除" @@ -3037,19 +3036,19 @@ msgstr "登录了" msgid "Filters" msgstr "过滤" -#: terminal/api/session.py:189 +#: terminal/api/session.py:185 msgid "Session does not exist: {}" msgstr "会话不存在: {}" -#: terminal/api/session.py:192 +#: terminal/api/session.py:188 msgid "Session is finished or the protocol not supported" msgstr "会话已经完成或协议不支持" -#: terminal/api/session.py:197 +#: terminal/api/session.py:193 msgid "User does not exist: {}" msgstr "用户不存在: {}" -#: terminal/api/session.py:201 +#: terminal/api/session.py:197 msgid "User does not have permission" msgstr "用户没有权限" diff --git a/apps/orgs/api.py b/apps/orgs/api.py index b241301a8..ace14112b 100644 --- a/apps/orgs/api.py +++ b/apps/orgs/api.py @@ -60,7 +60,10 @@ class OrgViewSet(BulkModelViewSet): @tmp_to_root_org() def get_data_from_model(self, model): if model == User: - data = model.objects.filter(orgs__id=self.org.id, m2m_org_members__role=ROLE.USER) + data = model.objects.filter( + orgs__id=self.org.id, + m2m_org_members__role__in=[ROLE.USER, ROLE.ADMIN, ROLE.AUDITOR] + ) elif model == Node: # 跟节点不能手动删除,所以排除检查 data = model.objects.filter(org_id=self.org.id).exclude(parent_key='', key__regex=r'^[0-9]+$') @@ -73,7 +76,7 @@ class OrgViewSet(BulkModelViewSet): for model in org_related_models: data = self.get_data_from_model(model) if data: - msg = _(f'Have `{model._meta.verbose_name}` exists, Please delete') + msg = _('Have {} exists, Please delete').format(model._meta.verbose_name) return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN) else: if str(current_org) == str(self.org): From 2e118665f5944d9355aaaa8b8077e46c840d5da8 Mon Sep 17 00:00:00 2001 From: xinwen Date: Mon, 17 May 2021 19:50:13 +0800 Subject: [PATCH 13/15] =?UTF-8?q?fix:=20=E8=BF=87=E6=9C=9F=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E9=80=80=E5=87=BA=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/backends/api.py | 7 ++++++- apps/jumpserver/settings/auth.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/authentication/backends/api.py b/apps/authentication/backends/api.py index 63356eff6..9da3bbbc3 100644 --- a/apps/authentication/backends/api.py +++ b/apps/authentication/backends/api.py @@ -8,7 +8,7 @@ from django.core.cache import cache from django.utils.translation import ugettext as _ from six import text_type from django.contrib.auth import get_user_model -from django.contrib.auth.backends import ModelBackend +from django.contrib.auth.backends import ModelBackend as DJModelBackend from rest_framework import HTTP_HEADER_ENCODING from rest_framework import authentication, exceptions from common.auth import signature @@ -25,6 +25,11 @@ def get_request_date_header(request): return date +class ModelBackend(DJModelBackend): + def user_can_authenticate(self, user): + return user.is_valid + + class AccessKeyAuthentication(authentication.BaseAuthentication): """App使用Access key进行签名认证, 目前签名算法比较简单, app注册或者手动建立后,会生成 access_key_id 和 access_key_secret, diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 9fe5886c0..502146f13 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -120,7 +120,7 @@ LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS -AUTH_BACKEND_MODEL = 'django.contrib.auth.backends.ModelBackend' +AUTH_BACKEND_MODEL = 'authentication.backends.api.ModelBackend' AUTH_BACKEND_PUBKEY = 'authentication.backends.pubkey.PublicKeyAuthBackend' AUTH_BACKEND_LDAP = 'authentication.backends.ldap.LDAPAuthorizationBackend' AUTH_BACKEND_OIDC_PASSWORD = 'jms_oidc_rp.backends.OIDCAuthPasswordBackend' From ba28f3263d7d818d729c6b3c45d27756e31f26f7 Mon Sep 17 00:00:00 2001 From: xinwen Date: Tue, 18 May 2021 13:55:44 +0800 Subject: [PATCH 14/15] =?UTF-8?q?fix:=20=E4=BC=81=E4=B8=9A=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1&=E9=92=89=E9=92=89=E8=A7=A3=E7=BB=91=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/authentication/api/dingtalk.py | 2 +- apps/authentication/api/wecom.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/authentication/api/dingtalk.py b/apps/authentication/api/dingtalk.py index e4b2ea85b..ce1732118 100644 --- a/apps/authentication/api/dingtalk.py +++ b/apps/authentication/api/dingtalk.py @@ -21,7 +21,7 @@ class DingTalkQRUnBindBase(APIView): if not user.dingtalk_id: raise errors.DingTalkNotBound - user.dingtalk_id = '' + user.dingtalk_id = None user.save() return Response() diff --git a/apps/authentication/api/wecom.py b/apps/authentication/api/wecom.py index 1ab5ff725..c66da5f79 100644 --- a/apps/authentication/api/wecom.py +++ b/apps/authentication/api/wecom.py @@ -21,7 +21,7 @@ class WeComQRUnBindBase(APIView): if not user.wecom_id: raise errors.WeComNotBound - user.wecom_id = '' + user.wecom_id = None user.save() return Response() From f9ca46dd6737eccd7e40843a5cd931865eb678d1 Mon Sep 17 00:00:00 2001 From: xinwen Date: Mon, 17 May 2021 19:32:12 +0800 Subject: [PATCH 15/15] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=8E=86=E5=8F=B2=E5=AF=86=E7=A0=81=E5=9C=A8=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E6=97=B6=E4=B8=8D=E8=B5=B7=E4=BD=9C=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/users/models/user.py | 16 +++++++--------- apps/users/serializers/profile.py | 2 -- apps/users/signals_handler.py | 18 +++++++++++++++++- apps/users/views/profile/reset.py | 2 -- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 715f02b9d..97a9e3d6d 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -7,8 +7,6 @@ import string import random import datetime -from functools import partial - from django.conf import settings from django.contrib.auth.models import AbstractUser from django.contrib.auth.hashers import check_password, make_password @@ -30,7 +28,7 @@ from users.exceptions import MFANotEnabled from ..signals import post_user_change_password -__all__ = ['User'] +__all__ = ['User', 'UserPasswordHistory'] logger = get_logger(__file__) @@ -83,12 +81,6 @@ class AuthMixin: else: return False - def save_history_password(self, password): - UserPasswordHistory.objects.create( - user=self, password=make_password(password), - date_created=self.date_password_last_updated - ) - def is_public_key_valid(self): """ Check if the user's ssh public key is valid. @@ -771,3 +763,9 @@ class UserPasswordHistory(models.Model): user = models.ForeignKey("users.User", related_name='history_passwords', on_delete=models.CASCADE, verbose_name=_('User')) date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created")) + + def __str__(self): + return f'{self.user} set at {self.date_created}' + + def __repr__(self): + return self.__str__() diff --git a/apps/users/serializers/profile.py b/apps/users/serializers/profile.py index 261dd6f01..c1caf1f99 100644 --- a/apps/users/serializers/profile.py +++ b/apps/users/serializers/profile.py @@ -39,8 +39,6 @@ class UserUpdatePasswordSerializer(serializers.ModelSerializer): limit_count = settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT msg = _('The new password cannot be the last {} passwords').format(limit_count) raise serializers.ValidationError(msg) - else: - self.instance.save_history_password(value) return value def validate_new_password_again(self, value): diff --git a/apps/users/signals_handler.py b/apps/users/signals_handler.py index 887531f45..9831367fc 100644 --- a/apps/users/signals_handler.py +++ b/apps/users/signals_handler.py @@ -6,17 +6,33 @@ from django_auth_ldap.backend import populate_user from django.conf import settings from django.core.exceptions import PermissionDenied from django_cas_ng.signals import cas_user_authenticated +from django.db.models.signals import post_save from jms_oidc_rp.signals import openid_create_or_update_user from common.utils import get_logger from .signals import post_user_create -from .models import User +from .models import User, UserPasswordHistory logger = get_logger(__file__) +@receiver(post_save, sender=User) +def save_passwd_change(sender, instance: User, **kwargs): + passwds = UserPasswordHistory.objects.filter(user=instance).order_by('-date_created')\ + .values_list('password', flat=True)[:int(settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT)] + + for p in passwds: + if instance.password == p: + break + else: + UserPasswordHistory.objects.create( + user=instance, password=instance.password, + date_created=instance.date_password_last_updated + ) + + @receiver(post_user_create) def on_user_create(sender, user=None, **kwargs): logger.debug("Receive user `{}` create signal".format(user.name)) diff --git a/apps/users/views/profile/reset.py b/apps/users/views/profile/reset.py index 356694020..ba9cfd9b7 100644 --- a/apps/users/views/profile/reset.py +++ b/apps/users/views/profile/reset.py @@ -111,8 +111,6 @@ class UserResetPasswordView(FormView): error = _('* The new password cannot be the last {} passwords').format(limit_count) form.add_error('new_password', error) return self.form_invalid(form) - else: - user.save_history_password(password) user.reset_password(password) User.expired_reset_password_token(token)