From 6571209864aa211e5a05f229866f9585c89f7250 Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 19 Apr 2021 16:59:28 +0800 Subject: [PATCH 01/39] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E7=9A=84=E7=B3=BB=E7=BB=9F=E7=94=A8=E6=88=B7=E5=BE=88?= =?UTF-8?q?=E5=BF=AB=E8=BF=87=E6=9C=9F=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/assets/tasks/push_system_user.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/assets/tasks/push_system_user.py b/apps/assets/tasks/push_system_user.py index 88b3a62a6..b56cfcdd8 100644 --- a/apps/assets/tasks/push_system_user.py +++ b/apps/assets/tasks/push_system_user.py @@ -57,7 +57,6 @@ def get_push_unixlike_system_user_tasks(system_user, username=None): 'state': 'present', 'home': system_user.home or Empty, 'groups': groups or Empty, - 'expires': 99999, 'comment': comment } From ec393c1440158905ab8facf701a4196fd9a27b08 Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 20 Apr 2021 11:27:02 +0800 Subject: [PATCH 02/39] =?UTF-8?q?fix(task):=20=E4=BF=AE=E5=A4=8D=E6=8E=A8?= =?UTF-8?q?=E9=80=81=E8=BF=87=E6=9C=9F=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/assets/tasks/push_system_user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/assets/tasks/push_system_user.py b/apps/assets/tasks/push_system_user.py index b56cfcdd8..f640e03a1 100644 --- a/apps/assets/tasks/push_system_user.py +++ b/apps/assets/tasks/push_system_user.py @@ -56,6 +56,7 @@ def get_push_unixlike_system_user_tasks(system_user, username=None): 'shell': system_user.shell or Empty, 'state': 'present', 'home': system_user.home or Empty, + 'expires': -1, 'groups': groups or Empty, 'comment': comment } From 94a798eb01c6b3bf49e2389df1a0f4e8c7e8f54c Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 20 Apr 2021 11:27:02 +0800 Subject: [PATCH 03/39] =?UTF-8?q?fix(task):=20=E4=BF=AE=E5=A4=8D=E6=8E=A8?= =?UTF-8?q?=E9=80=81=E8=BF=87=E6=9C=9F=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/assets/tasks/push_system_user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/assets/tasks/push_system_user.py b/apps/assets/tasks/push_system_user.py index b56cfcdd8..f640e03a1 100644 --- a/apps/assets/tasks/push_system_user.py +++ b/apps/assets/tasks/push_system_user.py @@ -56,6 +56,7 @@ def get_push_unixlike_system_user_tasks(system_user, username=None): 'shell': system_user.shell or Empty, 'state': 'present', 'home': system_user.home or Empty, + 'expires': -1, 'groups': groups or Empty, 'comment': comment } From 3d6aa15ecea33060b74f4e4755ed2e0df376fbd0 Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 20 Apr 2021 13:05:06 +0800 Subject: [PATCH 04/39] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E5=BA=94=E7=94=A8/=E5=BA=94=E7=94=A8=E6=8E=88?= =?UTF-8?q?=E6=9D=83/acl=E7=AD=89=E6=9C=AA=E8=AE=B0=E5=BD=95=E6=97=A5?= =?UTF-8?q?=E5=BF=97=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/audits/signals_handler.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/apps/audits/signals_handler.py b/apps/audits/signals_handler.py index 853030287..c99c52bc9 100644 --- a/apps/audits/signals_handler.py +++ b/apps/audits/signals_handler.py @@ -27,11 +27,23 @@ json_render = JSONRenderer() MODELS_NEED_RECORD = ( - 'User', 'UserGroup', 'Asset', 'Node', 'AdminUser', 'SystemUser', - 'Domain', 'Gateway', 'Organization', 'AssetPermission', 'CommandFilter', - 'CommandFilterRule', 'License', 'Setting', 'Account', 'SyncInstanceTask', - 'Platform', 'ChangeAuthPlan', 'GatherUserTask', - 'RemoteApp', 'RemoteAppPermission', 'DatabaseApp', 'DatabaseAppPermission', + # users + 'User', 'UserGroup', + # acls + 'LoginACL', 'LoginAssetACL', + # assets + 'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule', + 'CommandFilter', 'Platform', + # applications + 'Application', + # orgs + 'Organization', + # settings + 'Setting', + # perms + 'AssetPermission', 'ApplicationPermission', + # xpack + 'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask', ) From 7b679f3e82ff7cbddbb19b4a8c215a14f27d26ce Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 20 Apr 2021 11:27:02 +0800 Subject: [PATCH 05/39] =?UTF-8?q?fix(task):=20=E4=BF=AE=E5=A4=8D=E6=8E=A8?= =?UTF-8?q?=E9=80=81=E8=BF=87=E6=9C=9F=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(rdp): 修复下载rdp文件失败的问题 --- apps/authentication/api/connection_token.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index ae9e4a2de..d4d58e397 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -140,13 +140,13 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView # Todo: 上线后地址是 JumpServerAddr:3389 address = self.request.query_params.get('address') or '1.1.1.1' options['full address:s'] = address - options['username:s'] = '{}@{}'.format(user.username, token) + options['username:s'] = '{}|{}'.format(user.username, token) options['desktopwidth:i'] = width options['desktopheight:i'] = height data = '' for k, v in options.items(): data += f'{k}:{v}\n' - response = HttpResponse(data, content_type='text/plain') + response = HttpResponse(data, content_type='application/octet-stream') filename = "{}-{}-jumpserver.rdp".format(user.username, asset.hostname) response['Content-Disposition'] = 'attachment; filename={}'.format(filename) return response From 4563743f00e125459ca59292d0515918b859e68e Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 20 Apr 2021 16:17:18 +0800 Subject: [PATCH 06/39] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=B8=8B?= =?UTF-8?q?=E8=BD=BDrdp=E6=96=87=E4=BB=B6=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 --- apps/authentication/api/connection_token.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index ae9e4a2de..5da2f587a 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +import urllib.parse + from django.conf import settings from django.core.cache import cache from django.shortcuts import get_object_or_404 @@ -148,7 +150,8 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView data += f'{k}:{v}\n' response = HttpResponse(data, content_type='text/plain') filename = "{}-{}-jumpserver.rdp".format(user.username, asset.hostname) - response['Content-Disposition'] = 'attachment; filename={}'.format(filename) + filename = urllib.parse.quote(filename) + response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename return response @staticmethod From 65ad63272c72f589416f750952617e20fa6f6768 Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 20 Apr 2021 16:44:14 +0800 Subject: [PATCH 07/39] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E5=BA=94=E7=94=A8/=E5=BA=94=E7=94=A8=E6=8E=88?= =?UTF-8?q?=E6=9D=83/acl=E7=AD=89=E6=9C=AA=E8=AE=B0=E5=BD=95=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E7=9A=84=E9=97=AE=E9=A2=982?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/acls/models/login_acl.py | 3 +++ apps/acls/models/login_asset_acl.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/apps/acls/models/login_acl.py b/apps/acls/models/login_acl.py index 44d100098..323599f4c 100644 --- a/apps/acls/models/login_acl.py +++ b/apps/acls/models/login_acl.py @@ -33,6 +33,9 @@ class LoginACL(BaseACL): class Meta: ordering = ('priority', '-date_updated', 'name') + def __str__(self): + return self.name + @property def action_reject(self): return self.action == self.ActionChoices.reject diff --git a/apps/acls/models/login_asset_acl.py b/apps/acls/models/login_asset_acl.py index 162665c83..8ac140370 100644 --- a/apps/acls/models/login_asset_acl.py +++ b/apps/acls/models/login_asset_acl.py @@ -38,6 +38,9 @@ class LoginAssetACL(BaseACL, OrgModelMixin): unique_together = ('name', 'org_id') ordering = ('priority', '-date_updated', 'name') + def __str__(self): + return self.name + @classmethod def filter(cls, user, asset, system_user, action): queryset = cls.objects.filter(action=action) From ce8143c2ec96eed612ccce7ecdd851b11fe8c85d Mon Sep 17 00:00:00 2001 From: Bai Date: Fri, 23 Apr 2021 16:27:12 +0800 Subject: [PATCH 08/39] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9ACL=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E6=94=AF=E6=8C=81=E7=9A=84=E5=8D=8F=E8=AE=AE=E4=B8=BA?= =?UTF-8?q?:=20ssh=E3=80=81telnet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/acls/serializers/login_asset_acl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/acls/serializers/login_asset_acl.py b/apps/acls/serializers/login_asset_acl.py index 842cbc8a0..f451a912a 100644 --- a/apps/acls/serializers/login_asset_acl.py +++ b/apps/acls/serializers/login_asset_acl.py @@ -54,7 +54,7 @@ class LoginAssetACLSystemUsersSerializer(serializers.Serializer): protocol_group = serializers.ListField( default=['*'], child=serializers.CharField(max_length=16), label=_('Protocol'), help_text=protocol_group_help_text.format( - ', '.join(SystemUser.ASSET_CATEGORY_PROTOCOLS) + ', '.join([SystemUser.PROTOCOL_SSH, SystemUser.PROTOCOL_TELNET]) ) ) From c4727e1eba8538a69983c20e587eb80e8330f14c Mon Sep 17 00:00:00 2001 From: "fghbng@qq.com" Date: Sun, 25 Apr 2021 14:58:06 +0800 Subject: [PATCH 09/39] =?UTF-8?q?=E3=80=90=E4=BB=AA=E8=A1=A8=E7=9B=98?= =?UTF-8?q?=E3=80=91=E5=9C=A8=E7=BA=BF=E7=94=A8=E6=88=B7=E6=95=B0=E4=B8=8D?= =?UTF-8?q?=E5=AF=B9=EF=BC=8C=EF=BC=88=E8=BF=9E=E4=B8=8Awindows=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E4=B9=8B=E5=90=8E=EF=BC=8C=E5=9C=A8=E7=BA=BF=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=95=B0=E5=B0=B1=E4=B8=8D=E5=AF=B9=E4=BA=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/api.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index bda2537c8..0f0193c49 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -99,7 +99,7 @@ class DatesLoginMetricMixin: if count is not None: return count ds, de = self.get_date_start_2_end(date) - count = len(set(Session.objects.filter(date_start__range=(ds, de)).values_list('user', flat=True))) + count = len(set(Session.objects.filter(date_start__range=(ds, de)).values_list('user_id', flat=True))) self.__set_data_to_cache(date, tp, count) return count @@ -129,7 +129,7 @@ class DatesLoginMetricMixin: @lazyproperty def dates_total_count_active_users(self): - count = len(set(self.sessions_queryset.values_list('user', flat=True))) + count = len(set(self.sessions_queryset.values_list('user_id', flat=True))) return count @lazyproperty @@ -161,10 +161,10 @@ class DatesLoginMetricMixin: @lazyproperty def dates_total_count_disabled_assets(self): return Asset.objects.filter(is_active=False).count() - + # 以下是从week中而来 def get_dates_login_times_top5_users(self): - users = self.sessions_queryset.values_list('user', flat=True) + users = self.sessions_queryset.values_list('user_id', flat=True) users = [ {'user': user, 'total': total} for user, total in Counter(users).most_common(5) @@ -172,7 +172,7 @@ class DatesLoginMetricMixin: return users def get_dates_total_count_login_users(self): - return len(set(self.sessions_queryset.values_list('user', flat=True))) + return len(set(self.sessions_queryset.values_list('user_id', flat=True))) def get_dates_total_count_login_times(self): return self.sessions_queryset.count() @@ -186,8 +186,8 @@ class DatesLoginMetricMixin: return list(assets) def get_dates_login_times_top10_users(self): - users = self.sessions_queryset.values("user") \ - .annotate(total=Count("user")) \ + users = self.sessions_queryset.values("user_id") \ + .annotate(total=Count("user_id")) \ .annotate(last=Max("date_start")).order_by("-total")[:10] for user in users: user['last'] = str(user['last']) @@ -221,7 +221,7 @@ class TotalCountMixin: @staticmethod def get_total_count_online_users(): - count = len(set(Session.objects.filter(is_finished=False).values_list('user', flat=True))) + count = len(set(Session.objects.filter(is_finished=False).values_list('user_id', flat=True))) return count @staticmethod From d9552c00382f79a0b18f6ff08af7884e797695d7 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 25 Apr 2021 18:13:41 +0800 Subject: [PATCH 10/39] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E5=85=AC?= =?UTF-8?q?=E9=92=A5=E8=AE=BE=E7=BD=AE=EF=BC=8C=E8=AE=A9=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E9=80=89=E6=8B=A9=E6=98=AF=E5=90=A6=E5=BC=80?= =?UTF-8?q?=E5=90=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/conf.py | 1 + apps/jumpserver/settings/custom.py | 1 + apps/users/models/user.py | 11 ++++++++++- apps/users/serializers/profile.py | 4 +++- apps/users/serializers/user.py | 3 ++- 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 179376828..91f3da1cb 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -222,6 +222,7 @@ class Config(dict): 'TERMINAL_PASSWORD_AUTH': True, 'TERMINAL_PUBLIC_KEY_AUTH': True, + 'TERMINAL_ONLY_SOURCE_LOCAL_CAN_PUBLIC_KEY_AUTH': True, 'TERMINAL_HEARTBEAT_INTERVAL': 20, 'TERMINAL_ASSET_LIST_SORT_BY': 'hostname', 'TERMINAL_ASSET_LIST_PAGE_SIZE': 'auto', diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 936b27582..31b5d52ac 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -124,3 +124,4 @@ FORGOT_PASSWORD_URL = CONFIG.FORGOT_PASSWORD_URL # 自定义默认组织名 GLOBAL_ORG_DISPLAY_NAME = CONFIG.GLOBAL_ORG_DISPLAY_NAME HEALTH_CHECK_TOKEN = CONFIG.HEALTH_CHECK_TOKEN +TERMINAL_ONLY_SOURCE_LOCAL_CAN_PUBLIC_KEY_AUTH = CONFIG.TERMINAL_ONLY_SOURCE_LOCAL_CAN_PUBLIC_KEY_AUTH diff --git a/apps/users/models/user.py b/apps/users/models/user.py index fab14e252..b10c07223 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -5,6 +5,7 @@ import uuid import base64 import string import random +import datetime from django.conf import settings from django.contrib.auth.models import AbstractUser @@ -32,6 +33,9 @@ logger = get_logger(__file__) class AuthMixin: + date_password_last_updated: datetime.datetime + is_local: bool + @property def password_raw(self): raise AttributeError('Password raw is not a readable attribute') @@ -63,7 +67,12 @@ class AuthMixin: return self.can_use_ssh_key_login() def can_use_ssh_key_login(self): - return self.is_local and settings.TERMINAL_PUBLIC_KEY_AUTH + if not settings.TERMINAL_PUBLIC_KEY_AUTH: + return False + if self.is_local or settings.TERMINAL_ONLY_SOURCE_LOCAL_CAN_PUBLIC_KEY_AUTH: + return True + else: + return False def is_public_key_valid(self): """ diff --git a/apps/users/serializers/profile.py b/apps/users/serializers/profile.py index 1c5e99873..68e387245 100644 --- a/apps/users/serializers/profile.py +++ b/apps/users/serializers/profile.py @@ -101,7 +101,8 @@ class UserProfileSerializer(UserSerializer): class Meta(UserSerializer.Meta): fields = UserSerializer.Meta.fields + [ - 'public_key_comment', 'public_key_hash_md5', 'admin_or_audit_orgs', 'current_org_roles', + 'public_key_comment', 'public_key_hash_md5', + 'admin_or_audit_orgs', 'current_org_roles', 'guide_url', 'user_all_orgs' ] read_only_fields = [ @@ -164,6 +165,7 @@ class ChangeUserPasswordSerializer(serializers.ModelSerializer): model = User fields = ['password'] + class ResetOTPSerializer(serializers.Serializer): msg = serializers.CharField(read_only=True) diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 0d8e784ed..c63c6264d 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -34,6 +34,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): is_expired = serializers.BooleanField(read_only=True, label=_('Is expired')) can_update = serializers.SerializerMethodField(label=_('Can update')) can_delete = serializers.SerializerMethodField(label=_('Can delete')) + can_public_key_auth = serializers.ReadOnlyField(source='can_use_ssh_key_login') org_roles = serializers.ListField( label=_('Organization role name'), allow_null=True, required=False, child=serializers.ChoiceField(choices=ORG_ROLE.choices), default=["User"] @@ -48,7 +49,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): 'password', 'email', 'public_key', 'wechat', 'phone', 'mfa_level', 'mfa_enabled', 'mfa_level_display', 'mfa_force_enabled', 'role_display', 'org_role_display', 'total_role_display', 'comment', 'source', 'is_valid', 'is_expired', - 'is_active', 'created_by', 'is_first_login', + 'is_active', 'created_by', 'is_first_login', 'can_public_key_auth', 'password_strategy', 'date_password_last_updated', 'date_expired', 'avatar_url', 'source_display', 'date_joined', 'last_login' ] From ab0fda93f61abce65d4ccf351f0d219d08fa4731 Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 26 Apr 2021 10:21:22 +0800 Subject: [PATCH 11/39] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E5=85=AC?= =?UTF-8?q?=E9=92=A5=E8=AE=BE=E7=BD=AE=EF=BC=8C=E5=B9=B6=E5=88=A0=E6=8E=89?= =?UTF-8?q?=E4=B8=80=E9=83=A8=E5=88=86=E4=B8=8D=E7=94=A8=E7=9A=84=20html?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/conf.py | 1 - apps/jumpserver/settings/custom.py | 1 - apps/locale/zh/LC_MESSAGES/django.mo | Bin 74719 -> 70857 bytes apps/locale/zh/LC_MESSAGES/django.po | 797 +++++++----------- apps/settings/serializers/settings.py | 6 +- apps/users/models/user.py | 10 +- apps/users/templates/users/_user.html | 88 -- .../templates/users/user_bulk_update.html | 70 -- apps/users/templates/users/user_create.html | 100 --- apps/users/templates/users/user_detail.html | 550 ------------ .../templates/users/user_granted_asset.html | 26 - .../users/user_granted_database_app.html | 100 --- .../users/user_granted_remote_app.html | 93 -- .../users/user_group_create_update.html | 69 -- .../templates/users/user_group_detail.html | 168 ---- .../users/user_group_granted_asset.html | 47 -- .../templates/users/user_group_list.html | 112 --- apps/users/templates/users/user_list.html | 280 ------ apps/users/templates/users/user_profile.html | 233 ----- .../templates/users/user_profile_update.html | 84 -- .../templates/users/user_pubkey_update.html | 102 --- .../users/user_remote_app_permission.html | 168 ---- apps/users/templates/users/user_update.html | 117 --- 23 files changed, 308 insertions(+), 2914 deletions(-) delete mode 100644 apps/users/templates/users/_user.html delete mode 100644 apps/users/templates/users/user_bulk_update.html delete mode 100644 apps/users/templates/users/user_create.html delete mode 100644 apps/users/templates/users/user_detail.html delete mode 100644 apps/users/templates/users/user_granted_asset.html delete mode 100644 apps/users/templates/users/user_granted_database_app.html delete mode 100644 apps/users/templates/users/user_granted_remote_app.html delete mode 100644 apps/users/templates/users/user_group_create_update.html delete mode 100644 apps/users/templates/users/user_group_detail.html delete mode 100644 apps/users/templates/users/user_group_granted_asset.html delete mode 100644 apps/users/templates/users/user_group_list.html delete mode 100644 apps/users/templates/users/user_list.html delete mode 100644 apps/users/templates/users/user_profile.html delete mode 100644 apps/users/templates/users/user_profile_update.html delete mode 100644 apps/users/templates/users/user_pubkey_update.html delete mode 100644 apps/users/templates/users/user_remote_app_permission.html delete mode 100644 apps/users/templates/users/user_update.html diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 91f3da1cb..179376828 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -222,7 +222,6 @@ class Config(dict): 'TERMINAL_PASSWORD_AUTH': True, 'TERMINAL_PUBLIC_KEY_AUTH': True, - 'TERMINAL_ONLY_SOURCE_LOCAL_CAN_PUBLIC_KEY_AUTH': True, 'TERMINAL_HEARTBEAT_INTERVAL': 20, 'TERMINAL_ASSET_LIST_SORT_BY': 'hostname', 'TERMINAL_ASSET_LIST_PAGE_SIZE': 'auto', diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 31b5d52ac..936b27582 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -124,4 +124,3 @@ FORGOT_PASSWORD_URL = CONFIG.FORGOT_PASSWORD_URL # 自定义默认组织名 GLOBAL_ORG_DISPLAY_NAME = CONFIG.GLOBAL_ORG_DISPLAY_NAME HEALTH_CHECK_TOKEN = CONFIG.HEALTH_CHECK_TOKEN -TERMINAL_ONLY_SOURCE_LOCAL_CAN_PUBLIC_KEY_AUTH = CONFIG.TERMINAL_ONLY_SOURCE_LOCAL_CAN_PUBLIC_KEY_AUTH diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 4e146d83b81d1f910f5ed64ed4565a09bf68c181..78b235d0e6cc1d514a632be08d795827d4c719e6 100644 GIT binary patch delta 21245 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 delta 24831 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 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 49694c4a0..e24c71c54 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-04-14 17:52+0800\n" +"POT-Creation-Date: 2021-04-26 10:14+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -25,20 +25,11 @@ msgstr "" #: orgs/models.py:23 perms/models/base.py:49 settings/models.py:29 #: terminal/models/storage.py:23 terminal/models/storage.py:90 #: terminal/models/task.py:16 terminal/models/terminal.py:100 -#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:530 +#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:535 #: users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 #: users/templates/users/user_database_app_permission.html:36 -#: users/templates/users/user_detail.html:49 -#: users/templates/users/user_granted_database_app.html:34 -#: users/templates/users/user_granted_remote_app.html:34 -#: users/templates/users/user_group_detail.html:50 -#: users/templates/users/user_group_list.html:14 -#: users/templates/users/user_list.html:14 -#: users/templates/users/user_profile.html:51 -#: users/templates/users/user_pubkey_update.html:57 -#: users/templates/users/user_remote_app_permission.html:36 #: xpack/plugins/cloud/models.py:28 msgid "Name" msgstr "名称" @@ -56,8 +47,6 @@ msgstr "优先级可选范围为 1-100 (数值越小越优先)" #: acls/models/base.py:31 authentication/models.py:20 #: authentication/templates/authentication/_access_key_modal.html:32 #: perms/models/base.py:52 users/templates/users/_select_user_modal.html:18 -#: users/templates/users/user_detail.html:132 -#: users/templates/users/user_profile.html:63 msgid "Active" msgstr "激活中" @@ -72,13 +61,7 @@ msgstr "激活中" #: orgs/models.py:26 perms/models/base.py:57 settings/models.py:34 #: terminal/models/storage.py:29 terminal/models/storage.py:96 #: terminal/models/terminal.py:114 tickets/models/ticket.py:73 -#: users/models/group.py:16 users/models/user.py:563 -#: users/templates/users/user_detail.html:115 -#: users/templates/users/user_granted_database_app.html:38 -#: users/templates/users/user_granted_remote_app.html:37 -#: users/templates/users/user_group_detail.html:62 -#: users/templates/users/user_group_list.html:16 -#: users/templates/users/user_profile.html:138 +#: users/models/group.py:16 users/models/user.py:568 #: xpack/plugins/change_auth_plan/models.py:77 xpack/plugins/cloud/models.py:35 #: xpack/plugins/cloud/models.py:98 xpack/plugins/gathered_user/models.py:26 msgid "Comment" @@ -104,9 +87,6 @@ msgstr "登录IP" #: users/templates/users/user_asset_permission.html:44 #: users/templates/users/user_asset_permission.html:79 #: users/templates/users/user_database_app_permission.html:42 -#: users/templates/users/user_group_list.html:17 -#: users/templates/users/user_list.html:20 -#: users/templates/users/user_remote_app_permission.html:42 msgid "Action" msgstr "动作" @@ -118,22 +98,17 @@ msgstr "动作" #: perms/models/base.py:50 templates/index.html:78 #: terminal/backends/command/models.py:18 #: terminal/backends/command/serializers.py:12 terminal/models/session.py:37 -#: tickets/models/comment.py:17 users/models/user.py:159 -#: users/models/user.py:707 users/serializers/group.py:20 +#: tickets/models/comment.py:17 users/models/user.py:164 +#: users/models/user.py:712 users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 #: users/templates/users/user_database_app_permission.html:37 #: users/templates/users/user_database_app_permission.html:58 -#: users/templates/users/user_group_detail.html:73 -#: users/templates/users/user_group_list.html:15 -#: users/templates/users/user_list.html:135 -#: users/templates/users/user_remote_app_permission.html:37 -#: users/templates/users/user_remote_app_permission.html:58 msgid "User" msgstr "用户" #: acls/models/login_asset_acl.py:17 authentication/models.py:72 -#: tickets/const.py:9 users/templates/users/user_detail.html:250 +#: tickets/const.py:9 msgid "Login confirm" msgstr "登录复核" @@ -152,18 +127,16 @@ msgstr "系统用户" #: terminal/backends/command/serializers.py:13 terminal/models/session.py:39 #: users/templates/users/user_asset_permission.html:40 #: users/templates/users/user_asset_permission.html:70 -#: users/templates/users/user_granted_remote_app.html:36 #: xpack/plugins/change_auth_plan/models.py:282 #: xpack/plugins/cloud/models.py:202 msgid "Asset" msgstr "资产" #: acls/models/login_asset_acl.py:32 authentication/models.py:45 -#: users/templates/users/user_detail.html:258 msgid "Reviewers" msgstr "审批人" -#: acls/models/login_asset_acl.py:86 tickets/const.py:12 +#: acls/models/login_asset_acl.py:89 tickets/const.py:12 msgid "Login asset confirm" msgstr "登录资产复核" @@ -205,11 +178,8 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " #: applications/serializers/attrs/application_type/vmware_client.py:26 #: assets/models/base.py:251 assets/models/gathered_user.py:15 #: audits/models.py:100 authentication/forms.py:15 authentication/forms.py:17 -#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:528 +#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:533 #: users/templates/users/_select_user_modal.html:14 -#: users/templates/users/user_detail.html:53 -#: users/templates/users/user_list.html:15 -#: users/templates/users/user_profile.html:47 #: xpack/plugins/change_auth_plan/models.py:47 #: xpack/plugins/change_auth_plan/models.py:278 #: xpack/plugins/cloud/serializers.py:51 @@ -261,7 +231,6 @@ msgstr "所有复核人都不属于组织 `{}`" #: applications/const.py:9 #: applications/serializers/attrs/application_category/db.py:14 #: applications/serializers/attrs/application_type/mysql_workbench.py:26 -#: users/templates/users/user_granted_database_app.html:37 msgid "Database" msgstr "数据库" @@ -288,7 +257,6 @@ msgstr "类别" #: terminal/models/storage.py:26 terminal/models/storage.py:93 #: tickets/models/ticket.py:38 #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:27 -#: users/templates/users/user_granted_database_app.html:35 msgid "Type" msgstr "类型" @@ -315,9 +283,7 @@ msgid "Cluster" msgstr "集群" #: applications/serializers/attrs/application_category/db.py:11 -#: ops/models/adhoc.py:146 -#: users/templates/users/user_granted_database_app.html:36 -#: xpack/plugins/cloud/serializers.py:49 +#: ops/models/adhoc.py:146 xpack/plugins/cloud/serializers.py:49 msgid "Host" msgstr "主机" @@ -348,14 +314,12 @@ msgstr "目标URL" #: applications/serializers/attrs/application_type/mysql_workbench.py:34 #: applications/serializers/attrs/application_type/vmware_client.py:30 #: assets/models/base.py:252 assets/serializers/asset_user.py:71 -#: audits/signals_handler.py:46 authentication/forms.py:22 +#: audits/signals_handler.py:58 authentication/forms.py:22 #: authentication/templates/authentication/login.html:155 #: settings/serializers/settings.py:93 users/forms/profile.py:21 #: users/templates/users/user_otp_check_password.html:13 #: users/templates/users/user_password_update.html:43 #: users/templates/users/user_password_verify.html:18 -#: users/templates/users/user_profile_update.html:41 -#: users/templates/users/user_pubkey_update.html:41 #: users/templates/users/user_update.html:20 #: xpack/plugins/change_auth_plan/models.py:68 #: xpack/plugins/change_auth_plan/models.py:190 @@ -518,9 +482,8 @@ msgstr "标签管理" #: assets/models/cluster.py:28 assets/models/cmd_filter.py:26 #: assets/models/cmd_filter.py:60 assets/models/group.py:21 #: common/db/models.py:70 common/mixins/models.py:49 orgs/models.py:24 -#: orgs/models.py:422 perms/models/base.py:55 users/models/user.py:571 -#: users/serializers/group.py:35 users/templates/users/user_detail.html:97 -#: xpack/plugins/change_auth_plan/models.py:81 +#: orgs/models.py:422 perms/models/base.py:55 users/models/user.py:576 +#: users/serializers/group.py:35 xpack/plugins/change_auth_plan/models.py:81 #: xpack/plugins/cloud/models.py:104 xpack/plugins/gathered_user/models.py:30 msgid "Created by" msgstr "创建者" @@ -533,7 +496,6 @@ 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:25 #: orgs/models.py:420 perms/models/base.py:56 users/models/group.py:18 -#: users/templates/users/user_group_detail.html:58 #: xpack/plugins/cloud/models.py:107 msgid "Date created" msgstr "创建日期" @@ -580,8 +542,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:549 -#: users/templates/users/user_detail.html:62 +#: assets/models/cluster.py:22 users/models/user.py:554 msgid "Phone" msgstr "手机" @@ -607,7 +568,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:719 +#: users/models/user.py:724 msgid "System" msgstr "系统" @@ -734,8 +695,7 @@ msgstr "资产" msgid "Users" msgstr "用户管理" -#: assets/models/user.py:121 users/templates/users/user_group_list.html:90 -#: users/templates/users/user_profile.html:124 +#: assets/models/user.py:121 msgid "User groups" msgstr "用户组" @@ -782,8 +742,6 @@ msgstr "用户组" #: users/templates/users/user_asset_permission.html:159 #: users/templates/users/user_database_app_permission.html:40 #: users/templates/users/user_database_app_permission.html:67 -#: users/templates/users/user_remote_app_permission.html:40 -#: users/templates/users/user_remote_app_permission.html:67 msgid "System user" msgstr "系统用户" @@ -847,14 +805,11 @@ msgid "Backend" msgstr "后端" #: assets/serializers/asset_user.py:75 users/forms/profile.py:160 -#: users/models/user.py:560 users/templates/users/user_password_update.html:48 -#: users/templates/users/user_profile.html:69 -#: users/templates/users/user_profile_update.html:46 -#: users/templates/users/user_pubkey_update.html:46 +#: users/models/user.py:565 users/templates/users/user_password_update.html:48 msgid "Public key" msgstr "SSH公钥" -#: assets/serializers/asset_user.py:79 users/models/user.py:557 +#: assets/serializers/asset_user.py:79 users/models/user.py:562 msgid "Private key" msgstr "ssh私钥" @@ -984,25 +939,25 @@ msgid "" "The task of self-checking is already running and cannot be started repeatedly" msgstr "自检程序已经在运行,不能重复启动" -#: assets/tasks/push_system_user.py:192 +#: assets/tasks/push_system_user.py:193 #: assets/tasks/system_user_connectivity.py:89 msgid "System user is dynamic: {}" msgstr "系统用户是动态的: {}" -#: assets/tasks/push_system_user.py:232 +#: assets/tasks/push_system_user.py:233 msgid "Start push system user for platform: [{}]" msgstr "推送系统用户到平台: [{}]" -#: assets/tasks/push_system_user.py:233 +#: assets/tasks/push_system_user.py:234 #: assets/tasks/system_user_connectivity.py:81 msgid "Hosts count: {}" msgstr "主机数量: {}" -#: assets/tasks/push_system_user.py:272 assets/tasks/push_system_user.py:298 +#: assets/tasks/push_system_user.py:273 assets/tasks/push_system_user.py:299 msgid "Push system users to assets: {}" msgstr "推送系统用户到入资产: {}" -#: assets/tasks/push_system_user.py:284 +#: assets/tasks/push_system_user.py:285 msgid "Push system users to asset: {}({}) => {}" msgstr "推送系统用户到入资产: {}({}) => {}" @@ -1042,12 +997,6 @@ msgstr "没有匹配到资产,结束任务" #: authentication/templates/authentication/_access_key_modal.html:65 #: users/templates/users/user_asset_permission.html:128 #: users/templates/users/user_database_app_permission.html:111 -#: users/templates/users/user_detail.html:16 -#: users/templates/users/user_group_detail.html:27 -#: users/templates/users/user_group_list.html:53 -#: users/templates/users/user_list.html:94 -#: users/templates/users/user_list.html:98 -#: users/templates/users/user_remote_app_permission.html:111 msgid "Delete" msgstr "删除" @@ -1089,7 +1038,6 @@ msgid "Filename" msgstr "文件名" #: audits/models.py:42 audits/models.py:96 -#: users/templates/users/user_detail.html:487 msgid "Success" msgstr "成功" @@ -1114,15 +1062,6 @@ msgstr "创建" #: templates/_csv_update_modal.html:6 #: users/templates/users/user_asset_permission.html:127 #: users/templates/users/user_database_app_permission.html:110 -#: users/templates/users/user_detail.html:12 -#: users/templates/users/user_group_detail.html:23 -#: users/templates/users/user_group_list.html:51 -#: users/templates/users/user_list.html:84 -#: users/templates/users/user_list.html:87 -#: users/templates/users/user_profile.html:181 -#: users/templates/users/user_profile.html:191 -#: users/templates/users/user_profile.html:201 -#: users/templates/users/user_remote_app_permission.html:110 msgid "Update" msgstr "更新" @@ -1142,12 +1081,11 @@ msgstr "日期" msgid "Change by" msgstr "修改者" -#: audits/models.py:90 users/templates/users/user_detail.html:84 +#: audits/models.py:90 msgid "Disabled" msgstr "禁用" #: audits/models.py:91 settings/models.py:33 -#: users/templates/users/user_detail.html:82 msgid "Enabled" msgstr "启用" @@ -1180,9 +1118,8 @@ msgstr "用户代理" #: audits/models.py:105 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/login_otp.html:6 -#: users/forms/profile.py:64 users/models/user.py:552 -#: users/serializers/profile.py:99 users/templates/users/user_detail.html:77 -#: users/templates/users/user_profile.html:87 +#: users/forms/profile.py:64 users/models/user.py:557 +#: users/serializers/profile.py:99 msgid "MFA" msgstr "多因子认证" @@ -1250,11 +1187,11 @@ msgstr "运行用户(显示名称)" msgid "User for display" msgstr "用户(显示名称)" -#: audits/signals_handler.py:45 +#: audits/signals_handler.py:57 msgid "SSH Key" msgstr "SSH 密钥" -#: audits/signals_handler.py:47 +#: audits/signals_handler.py:59 msgid "SSO" msgstr "" @@ -1465,18 +1402,13 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:445 users/serializers/profile.py:96 -#: users/templates/users/user_profile.html:94 -#: users/templates/users/user_profile.html:163 -#: users/templates/users/user_profile.html:166 +#: users/models/user.py:450 users/serializers/profile.py:96 #: users/templates/users/user_verify_mfa.html:32 msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:446 users/serializers/profile.py:97 -#: users/templates/users/user_profile.html:92 -#: users/templates/users/user_profile.html:170 +#: users/models/user.py:451 users/serializers/profile.py:97 msgid "Enable" msgstr "启用" @@ -1508,14 +1440,7 @@ msgid "Need MFA for view auth" msgstr "需要多因子认证来查看账号信息" #: authentication/templates/authentication/_mfa_confirm_modal.html:20 -#: templates/_modal.html:23 users/templates/users/user_detail.html:264 -#: users/templates/users/user_detail.html:417 -#: users/templates/users/user_detail.html:443 -#: users/templates/users/user_detail.html:466 -#: users/templates/users/user_detail.html:511 -#: users/templates/users/user_group_create_update.html:28 -#: users/templates/users/user_list.html:184 -#: users/templates/users/user_password_verify.html:20 +#: templates/_modal.html:23 users/templates/users/user_password_verify.html:20 msgid "Confirm" msgstr "确认" @@ -1826,7 +1751,7 @@ msgstr "再次执行" msgid "Become" msgstr "Become" -#: ops/models/adhoc.py:150 users/templates/users/user_group_detail.html:54 +#: ops/models/adhoc.py:150 msgid "Create by" msgstr "创建者" @@ -1936,11 +1861,8 @@ msgstr "组织审计员" msgid "GLOBAL" msgstr "全局组织" -#: orgs/models.py:419 users/models/user.py:540 +#: orgs/models.py:419 users/models/user.py:545 #: users/templates/users/_select_user_modal.html:15 -#: users/templates/users/user_detail.html:73 -#: users/templates/users/user_list.html:16 -#: users/templates/users/user_profile.html:55 msgid "Role" msgstr "角色" @@ -1952,7 +1874,7 @@ msgstr "管理员正在修改授权,请稍等" msgid "The authorization cannot be revoked for the time being" msgstr "该授权暂时不能撤销" -#: perms/models/application_permission.py:27 users/models/user.py:160 +#: perms/models/application_permission.py:27 users/models/user.py:165 msgid "Application" msgstr "应用程序" @@ -2011,15 +1933,11 @@ msgid "Favorite" msgstr "收藏夹" #: perms/models/base.py:51 templates/_nav.html:21 users/models/group.py:31 -#: users/models/user.py:536 users/templates/users/_select_user_modal.html:16 +#: users/models/user.py:541 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 #: users/templates/users/user_database_app_permission.html:61 -#: users/templates/users/user_detail.html:209 -#: users/templates/users/user_list.html:17 -#: users/templates/users/user_remote_app_permission.html:38 -#: users/templates/users/user_remote_app_permission.html:61 msgid "User group" msgstr "用户组" @@ -2028,8 +1946,7 @@ msgstr "用户组" #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:77 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:43 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:81 -#: users/models/user.py:568 users/templates/users/user_detail.html:93 -#: users/templates/users/user_profile.html:120 +#: users/models/user.py:573 msgid "Date expired" msgstr "失效日期" @@ -2041,11 +1958,11 @@ msgstr "应用列表中包含与授权类型不同的应用。({})" #: perms/serializers/asset/permission.py:43 #: perms/serializers/asset/permission.py:61 users/serializers/user.py:34 -#: users/serializers/user.py:69 +#: users/serializers/user.py:70 msgid "Is expired" msgstr "是否过期" -#: perms/serializers/asset/permission.py:62 users/serializers/user.py:68 +#: perms/serializers/asset/permission.py:62 users/serializers/user.py:69 msgid "Is valid" msgstr "账户是否有效" @@ -2079,7 +1996,6 @@ msgid "Imported {} users successfully" msgstr "导入 {} 个用户成功" #: settings/models.py:123 users/templates/users/reset_password.html:29 -#: users/templates/users/user_profile.html:20 msgid "Setting" msgstr "设置" @@ -2262,23 +2178,29 @@ msgstr "自动" msgid "Password auth" msgstr "密码认证" -#: settings/serializers/settings.py:124 +#: settings/serializers/settings.py:125 msgid "Public key auth" msgstr "密钥认证" -#: settings/serializers/settings.py:125 +#: settings/serializers/settings.py:126 +msgid "" +"Tips: If use other auth method, like AD/LDAP, you should disable this to " +"avoid being able to log in after deleting" +msgstr "提示:如果你使用其它认证方式,如 AD/LDAP,你应该禁用此项,以避免第三方系统删除后,还可以登录" + +#: settings/serializers/settings.py:129 msgid "List sort by" msgstr "资产列表排序" -#: settings/serializers/settings.py:126 +#: settings/serializers/settings.py:130 msgid "List page size" msgstr "资产列表每页数量" -#: settings/serializers/settings.py:128 +#: settings/serializers/settings.py:132 msgid "Session keep duration" msgstr "会话日志保存时间" -#: settings/serializers/settings.py:129 +#: settings/serializers/settings.py:133 msgid "" "Units: days, Session, record, command will be delete if more than duration, " "only in database" @@ -2286,64 +2208,64 @@ msgstr "" "单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不" "受影响)" -#: settings/serializers/settings.py:131 +#: settings/serializers/settings.py:135 msgid "Telnet login regex" msgstr "Telnet 成功正则表达式" -#: settings/serializers/settings.py:136 +#: settings/serializers/settings.py:140 msgid "Global MFA auth" msgstr "全局启用 MFA 认证" -#: settings/serializers/settings.py:137 +#: settings/serializers/settings.py:141 msgid "All user enable MFA" msgstr "强制所有用户启用多因子认证" -#: settings/serializers/settings.py:140 +#: settings/serializers/settings.py:144 msgid "Batch command execution" msgstr "批量命令执行" -#: settings/serializers/settings.py:141 +#: settings/serializers/settings.py:145 msgid "Allow user run batch command or not using ansible" msgstr "是否允许用户使用 ansible 执行批量命令" -#: settings/serializers/settings.py:144 +#: settings/serializers/settings.py:148 msgid "Enable terminal register" msgstr "终端注册" -#: settings/serializers/settings.py:145 +#: settings/serializers/settings.py:149 msgid "" "Allow terminal register, after all terminal setup, you should disable this " "for security" msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭" -#: settings/serializers/settings.py:149 +#: settings/serializers/settings.py:153 msgid "Limit the number of login failures" msgstr "限制登录失败次数" -#: settings/serializers/settings.py:153 +#: settings/serializers/settings.py:157 msgid "Block logon interval" msgstr "禁止登录时间间隔" -#: settings/serializers/settings.py:154 +#: settings/serializers/settings.py:158 msgid "" "Tip: (unit/minute) if the user has failed to log in for a limited number of " "times, no login is allowed during this time interval." msgstr "" "提示:(单位:分)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录" -#: settings/serializers/settings.py:158 +#: settings/serializers/settings.py:162 msgid "Connection max idle time" msgstr "连接最大空闲时间" -#: settings/serializers/settings.py:159 +#: settings/serializers/settings.py:163 msgid "If idle time more than it, disconnect connection Unit: minute" msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)" -#: settings/serializers/settings.py:163 +#: settings/serializers/settings.py:167 msgid "User password expiration" msgstr "用户密码过期时间" -#: settings/serializers/settings.py:164 +#: settings/serializers/settings.py:168 msgid "" "Tip: (unit: day) If the user does not update the password during the time, " "the user password will expire failure;The password expiration reminder mail " @@ -2353,35 +2275,35 @@ msgstr "" "提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期" "提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户" -#: settings/serializers/settings.py:168 +#: settings/serializers/settings.py:172 msgid "Password minimum length" msgstr "密码最小长度" -#: settings/serializers/settings.py:171 +#: settings/serializers/settings.py:175 msgid "Must contain capital" msgstr "必须包含大写字符" -#: settings/serializers/settings.py:173 +#: settings/serializers/settings.py:177 msgid "Must contain lowercase" msgstr "必须包含小写字符" -#: settings/serializers/settings.py:174 +#: settings/serializers/settings.py:178 msgid "Must contain numeric" msgstr "必须包含数字" -#: settings/serializers/settings.py:175 +#: settings/serializers/settings.py:179 msgid "Must contain special" msgstr "必须包含特殊字符" -#: settings/serializers/settings.py:176 +#: settings/serializers/settings.py:180 msgid "Insecure command alert" msgstr "危险命令告警" -#: settings/serializers/settings.py:178 +#: settings/serializers/settings.py:182 msgid "Email recipient" msgstr "邮件收件人" -#: settings/serializers/settings.py:179 +#: settings/serializers/settings.py:183 msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" @@ -2532,12 +2454,7 @@ msgstr "商业支持" #: templates/_header_bar.html:70 templates/_nav.html:30 #: templates/_nav_user.html:37 users/forms/profile.py:43 -#: users/templates/users/_user.html:44 #: users/templates/users/user_password_update.html:39 -#: users/templates/users/user_profile.html:17 -#: users/templates/users/user_profile_update.html:37 -#: users/templates/users/user_profile_update.html:61 -#: users/templates/users/user_pubkey_update.html:37 msgid "Profile" msgstr "个人信息" @@ -2659,8 +2576,6 @@ msgid "Applications" msgstr "应用管理" #: templates/_nav.html:64 templates/_nav.html:82 templates/_nav_user.html:16 -#: users/templates/users/user_remote_app_permission.html:39 -#: users/templates/users/user_remote_app_permission.html:64 msgid "RemoteApp" msgstr "远程应用" @@ -3028,7 +2943,6 @@ msgid "High" msgstr "较高" #: terminal/const.py:33 users/templates/users/reset_password.html:50 -#: users/templates/users/user_create.html:35 #: users/templates/users/user_password_update.html:104 #: users/templates/users/user_update.html:57 msgid "Normal" @@ -3655,9 +3569,7 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:101 users/models/user.py:532 -#: users/templates/users/user_detail.html:57 -#: users/templates/users/user_profile.html:59 +#: users/forms/profile.py:101 users/models/user.py:537 msgid "Email" msgstr "邮件" @@ -3690,49 +3602,47 @@ msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" #: users/forms/profile.py:149 users/serializers/profile.py:71 -#: users/serializers/profile.py:144 users/serializers/profile.py:157 +#: users/serializers/profile.py:145 users/serializers/profile.py:158 msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/models/user.py:157 +#: users/models/user.py:162 msgid "System administrator" msgstr "系统管理员" -#: users/models/user.py:158 +#: users/models/user.py:163 msgid "System auditor" msgstr "系统审计员" -#: users/models/user.py:447 users/templates/users/user_profile.html:90 +#: users/models/user.py:452 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:512 +#: users/models/user.py:517 msgid "Local" msgstr "数据库" -#: users/models/user.py:543 +#: users/models/user.py:548 msgid "Avatar" msgstr "头像" -#: users/models/user.py:546 users/templates/users/user_detail.html:68 +#: users/models/user.py:551 msgid "Wechat" msgstr "微信" -#: users/models/user.py:576 users/templates/users/user_detail.html:89 -#: users/templates/users/user_list.html:18 -#: users/templates/users/user_profile.html:102 +#: users/models/user.py:581 msgid "Source" msgstr "用户来源" -#: users/models/user.py:580 +#: users/models/user.py:585 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:715 +#: users/models/user.py:720 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:718 +#: users/models/user.py:723 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -3740,7 +3650,7 @@ msgstr "Administrator是初始的超级管理员" msgid "The old password is incorrect" msgstr "旧密码错误" -#: users/serializers/profile.py:37 users/serializers/user.py:112 +#: users/serializers/profile.py:37 users/serializers/user.py:113 msgid "Password does not match security rules" msgstr "密码不满足安全规则" @@ -3748,7 +3658,7 @@ msgstr "密码不满足安全规则" msgid "The newly set password is inconsistent" msgstr "两次密码不一致" -#: users/serializers/profile.py:115 users/serializers/user.py:67 +#: users/serializers/profile.py:116 users/serializers/user.py:68 msgid "Is first login" msgstr "首次登录" @@ -3789,35 +3699,35 @@ msgstr "是否可更新" msgid "Can delete" msgstr "是否可删除" -#: users/serializers/user.py:38 users/serializers/user.py:74 +#: users/serializers/user.py:39 users/serializers/user.py:75 msgid "Organization role name" msgstr "组织角色名称" -#: users/serializers/user.py:70 +#: users/serializers/user.py:71 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:72 +#: users/serializers/user.py:73 msgid "Groups name" msgstr "用户组名" -#: users/serializers/user.py:73 +#: users/serializers/user.py:74 msgid "Source name" msgstr "用户来源名" -#: users/serializers/user.py:75 +#: users/serializers/user.py:76 msgid "Super role name" msgstr "超级角色名称" -#: users/serializers/user.py:76 +#: users/serializers/user.py:77 msgid "Total role name" msgstr "汇总角色名称" -#: users/serializers/user.py:100 +#: users/serializers/user.py:101 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/user.py:197 +#: users/serializers/user.py:198 msgid "name not unique" msgstr "名称重复" @@ -3825,9 +3735,8 @@ msgstr "名称重复" msgid "Security token validation" msgstr "安全令牌验证" -#: users/templates/users/_base_otp.html:14 users/templates/users/_user.html:13 -#: users/templates/users/user_profile_update.html:55 -#: xpack/plugins/cloud/models.py:78 xpack/plugins/cloud/serializers.py:145 +#: users/templates/users/_base_otp.html:14 xpack/plugins/cloud/models.py:78 +#: xpack/plugins/cloud/serializers.py:145 msgid "Account" msgstr "账户" @@ -3847,36 +3756,6 @@ msgstr "选择用户" msgid "Asset num" msgstr "资产数量" -#: users/templates/users/_user.html:21 -msgid "Auth" -msgstr "认证" - -#: users/templates/users/_user.html:27 -msgid "Security and Role" -msgstr "角色安全" - -#: users/templates/users/_user.html:51 -#: users/templates/users/user_bulk_update.html:23 -#: users/templates/users/user_detail.html:168 -#: users/templates/users/user_group_create_update.html:27 -#: users/templates/users/user_password_update.html:74 -#: users/templates/users/user_profile.html:209 -#: users/templates/users/user_profile_update.html:67 -#: users/templates/users/user_pubkey_update.html:74 -#: users/templates/users/user_pubkey_update.html:80 -msgid "Reset" -msgstr "重置" - -#: users/templates/users/_user.html:52 -#: users/templates/users/forgot_password.html:32 -#: users/templates/users/user_bulk_update.html:24 -#: users/templates/users/user_list.html:40 -#: users/templates/users/user_password_update.html:75 -#: users/templates/users/user_profile_update.html:68 -#: users/templates/users/user_pubkey_update.html:81 -msgid "Submit" -msgstr "提交" - #: users/templates/users/_user_detail_nav_header.html:11 msgid "User detail" msgstr "用户详情" @@ -3886,8 +3765,6 @@ msgid "User permissions" msgstr "用户授权" #: users/templates/users/_user_detail_nav_header.html:23 -#: users/templates/users/user_group_detail.html:20 -#: users/templates/users/user_group_granted_asset.html:21 msgid "Asset granted" msgstr "授权的资产" @@ -3932,56 +3809,53 @@ msgstr "获取更多信息" msgid "Input your email, that will send a mail to your" msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中" +#: users/templates/users/forgot_password.html:32 +#: users/templates/users/user_password_update.html:75 +msgid "Submit" +msgstr "提交" + #: users/templates/users/reset_password.html:5 -#: users/templates/users/reset_password.html:6 -#: users/templates/users/user_detail.html:402 users/utils.py:83 +#: users/templates/users/reset_password.html:6 users/utils.py:83 msgid "Reset password" msgstr "重置密码" #: users/templates/users/reset_password.html:23 -#: users/templates/users/user_create.html:13 #: users/templates/users/user_password_update.html:64 #: users/templates/users/user_update.html:13 msgid "Your password must satisfy" msgstr "您的密码必须满足:" #: users/templates/users/reset_password.html:24 -#: users/templates/users/user_create.html:14 #: users/templates/users/user_password_update.html:65 #: users/templates/users/user_update.html:14 msgid "Password strength" msgstr "密码强度:" #: users/templates/users/reset_password.html:48 -#: users/templates/users/user_create.html:33 #: users/templates/users/user_password_update.html:102 #: users/templates/users/user_update.html:55 msgid "Very weak" msgstr "很弱" #: users/templates/users/reset_password.html:49 -#: users/templates/users/user_create.html:34 #: users/templates/users/user_password_update.html:103 #: users/templates/users/user_update.html:56 msgid "Weak" msgstr "弱" #: users/templates/users/reset_password.html:51 -#: users/templates/users/user_create.html:36 #: users/templates/users/user_password_update.html:105 #: users/templates/users/user_update.html:58 msgid "Medium" msgstr "一般" #: users/templates/users/reset_password.html:52 -#: users/templates/users/user_create.html:37 #: users/templates/users/user_password_update.html:106 #: users/templates/users/user_update.html:59 msgid "Strong" msgstr "强" #: users/templates/users/reset_password.html:53 -#: users/templates/users/user_create.html:38 #: users/templates/users/user_password_update.html:107 #: users/templates/users/user_update.html:60 msgid "Very strong" @@ -3990,8 +3864,6 @@ msgstr "很强" #: users/templates/users/user_asset_permission.html:43 #: users/templates/users/user_asset_permission.html:155 #: users/templates/users/user_database_app_permission.html:41 -#: users/templates/users/user_list.html:19 -#: users/templates/users/user_remote_app_permission.html:41 #: xpack/plugins/cloud/models.py:34 msgid "Validity" msgstr "有效" @@ -4008,229 +3880,6 @@ msgstr "包含" msgid "Exclude" msgstr "不包含" -#: users/templates/users/user_bulk_update.html:8 -msgid "Select properties that need to be modified" -msgstr "选择需要修改属性" - -#: users/templates/users/user_bulk_update.html:10 -msgid "Select all" -msgstr "全选" - -#: users/templates/users/user_create.html:4 -#: users/templates/users/user_list.html:7 -msgid "Create user" -msgstr "创建用户" - -#: users/templates/users/user_detail.html:80 -msgid "Force enabled" -msgstr "强制启用" - -#: users/templates/users/user_detail.html:101 -#: users/templates/users/user_profile.html:106 -msgid "Date joined" -msgstr "创建日期" - -#: users/templates/users/user_detail.html:105 -#: users/templates/users/user_profile.html:110 -msgid "Last login" -msgstr "最后登录" - -#: users/templates/users/user_detail.html:110 -#: users/templates/users/user_profile.html:115 -msgid "Last password updated" -msgstr "最后更新密码" - -#: users/templates/users/user_detail.html:126 -#: users/templates/users/user_profile.html:150 -msgid "Quick modify" -msgstr "快速修改" - -#: users/templates/users/user_detail.html:148 -msgid "Force enabled MFA" -msgstr "强制启用多因子认证" - -#: users/templates/users/user_detail.html:165 -msgid "Reset MFA" -msgstr "重置多因子认证" - -#: users/templates/users/user_detail.html:174 -msgid "Send reset password mail" -msgstr "发送重置密码邮件" - -#: users/templates/users/user_detail.html:177 -#: users/templates/users/user_detail.html:187 -msgid "Send" -msgstr "发送" - -#: users/templates/users/user_detail.html:184 -msgid "Send reset ssh key mail" -msgstr "发送重置密钥邮件" - -#: users/templates/users/user_detail.html:193 -#: users/templates/users/user_detail.html:490 -msgid "Unblock user" -msgstr "解除登录限制" - -#: users/templates/users/user_detail.html:196 -msgid "Unblock" -msgstr "解除" - -#: users/templates/users/user_detail.html:217 -msgid "Join user groups" -msgstr "添加到用户组" - -#: users/templates/users/user_detail.html:226 -msgid "Join" -msgstr "加入" - -#: users/templates/users/user_detail.html:356 -#: users/templates/users/user_detail.html:383 -msgid "Update successfully!" -msgstr "更新成功" - -#: users/templates/users/user_detail.html:365 -msgid "Goto profile page enable MFA" -msgstr "请去个人信息页面启用自己的多因子认证" - -#: users/templates/users/user_detail.html:401 -msgid "An e-mail has been sent to the user`s mailbox." -msgstr "已发送邮件到用户邮箱" - -#: users/templates/users/user_detail.html:411 -#: users/templates/users/user_detail.html:437 -#: users/templates/users/user_detail.html:505 -#: users/templates/users/user_list.html:178 -msgid "Are you sure?" -msgstr "你确认吗?" - -#: users/templates/users/user_detail.html:412 -msgid "This will reset the user password and send a reset mail" -msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" - -#: users/templates/users/user_detail.html:415 -#: users/templates/users/user_detail.html:441 -#: users/templates/users/user_detail.html:509 -#: users/templates/users/user_list.html:182 -msgid "Cancel" -msgstr "取消" - -#: users/templates/users/user_detail.html:427 -msgid "" -"The reset-ssh-public-key E-mail has been sent successfully. Please inform " -"the user to update his new ssh public key." -msgstr "重设密钥邮件将会发送到用户邮箱" - -#: users/templates/users/user_detail.html:428 -msgid "Reset SSH public key" -msgstr "重置SSH密钥" - -#: users/templates/users/user_detail.html:438 -msgid "This will reset the user public key and send a reset mail" -msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱" - -#: users/templates/users/user_detail.html:456 -msgid "Successfully updated the SSH public key." -msgstr "更新SSH密钥成功" - -#: users/templates/users/user_detail.html:457 -#: users/templates/users/user_detail.html:461 -msgid "User SSH public key update" -msgstr "SSH密钥" - -#: users/templates/users/user_detail.html:506 -msgid "After unlocking the user, the user can log in normally." -msgstr "解除用户登录限制后,此用户即可正常登录" - -#: users/templates/users/user_detail.html:520 -msgid "Reset user MFA success" -msgstr "重置用户多因子认证成功" - -#: users/templates/users/user_granted_remote_app.html:35 -msgid "App type" -msgstr "应用类型" - -#: users/templates/users/user_group_detail.html:17 -#: users/templates/users/user_group_granted_asset.html:18 -msgid "User group detail" -msgstr "用户组详情" - -#: users/templates/users/user_group_detail.html:81 -msgid "Add user" -msgstr "添加用户" - -#: users/templates/users/user_group_detail.html:87 -msgid "Add" -msgstr "添加" - -#: users/templates/users/user_group_list.html:7 -msgid "Create user group" -msgstr "创建用户组" - -#: users/templates/users/user_list.html:30 -msgid "Delete selected" -msgstr "批量删除" - -#: users/templates/users/user_list.html:32 -msgid "Remove selected" -msgstr "批量移除" - -#: users/templates/users/user_list.html:34 -msgid "Update selected" -msgstr "批量更新" - -#: users/templates/users/user_list.html:35 -msgid "Deactive selected" -msgstr "禁用所选" - -#: users/templates/users/user_list.html:36 -msgid "Active selected" -msgstr "激活所选" - -#: users/templates/users/user_list.html:106 -#: users/templates/users/user_list.html:110 -msgid "Remove" -msgstr "移除" - -#: users/templates/users/user_list.html:179 -msgid "This will delete the selected users !!!" -msgstr "删除选中用户 !!!" - -#: users/templates/users/user_list.html:190 -msgid "User Deleting failed." -msgstr "用户删除失败" - -#: users/templates/users/user_list.html:191 -msgid "User Delete" -msgstr "删除" - -#: users/templates/users/user_list.html:213 -msgid "This will remove the selected users !!" -msgstr "移除选中用户 !!!" - -#: users/templates/users/user_list.html:215 -msgid "User Removing failed." -msgstr "用户移除失败" - -#: users/templates/users/user_list.html:216 -msgid "User Remove" -msgstr "移除" - -#: users/templates/users/user_list.html:265 -msgid "Are you sure about removing it?" -msgstr "您确定移除吗?" - -#: users/templates/users/user_list.html:266 -msgid "Remove the success" -msgstr "移除成功" - -#: users/templates/users/user_list.html:271 -msgid "User is expired" -msgstr "用户已失效" - -#: users/templates/users/user_list.html:274 -msgid "User is inactive" -msgstr "用户已禁用" - #: users/templates/users/user_otp_check_password.html:6 #: users/templates/users/user_verify_mfa.html:6 msgid "Authenticate" @@ -4275,57 +3924,15 @@ msgid "" "installed, go to the next step directly)." msgstr "安装完成后点击下一步进入绑定页面(如已安装,直接进入下一步)" +#: users/templates/users/user_password_update.html:74 +msgid "Reset" +msgstr "重置" + #: users/templates/users/user_password_verify.html:8 #: users/templates/users/user_password_verify.html:9 msgid "Verify password" msgstr "校验密码" -#: users/templates/users/user_profile.html:97 -msgid "Administrator Settings force MFA login" -msgstr "管理员设置强制使用多因子认证" - -#: users/templates/users/user_profile.html:156 -msgid "Set MFA" -msgstr "设置多因子认证" - -#: users/templates/users/user_profile.html:178 -msgid "Update MFA" -msgstr "更改多因子认证" - -#: users/templates/users/user_profile.html:188 -msgid "Update password" -msgstr "更改密码" - -#: users/templates/users/user_profile.html:198 -msgid "Update SSH public key" -msgstr "更改SSH密钥" - -#: users/templates/users/user_profile.html:206 -msgid "Reset public key and download" -msgstr "重置并下载SSH密钥" - -#: users/templates/users/user_pubkey_update.html:55 -msgid "Old public key" -msgstr "原来SSH密钥" - -#: users/templates/users/user_pubkey_update.html:63 -msgid "Fingerprint" -msgstr "指纹" - -#: users/templates/users/user_pubkey_update.html:69 -msgid "Update public key" -msgstr "更新密钥" - -#: users/templates/users/user_pubkey_update.html:72 -msgid "Or reset by server" -msgstr "或者重置并下载密钥" - -#: users/templates/users/user_pubkey_update.html:98 -msgid "" -"The new public key has been set successfully, Please download the " -"corresponding private key." -msgstr "新的公钥已设置成功,请下载对应的私钥" - #: users/templates/users/user_update.html:4 msgid "Update user" msgstr "更新用户" @@ -5182,6 +4789,202 @@ msgstr "旗舰版" msgid "Community edition" msgstr "社区版" +#~ msgid "Auth" +#~ msgstr "认证" + +#~ msgid "Security and Role" +#~ msgstr "角色安全" + +#~ msgid "Select properties that need to be modified" +#~ msgstr "选择需要修改属性" + +#~ msgid "Select all" +#~ msgstr "全选" + +#~ msgid "Create user" +#~ msgstr "创建用户" + +#~ msgid "Force enabled" +#~ msgstr "强制启用" + +#~ msgid "Date joined" +#~ msgstr "创建日期" + +#~ msgid "Last login" +#~ msgstr "最后登录" + +#~ msgid "Last password updated" +#~ msgstr "最后更新密码" + +#~ msgid "Quick modify" +#~ msgstr "快速修改" + +#~ msgid "Force enabled MFA" +#~ msgstr "强制启用多因子认证" + +#~ msgid "Reset MFA" +#~ msgstr "重置多因子认证" + +#~ msgid "Send reset password mail" +#~ msgstr "发送重置密码邮件" + +#~ msgid "Send" +#~ msgstr "发送" + +#~ msgid "Send reset ssh key mail" +#~ msgstr "发送重置密钥邮件" + +#~ msgid "Unblock user" +#~ msgstr "解除登录限制" + +#~ msgid "Unblock" +#~ msgstr "解除" + +#~ msgid "Join user groups" +#~ msgstr "添加到用户组" + +#~ msgid "Join" +#~ msgstr "加入" + +#~ msgid "Update successfully!" +#~ msgstr "更新成功" + +#~ msgid "Goto profile page enable MFA" +#~ msgstr "请去个人信息页面启用自己的多因子认证" + +#~ msgid "An e-mail has been sent to the user`s mailbox." +#~ msgstr "已发送邮件到用户邮箱" + +#~ msgid "Are you sure?" +#~ msgstr "你确认吗?" + +#~ msgid "This will reset the user password and send a reset mail" +#~ msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" + +#~ msgid "Cancel" +#~ msgstr "取消" + +#~ msgid "" +#~ "The reset-ssh-public-key E-mail has been sent successfully. Please inform " +#~ "the user to update his new ssh public key." +#~ msgstr "重设密钥邮件将会发送到用户邮箱" + +#~ msgid "Reset SSH public key" +#~ msgstr "重置SSH密钥" + +#~ msgid "This will reset the user public key and send a reset mail" +#~ msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱" + +#~ msgid "Successfully updated the SSH public key." +#~ msgstr "更新SSH密钥成功" + +#~ msgid "User SSH public key update" +#~ msgstr "SSH密钥" + +#~ msgid "After unlocking the user, the user can log in normally." +#~ msgstr "解除用户登录限制后,此用户即可正常登录" + +#~ msgid "Reset user MFA success" +#~ msgstr "重置用户多因子认证成功" + +#~ msgid "App type" +#~ msgstr "应用类型" + +#~ msgid "User group detail" +#~ msgstr "用户组详情" + +#~ msgid "Add user" +#~ msgstr "添加用户" + +#~ msgid "Add" +#~ msgstr "添加" + +#~ msgid "Create user group" +#~ msgstr "创建用户组" + +#~ msgid "Delete selected" +#~ msgstr "批量删除" + +#~ msgid "Remove selected" +#~ msgstr "批量移除" + +#~ msgid "Update selected" +#~ msgstr "批量更新" + +#~ msgid "Deactive selected" +#~ msgstr "禁用所选" + +#~ msgid "Active selected" +#~ msgstr "激活所选" + +#~ msgid "Remove" +#~ msgstr "移除" + +#~ msgid "This will delete the selected users !!!" +#~ msgstr "删除选中用户 !!!" + +#~ msgid "User Deleting failed." +#~ msgstr "用户删除失败" + +#~ msgid "User Delete" +#~ msgstr "删除" + +#~ msgid "This will remove the selected users !!" +#~ msgstr "移除选中用户 !!!" + +#~ msgid "User Removing failed." +#~ msgstr "用户移除失败" + +#~ msgid "User Remove" +#~ msgstr "移除" + +#~ msgid "Are you sure about removing it?" +#~ msgstr "您确定移除吗?" + +#~ msgid "Remove the success" +#~ msgstr "移除成功" + +#~ msgid "User is expired" +#~ msgstr "用户已失效" + +#~ msgid "User is inactive" +#~ msgstr "用户已禁用" + +#~ msgid "Administrator Settings force MFA login" +#~ msgstr "管理员设置强制使用多因子认证" + +#~ msgid "Set MFA" +#~ msgstr "设置多因子认证" + +#~ msgid "Update MFA" +#~ msgstr "更改多因子认证" + +#~ msgid "Update password" +#~ msgstr "更改密码" + +#~ msgid "Update SSH public key" +#~ msgstr "更改SSH密钥" + +#~ msgid "Reset public key and download" +#~ msgstr "重置并下载SSH密钥" + +#~ msgid "Old public key" +#~ msgstr "原来SSH密钥" + +#~ msgid "Fingerprint" +#~ msgstr "指纹" + +#~ msgid "Update public key" +#~ msgstr "更新密钥" + +#~ msgid "Or reset by server" +#~ msgstr "或者重置并下载密钥" + +#~ msgid "" +#~ "The new public key has been set successfully, Please download the " +#~ "corresponding private key." +#~ msgstr "新的公钥已设置成功,请下载对应的私钥" + #~ msgid "(Domain name support)" #~ msgstr "(支持域名)" diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index c6a3c32ac..95757eb8b 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -121,7 +121,11 @@ class TerminalSettingSerializer(serializers.Serializer): ('50', '50'), ) TERMINAL_PASSWORD_AUTH = serializers.BooleanField(required=False, label=_('Password auth')) - TERMINAL_PUBLIC_KEY_AUTH = serializers.BooleanField(required=False, label=_('Public key auth')) + TERMINAL_PUBLIC_KEY_AUTH = serializers.BooleanField( + required=False, label=_('Public key auth'), + help_text=_('Tips: If use other auth method, like AD/LDAP, you should disable this to ' + 'avoid being able to log in after deleting') + ) 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( diff --git a/apps/users/models/user.py b/apps/users/models/user.py index b10c07223..1f8b2418a 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -66,13 +66,9 @@ class AuthMixin: def can_update_ssh_key(self): return self.can_use_ssh_key_login() - def can_use_ssh_key_login(self): - if not settings.TERMINAL_PUBLIC_KEY_AUTH: - return False - if self.is_local or settings.TERMINAL_ONLY_SOURCE_LOCAL_CAN_PUBLIC_KEY_AUTH: - return True - else: - return False + @staticmethod + def can_use_ssh_key_login(): + return settings.TERMINAL_PUBLIC_KEY_AUTH def is_public_key_valid(self): """ diff --git a/apps/users/templates/users/_user.html b/apps/users/templates/users/_user.html deleted file mode 100644 index dbcf7f804..000000000 --- a/apps/users/templates/users/_user.html +++ /dev/null @@ -1,88 +0,0 @@ -{% extends '_base_create_update.html' %} -{% load i18n %} -{% load static %} -{% load bootstrap3 %} -{% block form %} - {% if form.non_field_errors %} -
- {{ form.non_field_errors }} -
- {% endif %} -
- {% csrf_token %} -

{% trans 'Account' %}

- {% bootstrap_field form.name layout="horizontal" %} - {% bootstrap_field form.username layout="horizontal" %} - {% bootstrap_field form.email layout="horizontal" %} - {% bootstrap_field form.groups layout="horizontal" %} - -
- -

{% trans 'Auth' %}

- {% block password %}{% endblock %} - {% bootstrap_field form.mfa_level layout="horizontal" %} - {% bootstrap_field form.source layout="horizontal" %} - -
-

{% trans 'Security and Role' %}

- {% bootstrap_field form.role layout="horizontal" %} -
- -
-
- - {% if form.errors %} - - {% else %} - - {% endif %} -
- {{ form.date_expired.errors }} -
-
-
-

{% trans 'Profile' %}

- {% bootstrap_field form.phone layout="horizontal" %} - {% bootstrap_field form.wechat layout="horizontal" %} - {% bootstrap_field form.comment layout="horizontal" %} -
-
-
- - -
-
-
- -{% endblock %} -{% block custom_foot_js %} - - - - - - - -{% endblock %} diff --git a/apps/users/templates/users/user_bulk_update.html b/apps/users/templates/users/user_bulk_update.html deleted file mode 100644 index 28b0e8cb7..000000000 --- a/apps/users/templates/users/user_bulk_update.html +++ /dev/null @@ -1,70 +0,0 @@ -{% extends '_base_create_update.html' %} -{% load static %} -{% load bootstrap3 %} -{% load i18n %} - -{% block form %} -
-

{% trans 'Select properties that need to be modified' %}

-
- {% trans 'Select all' %} - {% for field in form %} - {% if field.name != 'users' %} - {{ field.label }} - {% endif %} - {% endfor %} -
-
-
- {% csrf_token %} - {% bootstrap_form form layout="horizontal" %} -
-
- - -
-
-
-{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_create.html b/apps/users/templates/users/user_create.html deleted file mode 100644 index 5e15b5469..000000000 --- a/apps/users/templates/users/user_create.html +++ /dev/null @@ -1,100 +0,0 @@ -{% extends 'users/_user.html' %} -{% load i18n %} -{% load bootstrap3 %} -{% block user_template_title %}{% trans "Create user" %}{% endblock %} -{% block password %} - {% bootstrap_field form.password_strategy layout="horizontal" %} - {% bootstrap_field form.password layout="horizontal" %} - {# 密码popover #} -
- -
- -{% endblock %} - diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html deleted file mode 100644 index 0b624b78a..000000000 --- a/apps/users/templates/users/user_detail.html +++ /dev/null @@ -1,550 +0,0 @@ -{% extends 'users/_base_user_detail.html' %} -{% load static %} -{% load i18n %} - -{% block custom_head_css_js %} - - -{% endblock %} - -{% block content_nav_delete_update %} -
  • - {% trans 'Update' %} -
  • -
  • - - {% trans 'Delete' %} - -
  • -{% endblock %} - -{% block content_table %} -
    -
    -
    - {{ object.name }} -
    - - - - - - - - - - -
    -
    -
    - - - - - - - - - - - - - - - - - - {% if user.phone %} - - - - - {% endif %} - {% if object.wechat %} - - - - - {% endif %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% if object.can_update_password %} - - - - - {% endif %} - - - - - -
    - -
    {% trans 'Name' %}:{{ object.name }}
    {% trans 'Username' %}:{{ object.username }}
    {% trans 'Email' %}:{{ object.email }}
    {% trans 'Phone' %}:{{ object.phone }}
    {% trans 'Wechat' %}:{{ object.wechat }}
    {% trans 'Role' %}:{{ object.org_role_display }}
    {% trans 'MFA' %}: - {% if object.mfa_force_enabled %} - {% trans 'Force enabled' %} - {% elif object.mfa_enabled%} - {% trans 'Enabled' %} - {% else %} - {% trans 'Disabled' %} - {% endif %} -
    {% trans 'Source' %}:{{ object.get_source_display }}
    {% trans 'Date expired' %}:{{ object.date_expired|date:"Y-m-j H:i:s" }}
    {% trans 'Created by' %}:{{ object.created_by }}
    {% trans 'Date joined' %}:{{ object.date_joined|date:"Y-m-j H:i:s" }}
    {% trans 'Last login' %}:{{ object.last_login|date:"Y-m-j H:i:s" }}
    {% trans 'Last password updated' %}:{{ object.date_password_last_updated|date:"Y-m-j H:i:s" }}
    {% trans 'Comment' %}:{{ object.comment }}
    -
    -
    -
    -
    -
    -
    - {% trans 'Quick modify' %} -
    -
    - - - - - - - - - - - - - - - {% if object.can_update_password %} - - - - - {% endif %} - {% if object.can_update_ssh_key %} - - - - - {% endif %} - - - - - -
    {% trans 'Active' %}: - -
    -
    - - -
    -
    -
    -
    {% trans 'Force enabled MFA' %}: - -
    -
    - - -
    -
    -
    -
    {% trans 'Reset MFA' %}: - - - -
    {% trans 'Send reset password mail' %}: - - - -
    {% trans 'Send reset ssh key mail' %}: - - - -
    {% trans 'Unblock user' %} - - - -
    -
    -
    - {% if request.user.can_admin_current_org %} - - {% if object.can_user_current_org or object.can_admin_current_org %} -
    -
    - {% trans 'User group' %} -
    -
    - - - - - - - - - - - - {% for group in object.groups.all %} - - - - - {% endfor %} - -
    - -
    - -
    - {{ group.name }} - - -
    -
    -
    - {% endif %} - - {% if LICENSE_VALID and LOGIN_CONFIRM_ENABLE %} -
    -
    - {% trans 'Login confirm' %} -
    -
    - - - - - - - - - - - {% if object.get_login_confirm_setting %} - {% for u in object.login_confirm_setting.reviewers.all %} - - - - - {% endfor %} - {% endif %} - -
    - -
    - -
    - {{ u }} - - -
    -
    -
    - {% endif %} - - {% endif %} -
    - -{% include 'users/_user_update_pk_modal.html' %} - -{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_granted_asset.html b/apps/users/templates/users/user_granted_asset.html deleted file mode 100644 index 523697153..000000000 --- a/apps/users/templates/users/user_granted_asset.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends 'users/_base_user_detail.html' %} -{% load bootstrap3 %} -{% load static %} -{% load i18n %} - -{% block custom_head_css_js %} - - -{% endblock %} - -{% block content_table %} -{% include 'users/_granted_assets.html' %} -{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_granted_database_app.html b/apps/users/templates/users/user_granted_database_app.html deleted file mode 100644 index 7133a9217..000000000 --- a/apps/users/templates/users/user_granted_database_app.html +++ /dev/null @@ -1,100 +0,0 @@ -{% extends 'users/_base_user_detail.html' %} -{% load i18n static %} - -{% block custom_head_css_js %} - -{% endblock %} - -{% block content_table %} -
    -
    -
    - {{ object.name }} -
    - - - - - - - - - - -
    -
    -
    - - - - - - - - - - - - - -
    - - {% trans 'Name' %}{% trans 'Type' %}{% trans 'Host' %}{% trans 'Database' %}{% trans 'Comment' %}
    -
    -
    -
    -{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_granted_remote_app.html b/apps/users/templates/users/user_granted_remote_app.html deleted file mode 100644 index 19b8ab22c..000000000 --- a/apps/users/templates/users/user_granted_remote_app.html +++ /dev/null @@ -1,93 +0,0 @@ -{% extends 'users/_base_user_detail.html' %} -{% load i18n static %} - -{% block custom_head_css_js %} - -{% endblock %} - -{% block content_table %} -
    -
    -
    - {{ object.name }} -
    - - - - - - - - - - -
    -
    -
    - - - - - - - - - - - - -
    - - {% trans 'Name' %}{% trans 'App type' %}{% trans 'Asset' %}{% trans 'Comment' %}
    -
    -
    -
    -{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_group_create_update.html b/apps/users/templates/users/user_group_create_update.html deleted file mode 100644 index 09030eca8..000000000 --- a/apps/users/templates/users/user_group_create_update.html +++ /dev/null @@ -1,69 +0,0 @@ -{% extends 'base.html' %} -{% load static %} -{% load i18n %} -{% load bootstrap3 %} - -{% block content %} -
    -
    -
    -
    -
    -
    {{ action }}
    -
    -
    - {% if form.non_field_errors %} -
    - {{ form.non_field_errors }} -
    - {% endif %} -
    - {% csrf_token %} - {% bootstrap_field form.name layout="horizontal" %} - {% bootstrap_field form.users layout="horizontal" %} - {% bootstrap_field form.comment layout="horizontal" %} -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -
    - {% include "users/_select_user_modal.html" %} -{% endblock %} -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_group_detail.html b/apps/users/templates/users/user_group_detail.html deleted file mode 100644 index cd65bb746..000000000 --- a/apps/users/templates/users/user_group_detail.html +++ /dev/null @@ -1,168 +0,0 @@ -{% extends 'base.html' %} -{% load static %} -{% load i18n %} - -{% block custom_head_css_js %} - - -{% endblock %} -{% block content %} -
    -
    -
    -
    - -
    -
    -
    -
    - {{ user_group.name }} - -
    -
    - - - - - - - - - - - - - - - - - - - -
    {% trans 'Name' %}:{{ user_group.name }}
    {% trans 'Create by' %}:{{ user_group.created_by }}
    {% trans 'Date created' %}:{{ user_group.date_created }}
    {% trans 'Comment' %}:{{ user_group.comment }}
    -
    -
    -
    -
    -
    -
    - {% trans 'User' %} -
    -
    - - - - - - - - - - - - {% for user in user_group.users.all %} - - - - - {% endfor %} - -
    - -
    - -
    {{ user.name }} - -
    -
    -
    -
    -
    -
    -
    -
    -
    -{% endblock %} -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_group_granted_asset.html b/apps/users/templates/users/user_group_granted_asset.html deleted file mode 100644 index 2180e6313..000000000 --- a/apps/users/templates/users/user_group_granted_asset.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends 'base.html' %} -{% load bootstrap3 %} -{% load static %} -{% load i18n %} - -{% block custom_head_css_js %} - - -{% endblock %} -{% block content %} -
    -
    -
    -
    - -
    - {% include 'users/_granted_assets.html' %} -
    -
    -
    -
    -
    -{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_group_list.html b/apps/users/templates/users/user_group_list.html deleted file mode 100644 index 5e354d3db..000000000 --- a/apps/users/templates/users/user_group_list.html +++ /dev/null @@ -1,112 +0,0 @@ -{% extends '_base_list.html' %} -{% load i18n static %} -{% block table_search %} - {% include '_csv_import_export.html' %} -{% endblock %} -{% block table_container %} - - - - - - - - - - - -
    - - {% trans 'Name' %}{% trans 'User' %}{% trans 'Comment' %}{% trans 'Action' %}
    -{% endblock %} - -{% block content_bottom_left %}{% endblock %} -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_list.html b/apps/users/templates/users/user_list.html deleted file mode 100644 index 28b30f73d..000000000 --- a/apps/users/templates/users/user_list.html +++ /dev/null @@ -1,280 +0,0 @@ -{% extends '_base_list.html' %} -{% load i18n static %} -{% block table_search %} - {% include '_csv_import_export.html' %} -{% endblock %} -{% block table_container %} - - - - - - - - - - - - - - - - -
    - - {% trans 'Name' %}{% trans 'Username' %}{% trans 'Role' %}{% trans 'User group' %}{% trans 'Source' %}{% trans 'Validity' %}{% trans 'Action' %}
    -
    -
    - -
    - -
    -
    -
    -{% endblock %} -{% block content_bottom_left %}{% endblock %} -{% block custom_foot_js %} - - -{% endblock %} - diff --git a/apps/users/templates/users/user_profile.html b/apps/users/templates/users/user_profile.html deleted file mode 100644 index d1f77c7d0..000000000 --- a/apps/users/templates/users/user_profile.html +++ /dev/null @@ -1,233 +0,0 @@ -{% extends 'base.html' %} -{% load static %} -{% load i18n %} - -{% block custom_head_css_js %} - - -{% endblock %} -{% block content %} -
    -
    -
    -
    - -
    -
    -
    -
    - {{ user.name }} - -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - {% if user.can_update_ssh_key %} - - - - - {% endif %} - - - - - - - - - - - - - - - - - {% if user.can_update_password %} - - - - - {% endif %} - - - - - - - - - - - - -
    - -
    {% trans 'Username' %}{{ user.username }}
    {% trans 'Name' %}{{ user.name }}
    {% trans 'Role' %}{{ user.get_role_display }}
    {% trans 'Email' %}{{ user.email }}
    {% trans 'Active' %}{{ user.is_active|yesno:"Yes,No,Unkown" }}
    {% trans 'Public key' %} - - - - - - - -
    - {{ user.public_key_obj.comment }} -
    - {{ user.public_key_obj.hash_md5 }} -
    -
    {% trans 'MFA' %} - {% if user.mfa_force_enabled %} - {% trans 'Force enable' %} - {% elif user.mfa_enabled%} - {% trans 'Enable' %} - {% else %} - {% trans 'Disable' %} - {% endif %} - {% if mfa_setting %} - ( {% trans 'Administrator Settings force MFA login' %} ) - {% endif %} -
    {% trans 'Source' %}{{ user.get_source_display }}
    {% trans 'Date joined' %}{{ user.date_joined|date:"Y-m-d H:i:s" }}
    {% trans 'Last login' %}{{ user.last_login|date:"Y-m-d H:i:s" }}
    {% trans 'Last password updated' %}{{ user.date_password_last_updated|date:"Y-m-d H:i:s" }}
    {% trans 'Date expired' %}{{ user.date_expired|date:"Y-m-d H:i:s" }}
    {% trans 'User groups' %} - - {% for group in user.groups.all %} - - - - {% endfor %} -
    - {{ group.name }} -
    -
    {% trans 'Comment' %}:{{ user.comment }}
    -
    -
    -
    -
    -
    -
    -
    -
    - {% trans 'Quick modify' %} -
    -
    - - - - - - - {% if request.user.mfa_enabled %} - - - - - {% endif %} - {% if request.user.can_update_password %} - - - - - {% endif %} - {% if request.user.can_update_ssh_key %} - - - - - - - - - {% endif %} - -
    {% trans 'Set MFA' %}: - - {% trans 'Disable' %} - {% else %} - {% url 'authentication:user-otp-disable-authentication' %} - ">{% trans 'Disable' %} - {% endif %} - {% else %} - {% url 'authentication:user-otp-enable-start' %} - ">{% trans 'Enable' %} - {% endif %} - - -
    {% trans 'Update MFA' %}: - - {% trans 'Update' %} - -
    {% trans 'Update password' %}: - - {% trans 'Update' %} - -
    {% trans 'Update SSH public key' %}: - - {% trans 'Update' %} - -
    {% trans 'Reset public key and download' %}: - - {% trans 'Reset' %} - -
    -
    -
    -
    -
    -
    -
    -
    -{% endblock %} -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_profile_update.html b/apps/users/templates/users/user_profile_update.html deleted file mode 100644 index e59edeccc..000000000 --- a/apps/users/templates/users/user_profile_update.html +++ /dev/null @@ -1,84 +0,0 @@ -{% extends 'base.html' %} -{% load static %} -{% load i18n %} -{% load bootstrap3 %} - -{% block custom_head_css_js %} - - - - - -{% endblock %} -{% block content %} -
    -
    -
    -
    -
    - -
    -
    -
    -
    - {% csrf_token %} -

    {% trans 'Account' %}

    - {% bootstrap_field form.username layout="horizontal" %} - {% bootstrap_field form.name layout="horizontal" %} - {% bootstrap_field form.email layout="horizontal" %} - -
    -

    {% trans 'Profile' %}

    - {% bootstrap_field form.phone layout="horizontal" %} - {% bootstrap_field form.wechat layout="horizontal" %} -
    -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -
    -
    -{% endblock %} - -{% block custom_foot_js %} - - -{% endblock %} diff --git a/apps/users/templates/users/user_pubkey_update.html b/apps/users/templates/users/user_pubkey_update.html deleted file mode 100644 index 4ab03f01c..000000000 --- a/apps/users/templates/users/user_pubkey_update.html +++ /dev/null @@ -1,102 +0,0 @@ -{% extends 'base.html' %} -{% load static %} -{% load i18n %} -{% load bootstrap3 %} - -{% block custom_head_css_js %} - - - - - -{% endblock %} -{% block content %} -
    -
    -
    -
    -
    - -
    -
    -
    -
    - {% csrf_token %} -

    {% trans 'Old public key' %}

    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    -

    {% trans 'Update public key' %}

    - {% bootstrap_field form.public_key layout="horizontal" %} -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -
    -
    -{% endblock %} - -{% block custom_foot_js %} - - -{% endblock %} diff --git a/apps/users/templates/users/user_remote_app_permission.html b/apps/users/templates/users/user_remote_app_permission.html deleted file mode 100644 index d1f6aabb7..000000000 --- a/apps/users/templates/users/user_remote_app_permission.html +++ /dev/null @@ -1,168 +0,0 @@ -{% extends 'users/_base_user_detail.html' %} -{% load static %} -{% load i18n %} - -{% block custom_head_css_js %} - - -{% endblock %} - -{% block content_table %} -
    -
    -
    - {{ object.name }} -
    - - - - - - - - - - -
    -
    -
    - - - - - - - - - - - - - - - -
    {% trans 'Name' %}{% trans 'User' %}{% trans 'User group' %}{% trans 'RemoteApp' %}{% trans 'System user' %}{% trans 'Validity' %}{% trans 'Action' %}
    -
    -
    -
    -{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_update.html b/apps/users/templates/users/user_update.html deleted file mode 100644 index 182ec88aa..000000000 --- a/apps/users/templates/users/user_update.html +++ /dev/null @@ -1,117 +0,0 @@ -{% extends 'users/_user.html' %} -{% load i18n %} -{% load bootstrap3 %} -{% block user_template_title %}{% trans "Update user" %}{% endblock %} -{% block password %} - {% if object.can_update_password %} - {% bootstrap_field form.password layout="horizontal" %} - {# 密码popover #} -
    - -
    - {% else %} -
    - -
    - {% trans 'User auth from {}, go there change password' %} -
    -
    - {% endif %} - {% if object.can_update_ssh_key %} - {% bootstrap_field form.public_key layout="horizontal" %} - {% else %} -
    - -
    - {% trans 'User auth from {}, ssh key login is not supported' %} -
    -
    - {% endif %} -{% endblock %} - -{% block custom_foot_js %} - {{ block.super }} - -{% endblock %} From 99cce185dd01a931181067af6a3e0a3ab9dabeca Mon Sep 17 00:00:00 2001 From: xinwen Date: Tue, 27 Apr 2021 14:21:48 +0800 Subject: [PATCH 12/39] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E5=A4=B1=E6=95=88=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../management/commands/expire_caches.py | 19 +++++++++++++++++++ jms | 9 +++++++++ 2 files changed, 28 insertions(+) create mode 100644 apps/common/management/commands/expire_caches.py diff --git a/apps/common/management/commands/expire_caches.py b/apps/common/management/commands/expire_caches.py new file mode 100644 index 000000000..fb09f47eb --- /dev/null +++ b/apps/common/management/commands/expire_caches.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from assets.signals_handler.node_assets_mapping import expire_node_assets_mapping_for_memory +from orgs.models import Organization + + +def expire_node_assets_mapping(): + org_ids = Organization.objects.all().values_list('id', flat=True) + org_ids = [*org_ids, '00000000-0000-0000-0000-000000000000'] + + for org_id in org_ids: + expire_node_assets_mapping_for_memory(org_id) + + +class Command(BaseCommand): + help = 'Expire caches' + + def handle(self, *args, **options): + expire_node_assets_mapping() diff --git a/jms b/jms index 24b71f4e4..6bd71849d 100755 --- a/jms +++ b/jms @@ -97,6 +97,14 @@ def check_migrations(): # sys.exit(1) +def expire_caches(): + apps_dir = os.path.join(BASE_DIR, 'apps') + code = subprocess.call("python manage.py expire_caches", shell=True, cwd=apps_dir) + + if code == 1: + return + + def perform_db_migrate(): logging.info("Check database structure change ...") os.chdir(os.path.join(BASE_DIR, 'apps')) @@ -116,6 +124,7 @@ def prepare(): check_database_connection() check_migrations() upgrade_db() + expire_caches() def check_pid(pid): From e9b174f342195268b5dbd8c5e9e27b64db13ac2a Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 26 Apr 2021 15:33:51 +0800 Subject: [PATCH 13/39] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E8=BF=87=E6=BB=A4=E8=A7=84=E5=88=99Model:=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0Action-reconfirm;=20=E6=B7=BB=E5=8A=A0field-reviewers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0070_auto_20210426_1515.py | 25 +++++++++++++++++++ apps/assets/models/cmd_filter.py | 25 ++++++++++++------- apps/assets/models/user.py | 4 +-- apps/assets/serializers/cmd_filter.py | 2 +- 4 files changed, 44 insertions(+), 12 deletions(-) create mode 100644 apps/assets/migrations/0070_auto_20210426_1515.py diff --git a/apps/assets/migrations/0070_auto_20210426_1515.py b/apps/assets/migrations/0070_auto_20210426_1515.py new file mode 100644 index 000000000..ca6ff4273 --- /dev/null +++ b/apps/assets/migrations/0070_auto_20210426_1515.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1 on 2021-04-26 07:15 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('assets', '0069_change_node_key0_to_key1'), + ] + + operations = [ + migrations.AddField( + model_name='commandfilterrule', + name='reviewers', + field=models.ManyToManyField(blank=True, related_name='review_cmd_filter_rules', to=settings.AUTH_USER_MODEL, verbose_name='Reviewers'), + ), + migrations.AlterField( + model_name='commandfilterrule', + name='action', + field=models.IntegerField(choices=[(0, 'Deny'), (1, 'Allow'), (2, 'Reconfirm')], default=0, verbose_name='Action'), + ), + ] diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py index c1242fd7e..e0826da5d 100644 --- a/apps/assets/models/cmd_filter.py +++ b/apps/assets/models/cmd_filter.py @@ -41,11 +41,12 @@ class CommandFilterRule(OrgModelMixin): (TYPE_COMMAND, _('Command')), ) - ACTION_DENY, ACTION_ALLOW, ACTION_UNKNOWN = range(3) - ACTION_CHOICES = ( - (ACTION_DENY, _('Deny')), - (ACTION_ALLOW, _('Allow')), - ) + ACTION_UNKNOWN = 10 + + class ActionChoices(models.IntegerChoices): + deny = 0, _('Deny') + allow = 1, _('Allow') + confirm = 2, _('Reconfirm') id = models.UUIDField(default=uuid.uuid4, primary_key=True) filter = models.ForeignKey('CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules') @@ -53,7 +54,13 @@ class CommandFilterRule(OrgModelMixin): priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)]) content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command")) - action = models.IntegerField(default=ACTION_DENY, choices=ACTION_CHOICES, verbose_name=_("Action")) + action = models.IntegerField(default=ActionChoices.deny, choices=ActionChoices.choices, verbose_name=_("Action")) + # 动作: 附加字段 + # - confirm: 命令复核人 + reviewers = models.ManyToManyField( + 'users.User', related_name='review_cmd_filter_rules', blank=True, + verbose_name=_("Reviewers") + ) comment = models.CharField(max_length=64, blank=True, default='', verbose_name=_("Comment")) date_created = models.DateTimeField(auto_now_add=True) date_updated = models.DateTimeField(auto_now=True) @@ -89,10 +96,10 @@ class CommandFilterRule(OrgModelMixin): if not found: return self.ACTION_UNKNOWN, '' - if self.action == self.ACTION_ALLOW: - return self.ACTION_ALLOW, found.group() + if self.action == self.ActionChoices.allow: + return self.ActionChoices.allow, found.group() else: - return self.ACTION_DENY, found.group() + return self.ActionChoices.deny, found.group() def __str__(self): return '{} % {}'.format(self.type, self.content) diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 1640c7f32..37af8ea86 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -196,9 +196,9 @@ class SystemUser(BaseUser): def is_command_can_run(self, command): for rule in self.cmd_filter_rules: action, matched_cmd = rule.match(command) - if action == rule.ACTION_ALLOW: + if action == rule.ActionChoices.allow: return True, None - elif action == rule.ACTION_DENY: + elif action == rule.ActionChoices.deny: return False, matched_cmd return True, None diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py index 0c8eca3d4..e7059d04a 100644 --- a/apps/assets/serializers/cmd_filter.py +++ b/apps/assets/serializers/cmd_filter.py @@ -34,7 +34,7 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer): fields_mini = ['id'] fields_small = fields_mini + [ 'type', 'type_display', 'content', 'priority', - 'action', 'action_display', + 'action', 'action_display', 'reviewers', 'comment', 'created_by', 'date_created', 'date_updated' ] fields_fk = ['filter'] From 50918a3dd2b9e4c5684db1ff4d7d09e0917e9b66 Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 26 Apr 2021 17:56:06 +0800 Subject: [PATCH 14/39] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E5=A4=8D=E6=A0=B8=E9=80=BB=E8=BE=91;=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=91=BD=E4=BB=A4=E5=A4=8D=E6=A0=B8=E5=B7=A5=E5=8D=95?= =?UTF-8?q?;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/acls/api/login_asset_check.py | 36 ++--------- apps/assets/api/cmd_filter.py | 63 ++++++++++++++++++- apps/assets/models/cmd_filter.py | 21 +++++++ apps/assets/serializers/cmd_filter.py | 35 +++++++++++ apps/assets/urls/api_urls.py | 3 + apps/terminal/models/session.py | 5 ++ apps/tickets/api/__init__.py | 1 + apps/tickets/api/common.py | 44 +++++++++++++ apps/tickets/const.py | 1 + apps/tickets/handler/command_confirm.py | 30 +++++++++ .../migrations/0009_auto_20210426_1720.py | 18 ++++++ .../meta/ticket_type/command_confirm.py | 25 ++++++++ 12 files changed, 248 insertions(+), 34 deletions(-) create mode 100644 apps/tickets/api/common.py create mode 100644 apps/tickets/handler/command_confirm.py create mode 100644 apps/tickets/migrations/0009_auto_20210426_1720.py create mode 100644 apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py index ed525e6cf..d689675b0 100644 --- a/apps/acls/api/login_asset_check.py +++ b/apps/acls/api/login_asset_check.py @@ -5,7 +5,7 @@ from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView from common.permissions import IsAppUser from common.utils import reverse, lazyproperty from orgs.utils import tmp_to_org, tmp_to_root_org -from tickets.models import Ticket +from tickets.api import GenericTicketStatusRetrieveCloseAPI from ..models import LoginAssetACL from .. import serializers @@ -48,7 +48,7 @@ class LoginAssetCheckAPI(CreateAPIView): org_id=self.serializer.org.id ) confirm_status_url = reverse( - view_name='acls:login-asset-confirm-status', + view_name='api-acls:login-asset-confirm-status', kwargs={'pk': str(ticket.id)} ) ticket_detail_url = reverse( @@ -72,34 +72,6 @@ class LoginAssetCheckAPI(CreateAPIView): return serializer -class LoginAssetConfirmStatusAPI(RetrieveDestroyAPIView): - permission_classes = (IsAppUser, ) +class LoginAssetConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI): + pass - def retrieve(self, request, *args, **kwargs): - if self.ticket.action_open: - status = 'await' - elif self.ticket.action_approve: - status = 'approve' - else: - status = 'reject' - data = { - 'status': status, - 'action': self.ticket.action, - 'processor': self.ticket.processor_display - } - return Response(data=data, status=200) - - def destroy(self, request, *args, **kwargs): - if self.ticket.status_open: - self.ticket.close(processor=self.ticket.applicant) - data = { - 'action': self.ticket.action, - 'status': self.ticket.status, - 'processor': self.ticket.processor_display - } - return Response(data=data, status=200) - - @lazyproperty - def ticket(self): - with tmp_to_root_org(): - return get_object_or_404(Ticket, pk=self.kwargs['pk']) diff --git a/apps/assets/api/cmd_filter.py b/apps/assets/api/cmd_filter.py index 95aac8af9..c7ffb792f 100644 --- a/apps/assets/api/cmd_filter.py +++ b/apps/assets/api/cmd_filter.py @@ -1,15 +1,25 @@ # -*- coding: utf-8 -*- # +from rest_framework.response import Response +from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView from django.shortcuts import get_object_or_404 +from common.utils import reverse +from common.utils import lazyproperty from orgs.mixins.api import OrgBulkModelViewSet -from ..hands import IsOrgAdmin +from orgs.utils import tmp_to_root_org +from tickets.models import Ticket +from tickets.api import GenericTicketStatusRetrieveCloseAPI +from ..hands import IsOrgAdmin, IsAppUser from ..models import CommandFilter, CommandFilterRule from .. import serializers -__all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet'] +__all__ = [ + 'CommandFilterViewSet', 'CommandFilterRuleViewSet', 'CommandConfirmAPI', + 'CommandConfirmStatusAPI' +] class CommandFilterViewSet(OrgBulkModelViewSet): @@ -35,3 +45,52 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet): return cmd_filter.rules.all() +class CommandConfirmAPI(CreateAPIView): + permission_classes = () + # permission_classes = (IsAppUser, ) + serializer_class = serializers.CommandConfirmSerializer + + def create(self, request, *args, **kwargs): + ticket = self.create_command_confirm_ticket() + response_data = self.get_response_data(ticket) + return Response(data=response_data, status=200) + + def create_command_confirm_ticket(self): + ticket = self.serializer.cmd_filter_rule.create_command_confirm_ticket( + run_command=self.serializer.data.get('run_command'), + session=self.serializer.session, + cmd_filter_rule=self.serializer.cmd_filter_rule, + org_id=self.serializer.org.id + ) + return ticket + + @staticmethod + def get_response_data(ticket): + confirm_status_url = reverse( + view_name='api-assets:command-confirm-status', + kwargs={'pk': str(ticket.id)} + ) + ticket_detail_url = reverse( + view_name='api-tickets:ticket-detail', + kwargs={'pk': str(ticket.id)}, + external=True, api_to_ui=True + ) + ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type) + return { + 'check_confirm_status': {'method': 'GET', 'url': confirm_status_url}, + 'close_confirm': {'method': 'DELETE', 'url': confirm_status_url}, + 'ticket_detail_url': ticket_detail_url, + 'reviewers': [str(user) for user in ticket.assignees.all()] + } + + @lazyproperty + def serializer(self): + serializer = self.get_serializer(data=self.request.data) + serializer.is_valid(raise_exception=True) + return serializer + + +class CommandConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI): + permission_classes = () + pass + diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py index e0826da5d..dff99298c 100644 --- a/apps/assets/models/cmd_filter.py +++ b/apps/assets/models/cmd_filter.py @@ -103,3 +103,24 @@ class CommandFilterRule(OrgModelMixin): def __str__(self): return '{} % {}'.format(self.type, self.content) + + def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id): + from tickets.const import TicketTypeChoices + from tickets.models import Ticket + data = { + 'title': _('Command confirm') + ' ({})'.format(session.user), + 'type': TicketTypeChoices.command_confirm, + 'meta': { + 'apply_run_user': session.user, + 'apply_run_asset': session.asset, + 'apply_run_system_user': session.system_user, + 'apply_run_command': run_command, + 'apply_from_session': str(session.id), + 'apply_from_cmd_filter_rule': str(cmd_filter_rule.id), + }, + 'org_id': org_id, + } + ticket = Ticket.objects.create(**data) + ticket.assignees.set(self.reviewers.all()) + ticket.open(applicant=session.user_obj) + return ticket diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py index e7059d04a..2e60b31d7 100644 --- a/apps/assets/serializers/cmd_filter.py +++ b/apps/assets/serializers/cmd_filter.py @@ -6,6 +6,9 @@ from rest_framework import serializers from common.drf.serializers import AdaptedBulkListSerializer from ..models import CommandFilter, CommandFilterRule, SystemUser from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from orgs.utils import tmp_to_root_org +from common.utils import get_object_or_none, lazyproperty +from terminal.models import Session class CommandFilterSerializer(BulkOrgResourceModelSerializer): @@ -50,3 +53,35 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer): # msg = _("Content should not be contain: {}").format(invalid_char) # raise serializers.ValidationError(msg) # return content + + +class CommandConfirmSerializer(serializers.Serializer): + session_id = serializers.UUIDField(required=True, allow_null=False) + cmd_filter_rule_id = serializers.UUIDField(required=True, allow_null=False) + run_command = serializers.CharField(required=True, allow_null=False) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.session = None + self.cmd_filter_rule = None + + def validate_session_id(self, session_id): + self.session = self.validate_object_exist(Session, session_id) + return session_id + + def validate_cmd_filter_rule_id(self, cmd_filter_rule_id): + self.cmd_filter_rule = self.validate_object_exist(CommandFilterRule, cmd_filter_rule_id) + return cmd_filter_rule_id + + @staticmethod + def validate_object_exist(model, field_id): + with tmp_to_root_org(): + obj = get_object_or_none(model, id=field_id) + if not obj: + error = '{} Model object does not exist'.format(model.__name__) + raise serializers.ValidationError(error) + return obj + + @lazyproperty + def org(self): + return self.session.org diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 5a5e6d803..c8413f83e 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -63,6 +63,9 @@ urlpatterns = [ path('gateways//test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'), + path('cmd-filters/command-confirm/', api.CommandConfirmAPI.as_view(), name='command-confirm'), + path('cmd-filters/command-confirm//status/', api.CommandConfirmStatusAPI.as_view(), name='command-confirm-status') + ] old_version_urlpatterns = [ diff --git a/apps/terminal/models/session.py b/apps/terminal/models/session.py index 89e338143..6d85759af 100644 --- a/apps/terminal/models/session.py +++ b/apps/terminal/models/session.py @@ -11,6 +11,7 @@ from django.core.files.storage import default_storage from django.core.cache import cache from assets.models import Asset +from users.models import User from orgs.mixins.models import OrgModelMixin from common.db.models import ChoiceSet from ..backends import get_multi_command_storage @@ -79,6 +80,10 @@ class Session(OrgModelMixin): def asset_obj(self): return Asset.objects.get(id=self.asset_id) + @property + def user_obj(self): + return User.objects.get(id=self.user_id) + @property def _date_start_first_has_replay_rdp_session(self): if self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION is None: diff --git a/apps/tickets/api/__init__.py b/apps/tickets/api/__init__.py index 6b519ef80..a6b5e39c6 100644 --- a/apps/tickets/api/__init__.py +++ b/apps/tickets/api/__init__.py @@ -3,3 +3,4 @@ from .ticket import * from .assignee import * from .comment import * +from .common import * diff --git a/apps/tickets/api/common.py b/apps/tickets/api/common.py new file mode 100644 index 000000000..fe5a5d1e9 --- /dev/null +++ b/apps/tickets/api/common.py @@ -0,0 +1,44 @@ +from django.shortcuts import get_object_or_404 +from rest_framework.response import Response +from rest_framework.generics import RetrieveDestroyAPIView + +from common.permissions import IsAppUser +from common.utils import lazyproperty +from orgs.utils import tmp_to_root_org +from ..models import Ticket + + +__all__ = ['GenericTicketStatusRetrieveCloseAPI'] + + +class GenericTicketStatusRetrieveCloseAPI(RetrieveDestroyAPIView): + permission_classes = (IsAppUser, ) + + def retrieve(self, request, *args, **kwargs): + if self.ticket.action_open: + status = 'await' + elif self.ticket.action_approve: + status = 'approve' + else: + status = 'reject' + data = { + 'status': status, + 'action': self.ticket.action, + 'processor': self.ticket.processor_display + } + return Response(data=data, status=200) + + def destroy(self, request, *args, **kwargs): + if self.ticket.status_open: + self.ticket.close(processor=self.ticket.applicant) + data = { + 'action': self.ticket.action, + 'status': self.ticket.status, + 'processor': self.ticket.processor_display + } + return Response(data=data, status=200) + + @lazyproperty + def ticket(self): + with tmp_to_root_org(): + return get_object_or_404(Ticket, pk=self.kwargs['pk']) diff --git a/apps/tickets/const.py b/apps/tickets/const.py index 591ead607..3397353d4 100644 --- a/apps/tickets/const.py +++ b/apps/tickets/const.py @@ -10,6 +10,7 @@ class TicketTypeChoices(TextChoices): apply_asset = 'apply_asset', _('Apply for asset') apply_application = 'apply_application', _('Apply for application') login_asset_confirm = 'login_asset_confirm', _('Login asset confirm') + command_confirm = 'command_confirm', _('Command confirm') class TicketActionChoices(TextChoices): diff --git a/apps/tickets/handler/command_confirm.py b/apps/tickets/handler/command_confirm.py new file mode 100644 index 000000000..33d6739e3 --- /dev/null +++ b/apps/tickets/handler/command_confirm.py @@ -0,0 +1,30 @@ +from django.utils.translation import ugettext as _ +from .base import BaseHandler + + +class Handler(BaseHandler): + + # body + def _construct_meta_body_of_open(self): + apply_run_user = self.ticket.meta.get('apply_run_user') + apply_run_asset = self.ticket.meta.get('apply_run_asset') + apply_run_system_user = self.ticket.meta.get('apply_run_system_user') + apply_run_command = self.ticket.meta.get('apply_run_command') + apply_from_session = self.ticket.meta.get('apply_from_session') + apply_from_cmd_filter_rule = self.ticket.meta.get('apply_from_cmd_filter_rule') + + applied_body = '''{}: {}, + {}: {}, + {}: {}, + {}: {}, + {}: {}, + {}: {}, + '''.format( + _("Applied run user"), apply_run_user, + _("Applied run asset"), apply_run_asset, + _("Applied run system user"), apply_run_system_user, + _("Applied run command"), apply_run_command, + _("Applied from session"), apply_from_session, + _("Applied from command filter rules"), apply_from_cmd_filter_rule, + ) + return applied_body diff --git a/apps/tickets/migrations/0009_auto_20210426_1720.py b/apps/tickets/migrations/0009_auto_20210426_1720.py new file mode 100644 index 000000000..e584c2560 --- /dev/null +++ b/apps/tickets/migrations/0009_auto_20210426_1720.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2021-04-26 09:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0008_auto_20210311_1113'), + ] + + operations = [ + migrations.AlterField( + model_name='ticket', + name='type', + field=models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm'), ('apply_asset', 'Apply for asset'), ('apply_application', 'Apply for application'), ('login_asset_confirm', 'Login asset confirm'), ('command_confirm', 'Command confirm')], default='general', max_length=64, verbose_name='Type'), + ), + ] diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py b/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py new file mode 100644 index 000000000..92c72f920 --- /dev/null +++ b/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py @@ -0,0 +1,25 @@ +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + + +__all__ = [ + 'ApplySerializer', 'CommandConfirmSerializer', +] + + +class ApplySerializer(serializers.Serializer): + # 申请信息 + apply_run_user = serializers.CharField(required=True, label=_('Run user')) + apply_run_asset = serializers.CharField(required=True, label=_('Run asset')) + apply_run_system_user = serializers.CharField( + required=True, max_length=64, label=_('Run system user') + ) + apply_run_command = serializers.CharField(required=True, label=_('Run command')) + apply_from_session = serializers.UUIDField(required=False, label=_('From session')) + apply_from_cmd_filter_rule = serializers.UUIDField( + required=False, label=_('From cmd filter rule') + ) + + +class CommandConfirmSerializer(ApplySerializer): + pass From 5a3c67989b7ff58f81e7830f90573494661a7a46 Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 26 Apr 2021 18:52:25 +0800 Subject: [PATCH 15/39] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E5=A4=8D=E6=A0=B8=E9=80=BB=E8=BE=91;=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=91=BD=E4=BB=A4=E5=A4=8D=E6=A0=B8=E5=B7=A5=E5=8D=95?= =?UTF-8?q?;=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/models/cmd_filter.py | 4 ++-- apps/tickets/handler/command_confirm.py | 8 ++++---- apps/tickets/serializers/ticket/meta/meta.py | 9 ++++++++- .../ticket/meta/ticket_type/command_confirm.py | 4 ++-- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py index dff99298c..72547da19 100644 --- a/apps/assets/models/cmd_filter.py +++ b/apps/assets/models/cmd_filter.py @@ -115,8 +115,8 @@ class CommandFilterRule(OrgModelMixin): 'apply_run_asset': session.asset, 'apply_run_system_user': session.system_user, 'apply_run_command': run_command, - 'apply_from_session': str(session.id), - 'apply_from_cmd_filter_rule': str(cmd_filter_rule.id), + 'apply_from_session_id': str(session.id), + 'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id), }, 'org_id': org_id, } diff --git a/apps/tickets/handler/command_confirm.py b/apps/tickets/handler/command_confirm.py index 33d6739e3..0e1fd0d6a 100644 --- a/apps/tickets/handler/command_confirm.py +++ b/apps/tickets/handler/command_confirm.py @@ -10,8 +10,8 @@ class Handler(BaseHandler): apply_run_asset = self.ticket.meta.get('apply_run_asset') apply_run_system_user = self.ticket.meta.get('apply_run_system_user') apply_run_command = self.ticket.meta.get('apply_run_command') - apply_from_session = self.ticket.meta.get('apply_from_session') - apply_from_cmd_filter_rule = self.ticket.meta.get('apply_from_cmd_filter_rule') + apply_from_session_id = self.ticket.meta.get('apply_from_session_id') + apply_from_cmd_filter_rule_id = self.ticket.meta.get('apply_from_cmd_filter_rule_id') applied_body = '''{}: {}, {}: {}, @@ -24,7 +24,7 @@ class Handler(BaseHandler): _("Applied run asset"), apply_run_asset, _("Applied run system user"), apply_run_system_user, _("Applied run command"), apply_run_command, - _("Applied from session"), apply_from_session, - _("Applied from command filter rules"), apply_from_cmd_filter_rule, + _("Applied from session"), apply_from_session_id, + _("Applied from command filter rules"), apply_from_cmd_filter_rule_id, ) return applied_body diff --git a/apps/tickets/serializers/ticket/meta/meta.py b/apps/tickets/serializers/ticket/meta/meta.py index ee8a402dd..12b576857 100644 --- a/apps/tickets/serializers/ticket/meta/meta.py +++ b/apps/tickets/serializers/ticket/meta/meta.py @@ -1,5 +1,7 @@ from tickets import const -from .ticket_type import apply_asset, apply_application, login_confirm, login_asset_confirm +from .ticket_type import ( + apply_asset, apply_application, login_confirm, login_asset_confirm, command_confirm +) __all__ = [ 'type_serializer_classes_mapping', @@ -35,5 +37,10 @@ type_serializer_classes_mapping = { 'default': login_asset_confirm.LoginAssetConfirmSerializer, action_open: login_asset_confirm.ApplySerializer, action_approve: login_asset_confirm.LoginAssetConfirmSerializer(read_only=True), + }, + const.TicketTypeChoices.command_confirm.value: { + 'default': command_confirm.CommandConfirmSerializer, + action_open: command_confirm.ApplySerializer, + action_approve: command_confirm.CommandConfirmSerializer(read_only=True) } } diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py b/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py index 92c72f920..4dbdc08fc 100644 --- a/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py +++ b/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py @@ -15,8 +15,8 @@ class ApplySerializer(serializers.Serializer): required=True, max_length=64, label=_('Run system user') ) apply_run_command = serializers.CharField(required=True, label=_('Run command')) - apply_from_session = serializers.UUIDField(required=False, label=_('From session')) - apply_from_cmd_filter_rule = serializers.UUIDField( + apply_from_session_id = serializers.UUIDField(required=False, label=_('From session')) + apply_from_cmd_filter_rule_id = serializers.UUIDField( required=False, label=_('From cmd filter rule') ) From 74c7b18dc4ac5b9c3d85ba68f55d1afc743b099b Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 26 Apr 2021 18:54:08 +0800 Subject: [PATCH 16/39] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E5=A4=8D=E6=A0=B8=E9=80=BB=E8=BE=91;=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=91=BD=E4=BB=A4=E5=A4=8D=E6=A0=B8=E5=B7=A5=E5=8D=95?= =?UTF-8?q?;=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/cmd_filter.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/assets/api/cmd_filter.py b/apps/assets/api/cmd_filter.py index c7ffb792f..56cbbd6c3 100644 --- a/apps/assets/api/cmd_filter.py +++ b/apps/assets/api/cmd_filter.py @@ -46,8 +46,7 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet): class CommandConfirmAPI(CreateAPIView): - permission_classes = () - # permission_classes = (IsAppUser, ) + permission_classes = (IsAppUser, ) serializer_class = serializers.CommandConfirmSerializer def create(self, request, *args, **kwargs): @@ -91,6 +90,5 @@ class CommandConfirmAPI(CreateAPIView): class CommandConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI): - permission_classes = () pass From 7712c1659e8e971c4fb59453070afc5527237f5e Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 26 Apr 2021 19:39:00 +0800 Subject: [PATCH 17/39] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E5=A4=8D=E6=A0=B8=E9=80=BB=E8=BE=91;=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=91=BD=E4=BB=A4=E5=A4=8D=E6=A0=B8=E5=B7=A5=E5=8D=95?= =?UTF-8?q?;=204?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/locale/zh/LC_MESSAGES/django.mo | Bin 74719 -> 75634 bytes apps/locale/zh/LC_MESSAGES/django.po | 136 +++++++++++++++++++-------- 2 files changed, 96 insertions(+), 40 deletions(-) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 4e146d83b81d1f910f5ed64ed4565a09bf68c181..f47606a0d8ee91e55b19356e5a7572a55bcd1b20 100644 GIT binary patch delta 22848 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\n" "Language-Team: JumpServer team\n" @@ -43,12 +43,12 @@ msgstr "" msgid "Name" msgstr "名称" -#: acls/models/base.py:27 assets/models/cmd_filter.py:53 +#: acls/models/base.py:27 assets/models/cmd_filter.py:54 #: assets/models/user.py:122 msgid "Priority" msgstr "优先级" -#: acls/models/base.py:28 assets/models/cmd_filter.py:53 +#: acls/models/base.py:28 assets/models/cmd_filter.py:54 #: assets/models/user.py:122 msgid "1-100, the lower the value will be match first" msgstr "优先级可选范围为 1-100 (数值越小越优先)" @@ -66,7 +66,7 @@ msgstr "激活中" #: acls/models/base.py:32 applications/models/application.py:24 #: assets/models/asset.py:147 assets/models/asset.py:223 #: assets/models/base.py:255 assets/models/cluster.py:29 -#: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:57 +#: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:64 #: assets/models/domain.py:22 assets/models/domain.py:56 #: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37 #: orgs/models.py:26 perms/models/base.py:57 settings/models.py:34 @@ -84,11 +84,11 @@ msgstr "激活中" msgid "Comment" msgstr "备注" -#: acls/models/login_acl.py:16 tickets/const.py:18 +#: acls/models/login_acl.py:16 tickets/const.py:19 msgid "Reject" msgstr "拒绝" -#: acls/models/login_acl.py:17 assets/models/cmd_filter.py:47 +#: acls/models/login_acl.py:17 assets/models/cmd_filter.py:48 msgid "Allow" msgstr "允许" @@ -98,7 +98,7 @@ msgstr "登录IP" #: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:26 #: acls/serializers/login_acl.py:34 acls/serializers/login_asset_acl.py:75 -#: assets/models/cmd_filter.py:56 audits/models.py:57 +#: assets/models/cmd_filter.py:57 audits/models.py:57 #: authentication/templates/authentication/_access_key_modal.html:34 #: tickets/models/ticket.py:43 users/templates/users/_granted_assets.html:29 #: users/templates/users/user_asset_permission.html:44 @@ -117,7 +117,7 @@ msgstr "动作" #: authentication/models.py:97 orgs/models.py:18 orgs/models.py:418 #: perms/models/base.py:50 templates/index.html:78 #: terminal/backends/command/models.py:18 -#: terminal/backends/command/serializers.py:12 terminal/models/session.py:37 +#: terminal/backends/command/serializers.py:12 terminal/models/session.py:38 #: tickets/models/comment.py:17 users/models/user.py:159 #: users/models/user.py:707 users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 @@ -149,7 +149,7 @@ msgstr "系统用户" #: assets/serializers/system_user.py:192 audits/models.py:38 #: perms/models/asset_permission.py:99 templates/index.html:82 #: terminal/backends/command/models.py:19 -#: terminal/backends/command/serializers.py:13 terminal/models/session.py:39 +#: terminal/backends/command/serializers.py:13 terminal/models/session.py:40 #: users/templates/users/user_asset_permission.html:40 #: users/templates/users/user_asset_permission.html:70 #: users/templates/users/user_granted_remote_app.html:36 @@ -158,12 +158,12 @@ msgstr "系统用户" msgid "Asset" msgstr "资产" -#: acls/models/login_asset_acl.py:32 authentication/models.py:45 -#: users/templates/users/user_detail.html:258 +#: acls/models/login_asset_acl.py:32 assets/models/cmd_filter.py:62 +#: authentication/models.py:45 users/templates/users/user_detail.html:258 msgid "Reviewers" msgstr "审批人" -#: acls/models/login_asset_acl.py:86 tickets/const.py:12 +#: acls/models/login_asset_acl.py:89 tickets/const.py:12 msgid "Login asset confirm" msgstr "登录资产复核" @@ -281,7 +281,7 @@ msgstr "自定义" msgid "Category" msgstr "类别" -#: applications/models/application.py:16 assets/models/cmd_filter.py:52 +#: applications/models/application.py:16 assets/models/cmd_filter.py:53 #: perms/models/application_permission.py:23 #: perms/serializers/application/permission.py:17 #: perms/serializers/application/user_permission.py:34 @@ -348,7 +348,7 @@ msgstr "目标URL" #: applications/serializers/attrs/application_type/mysql_workbench.py:34 #: applications/serializers/attrs/application_type/vmware_client.py:30 #: assets/models/base.py:252 assets/serializers/asset_user.py:71 -#: audits/signals_handler.py:46 authentication/forms.py:22 +#: audits/signals_handler.py:58 authentication/forms.py:22 #: authentication/templates/authentication/login.html:155 #: settings/serializers/settings.py:93 users/forms/profile.py:21 #: users/templates/users/user_otp_check_password.html:13 @@ -516,7 +516,7 @@ msgstr "标签管理" #: assets/models/asset.py:221 assets/models/base.py:258 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:26 -#: assets/models/cmd_filter.py:60 assets/models/group.py:21 +#: assets/models/cmd_filter.py:67 assets/models/group.py:21 #: common/db/models.py:70 common/mixins/models.py:49 orgs/models.py:24 #: orgs/models.py:422 perms/models/base.py:55 users/models/user.py:571 #: users/serializers/group.py:35 users/templates/users/user_detail.html:97 @@ -624,30 +624,38 @@ msgid "Regex" msgstr "正则表达式" #: assets/models/cmd_filter.py:41 ops/models/command.py:25 -#: terminal/backends/command/serializers.py:15 terminal/models/session.py:48 +#: terminal/backends/command/serializers.py:15 terminal/models/session.py:49 msgid "Command" msgstr "命令" -#: assets/models/cmd_filter.py:46 +#: assets/models/cmd_filter.py:47 msgid "Deny" msgstr "拒绝" -#: assets/models/cmd_filter.py:51 +#: assets/models/cmd_filter.py:49 +msgid "Reconfirm" +msgstr "复核" + +#: assets/models/cmd_filter.py:52 msgid "Filter" msgstr "过滤器" -#: assets/models/cmd_filter.py:55 xpack/plugins/license/models.py:29 +#: assets/models/cmd_filter.py:56 xpack/plugins/license/models.py:29 msgid "Content" msgstr "内容" -#: assets/models/cmd_filter.py:55 +#: assets/models/cmd_filter.py:56 msgid "One line one command" msgstr "每行一个命令" -#: assets/models/cmd_filter.py:64 +#: assets/models/cmd_filter.py:71 msgid "Command filter rule" msgstr "命令过滤规则" +#: assets/models/cmd_filter.py:111 tickets/const.py:13 +msgid "Command confirm" +msgstr "命令复核" + #: assets/models/domain.py:64 msgid "Gateway" msgstr "网关" @@ -775,7 +783,7 @@ msgstr "用户组" #: perms/models/application_permission.py:31 #: perms/models/asset_permission.py:101 templates/_nav.html:45 #: terminal/backends/command/models.py:20 -#: terminal/backends/command/serializers.py:14 terminal/models/session.py:41 +#: terminal/backends/command/serializers.py:14 terminal/models/session.py:42 #: users/templates/users/_granted_assets.html:27 #: users/templates/users/user_asset_permission.html:42 #: users/templates/users/user_asset_permission.html:76 @@ -984,25 +992,25 @@ msgid "" "The task of self-checking is already running and cannot be started repeatedly" msgstr "自检程序已经在运行,不能重复启动" -#: assets/tasks/push_system_user.py:192 +#: assets/tasks/push_system_user.py:193 #: assets/tasks/system_user_connectivity.py:89 msgid "System user is dynamic: {}" msgstr "系统用户是动态的: {}" -#: assets/tasks/push_system_user.py:232 +#: assets/tasks/push_system_user.py:233 msgid "Start push system user for platform: [{}]" msgstr "推送系统用户到平台: [{}]" -#: assets/tasks/push_system_user.py:233 +#: assets/tasks/push_system_user.py:234 #: assets/tasks/system_user_connectivity.py:81 msgid "Hosts count: {}" msgstr "主机数量: {}" -#: assets/tasks/push_system_user.py:272 assets/tasks/push_system_user.py:298 +#: assets/tasks/push_system_user.py:273 assets/tasks/push_system_user.py:299 msgid "Push system users to assets: {}" msgstr "推送系统用户到入资产: {}" -#: assets/tasks/push_system_user.py:284 +#: assets/tasks/push_system_user.py:285 msgid "Push system users to asset: {}({}) => {}" msgstr "推送系统用户到入资产: {}({}) => {}" @@ -1076,7 +1084,7 @@ msgid "Symlink" msgstr "建立软链接" #: audits/models.py:37 audits/models.py:60 audits/models.py:71 -#: terminal/models/session.py:44 +#: terminal/models/session.py:45 msgid "Remote addr" msgstr "远端地址" @@ -1094,7 +1102,7 @@ msgid "Success" msgstr "成功" #: audits/models.py:43 ops/models/command.py:30 perms/models/base.py:53 -#: terminal/models/session.py:51 +#: terminal/models/session.py:52 #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:43 #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:74 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:40 @@ -1250,11 +1258,11 @@ msgstr "运行用户(显示名称)" msgid "User for display" msgstr "用户(显示名称)" -#: audits/signals_handler.py:45 +#: audits/signals_handler.py:57 msgid "SSH Key" msgstr "SSH 密钥" -#: audits/signals_handler.py:47 +#: audits/signals_handler.py:59 msgid "SSO" msgstr "" @@ -1486,7 +1494,7 @@ msgstr "删除成功" #: authentication/templates/authentication/_access_key_modal.html:155 #: authentication/templates/authentication/_mfa_confirm_modal.html:53 -#: templates/_modal.html:22 tickets/const.py:19 +#: templates/_modal.html:22 tickets/const.py:20 msgid "Close" msgstr "关闭" @@ -3036,7 +3044,7 @@ msgstr "正常" #: terminal/const.py:34 msgid "Offline" -msgstr "" +msgstr "离线" #: terminal/exceptions.py:8 msgid "Bulk create not support" @@ -3046,15 +3054,15 @@ msgstr "不支持批量创建" msgid "Storage is invalid" msgstr "存储无效" -#: terminal/models/session.py:43 +#: terminal/models/session.py:44 msgid "Login from" msgstr "登录来源" -#: terminal/models/session.py:47 +#: terminal/models/session.py:48 msgid "Replay" msgstr "回放" -#: terminal/models/session.py:52 +#: terminal/models/session.py:53 msgid "Date end" msgstr "结束日期" @@ -3201,7 +3209,7 @@ msgstr "文档类型" #: terminal/serializers/storage.py:185 msgid "Ignore Certificate Verification" -msgstr "" +msgstr "忽略证书认证" #: terminal/serializers/terminal.py:66 terminal/serializers/terminal.py:74 msgid "Not found" @@ -3291,15 +3299,15 @@ msgstr "申请资产" msgid "Apply for application" msgstr "申请应用" -#: tickets/const.py:16 tickets/const.py:23 +#: tickets/const.py:17 tickets/const.py:24 msgid "Open" msgstr "打开" -#: tickets/const.py:17 +#: tickets/const.py:18 msgid "Approve" msgstr "同意" -#: tickets/const.py:24 +#: tickets/const.py:25 msgid "Closed" msgstr "关闭" @@ -3414,6 +3422,30 @@ msgstr "工单申请信息" msgid "Ticket approved info" msgstr "工单批准信息" +#: tickets/handler/command_confirm.py:23 +msgid "Applied run user" +msgstr "申请运行的用户" + +#: tickets/handler/command_confirm.py:24 +msgid "Applied run asset" +msgstr "申请运行的资产" + +#: tickets/handler/command_confirm.py:25 +msgid "Applied run system user" +msgstr "申请运行的系统用户" + +#: tickets/handler/command_confirm.py:26 +msgid "Applied run command" +msgstr "申请运行的命令" + +#: tickets/handler/command_confirm.py:27 +msgid "Applied from session" +msgstr "申请来自会话" + +#: tickets/handler/command_confirm.py:28 +msgid "Applied from command filter rules" +msgstr "申请来自命令过滤规则" + #: tickets/handler/login_asset_confirm.py:16 msgid "Applied login user" msgstr "申请登录的用户" @@ -3551,6 +3583,30 @@ msgstr "批准的资产" msgid "No `Asset` are found under Organization `{}`" msgstr "在组织 `{}` 下没有发现 `资产`" +#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:12 +msgid "Run user" +msgstr "运行的用户" + +#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:13 +msgid "Run asset" +msgstr "运行的资产" + +#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:15 +msgid "Run system user" +msgstr "运行的系统用户" + +#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:17 +msgid "Run command" +msgstr "运行的命令" + +#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:18 +msgid "From session" +msgstr "来自会话" + +#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:20 +msgid "From cmd filter rule" +msgstr "来自命令过滤规则" + #: tickets/serializers/ticket/meta/ticket_type/common.py:11 msgid "Created by ticket ({}-{})" msgstr "通过工单创建 ({}-{})" From 4a9e83ba151ce3f4afbec055042e8732961a4463 Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 27 Apr 2021 17:48:07 +0800 Subject: [PATCH 18/39] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E5=A4=8D=E6=A0=B8=E9=80=BB=E8=BE=91;=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=91=BD=E4=BB=A4=E5=A4=8D=E6=A0=B8=E5=B7=A5=E5=8D=95?= =?UTF-8?q?;=205?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/models/cmd_filter.py | 1 + apps/tickets/handler/command_confirm.py | 2 ++ .../serializers/ticket/meta/ticket_type/command_confirm.py | 1 + 3 files changed, 4 insertions(+) diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py index 72547da19..1ef14bad0 100644 --- a/apps/assets/models/cmd_filter.py +++ b/apps/assets/models/cmd_filter.py @@ -117,6 +117,7 @@ class CommandFilterRule(OrgModelMixin): 'apply_run_command': run_command, 'apply_from_session_id': str(session.id), 'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id), + 'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id) }, 'org_id': org_id, } diff --git a/apps/tickets/handler/command_confirm.py b/apps/tickets/handler/command_confirm.py index 0e1fd0d6a..2d66db2d8 100644 --- a/apps/tickets/handler/command_confirm.py +++ b/apps/tickets/handler/command_confirm.py @@ -12,6 +12,7 @@ class Handler(BaseHandler): apply_run_command = self.ticket.meta.get('apply_run_command') apply_from_session_id = self.ticket.meta.get('apply_from_session_id') apply_from_cmd_filter_rule_id = self.ticket.meta.get('apply_from_cmd_filter_rule_id') + apply_from_cmd_filter_id = self.ticket.meta.get('apply_from_cmd_filter_id') applied_body = '''{}: {}, {}: {}, @@ -26,5 +27,6 @@ class Handler(BaseHandler): _("Applied run command"), apply_run_command, _("Applied from session"), apply_from_session_id, _("Applied from command filter rules"), apply_from_cmd_filter_rule_id, + _("Applied from command filter"), apply_from_cmd_filter_id, ) return applied_body diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py b/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py index 4dbdc08fc..eb631fe98 100644 --- a/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py +++ b/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py @@ -19,6 +19,7 @@ class ApplySerializer(serializers.Serializer): apply_from_cmd_filter_rule_id = serializers.UUIDField( required=False, label=_('From cmd filter rule') ) + apply_from_cmd_filter_id = serializers.UUIDField(required=False, label=_('From cmd filter')) class CommandConfirmSerializer(ApplySerializer): From d1a005f75019a0c3e279c79c66b71fe709cfcae9 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 25 Apr 2021 16:22:38 +0800 Subject: [PATCH 19/39] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96MFA=20verify=20?= =?UTF-8?q?requierd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/asset_user.py | 10 ++-------- apps/common/exceptions.py | 6 ++++++ apps/common/permissions.py | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/assets/api/asset_user.py b/apps/assets/api/asset_user.py index e2844a5e5..da0fe8c7e 100644 --- a/apps/assets/api/asset_user.py +++ b/apps/assets/api/asset_user.py @@ -10,10 +10,10 @@ from common.permissions import IsOrgAdminOrAppUser, NeedMFAVerify from common.utils import get_object_or_none, get_logger from common.mixins import CommonApiMixin from ..backends import AssetUserManager -from ..models import Asset, Node, SystemUser +from ..models import Node from .. import serializers from ..tasks import ( - test_asset_users_connectivity_manual, push_system_user_a_asset_manual + test_asset_users_connectivity_manual ) @@ -100,12 +100,6 @@ class AssetUserViewSet(CommonApiMixin, BulkModelViewSet): obj = queryset.get(id=pk) return obj - def get_exception_handler(self): - def handler(e, context): - logger.error(e, exc_info=True) - return Response({"error": str(e)}, status=400) - return handler - def perform_destroy(self, instance): manager = AssetUserManager() manager.delete(instance) diff --git a/apps/common/exceptions.py b/apps/common/exceptions.py index 3422f0e0c..9d3008c50 100644 --- a/apps/common/exceptions.py +++ b/apps/common/exceptions.py @@ -39,3 +39,9 @@ class ReferencedByOthers(JMSException): status_code = status.HTTP_400_BAD_REQUEST default_code = 'referenced_by_others' default_detail = _('Is referenced by other objects and cannot be deleted') + + +class MFAVerifyRequired(JMSException): + status_code = status.HTTP_400_BAD_REQUEST + default_code = 'mfa_verify_required' + default_detail = _('This action require verify your MFA') diff --git a/apps/common/permissions.py b/apps/common/permissions.py index 7df83046d..1fced6478 100644 --- a/apps/common/permissions.py +++ b/apps/common/permissions.py @@ -3,6 +3,7 @@ import time from rest_framework import permissions from django.conf import settings +from common.exceptions import MFAVerifyRequired from orgs.utils import current_org @@ -114,7 +115,7 @@ class NeedMFAVerify(permissions.BasePermission): mfa_verify_time = request.session.get('MFA_VERIFY_TIME', 0) if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL: return True - return False + raise MFAVerifyRequired() class CanUpdateDeleteUser(permissions.BasePermission): From 9fe5496ce920ddce9a4a78da1ae75e0d3841ac1e Mon Sep 17 00:00:00 2001 From: jing guo Date: Tue, 27 Apr 2021 18:19:54 +0800 Subject: [PATCH 20/39] =?UTF-8?q?=E9=80=9A=E8=BF=87=20api=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E8=B5=84=E4=BA=A7=EF=BC=8C=E4=B8=8D=E5=86=99=20protoc?= =?UTF-8?q?ols=20=E6=97=B6=EF=BC=8C=E9=BB=98=E8=AE=A4=E5=80=BC=E5=BA=94?= =?UTF-8?q?=E8=AF=A5=E6=98=AF=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/serializers/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index 9d8f9f27a..80e923b00 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -65,7 +65,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer): platform = serializers.SlugRelatedField( slug_field='name', queryset=Platform.objects.all(), label=_("Platform") ) - protocols = ProtocolsField(label=_('Protocols'), required=False) + protocols = ProtocolsField(label=_('Protocols'), required=False, default=['ssh/22']) domain_display = serializers.ReadOnlyField(source='domain.name', label=_('Domain name')) admin_user_display = serializers.ReadOnlyField(source='admin_user.name', label=_('Admin user name')) nodes_display = serializers.ListField(child=serializers.CharField(), label=_('Nodes name'), required=False) From fd203c67c377bfa0d5f22e3b0b81114b1a052446 Mon Sep 17 00:00:00 2001 From: xinwen Date: Thu, 22 Apr 2021 17:25:06 +0800 Subject: [PATCH 21/39] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E6=97=A0?= =?UTF-8?q?=E6=95=88=E7=9A=84=20es=20=E5=91=BD=E4=BB=A4=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E6=97=B6=EF=BC=8C=E6=8A=9B=E5=87=BA=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/terminal/backends/command/es.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/terminal/backends/command/es.py b/apps/terminal/backends/command/es.py index d8197391d..e03847e0a 100644 --- a/apps/terminal/backends/command/es.py +++ b/apps/terminal/backends/command/es.py @@ -7,6 +7,7 @@ import pytz from uuid import UUID import inspect +from django.utils.translation import gettext_lazy as _ from django.db.models import QuerySet as DJQuerySet from elasticsearch import Elasticsearch from elasticsearch.helpers import bulk @@ -14,12 +15,18 @@ from elasticsearch.exceptions import RequestError from common.utils.common import lazyproperty from common.utils import get_logger +from common.exceptions import JMSException from .models import AbstractSessionCommand logger = get_logger(__file__) +class InvalidElasticsearch(JMSException): + default_code = 'invalid_elasticsearch' + default_detail = _('Invalid elasticsearch config') + + class CommandStore(): def __init__(self, config): hosts = config.get("HOSTS") @@ -33,6 +40,8 @@ class CommandStore(): self.es = Elasticsearch(hosts=hosts, max_retries=0, **kwargs) def pre_use_check(self): + if not self.ping(timeout=3): + raise InvalidElasticsearch self._ensure_index_exists() def _ensure_index_exists(self): From f5d8e125cb6ef4519e160b414568b8f285035b47 Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 28 Apr 2021 15:26:40 +0800 Subject: [PATCH 22/39] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E8=B5=84=E4=BA=A7=E4=B8=8D=E4=BC=A0nodes=E6=97=B6?= =?UTF-8?q?=E6=8A=A5=E9=94=99=E7=9A=84=E9=97=AE=E9=A2=98=20&=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8DOption=E8=B5=84=E4=BA=A7API=E6=97=B6=E6=8A=A5JSON?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96=E5=A4=B1=E8=B4=A5=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 --- apps/assets/models/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index a21778a42..c009d00a1 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -35,7 +35,7 @@ def default_node(): try: from .node import Node root = Node.org_root() - return root + return Node.objects.filter(id=root.id) except: return None From 657a2ac7e79f292687e5db19dd89f94e779b1f6f Mon Sep 17 00:00:00 2001 From: xinwen Date: Thu, 15 Apr 2021 20:40:06 +0800 Subject: [PATCH 23/39] =?UTF-8?q?fix:=20=E5=91=BD=E4=BB=A4=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=AF=BC=E5=87=BA=E9=80=89=E6=8B=A9=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/terminal/api/command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/terminal/api/command.py b/apps/terminal/api/command.py index 814b2ecae..497e40fbe 100644 --- a/apps/terminal/api/command.py +++ b/apps/terminal/api/command.py @@ -17,6 +17,7 @@ from terminal.filters import CommandFilter from orgs.utils import current_org from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser from common.const.http import GET +from common.drf.api import JMSBulkModelViewSet from common.utils import get_logger from terminal.utils import send_command_alert_mail from terminal.serializers import InsecureCommandAlertSerializer @@ -94,7 +95,7 @@ class CommandQueryMixin: return date_from_st, date_to_st -class CommandViewSet(viewsets.ModelViewSet): +class CommandViewSet(JMSBulkModelViewSet): """接受app发送来的command log, 格式如下 { "user": "admin", From 4519ccfe1a46b6a47b02b3193c4891c9b880b9ad Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Wed, 28 Apr 2021 16:42:54 +0800 Subject: [PATCH 24/39] =?UTF-8?q?=E6=8E=88=E6=9D=83=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E4=BC=98=E5=8C=96=20(#6057)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 授权导入优化,支持使用 用户名,资产名,ip,节点路径,系统用户名称导入 * Update permission.py * 授权导入优化 * 授权导入优化 * 授权导入优化 * 授权导入优化 Co-authored-by: fghbng@qq.com --- apps/perms/models/asset_permission.py | 22 +++++++- apps/perms/serializers/asset/permission.py | 65 +++++++++++++++++----- 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index f7551306e..822cc363f 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -72,7 +72,7 @@ class Action: def value_to_choices_display(cls, value): choices = cls.value_to_choices(value) return [str(dict(cls.choices())[i]) for i in choices] - + @classmethod def choices_to_value(cls, value): if not isinstance(value, list): @@ -143,6 +143,26 @@ class AssetPermission(BasePermission): assets = Asset.objects.filter(id__in=asset_ids) return assets + def users_display(self): + names = [user.username for user in self.users.all()] + return names + + def user_groups_display(self): + names = [group.name for group in self.user_groups.all()] + return names + + def assets_display(self): + names = [asset.hostname for asset in self.assets.all()] + return names + + def system_users_display(self): + names = [system_user.name for system_user in self.system_users.all()] + return names + + def nodes_display(self): + names = [node.full_value for node in self.nodes.all()] + return names + class UserAssetGrantedTreeNodeRelation(OrgModelMixin, FamilyMixin, models.JMSBaseModel): class NodeFrom(ChoiceSet): diff --git a/apps/perms/serializers/asset/permission.py b/apps/perms/serializers/asset/permission.py index c676989df..67a42af4c 100644 --- a/apps/perms/serializers/asset/permission.py +++ b/apps/perms/serializers/asset/permission.py @@ -3,7 +3,7 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from django.db.models import Prefetch +from django.db.models import Prefetch, Q from orgs.mixins.serializers import BulkOrgResourceModelSerializer from perms.models import AssetPermission, Action @@ -41,21 +41,27 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): actions = ActionsField(required=False, allow_null=True) is_valid = serializers.BooleanField(read_only=True) is_expired = serializers.BooleanField(read_only=True, label=_('Is expired')) + users_display = serializers.ListField(child=serializers.CharField(), label=_('Users name'), required=False) + user_groups_display = serializers.ListField(child=serializers.CharField(), label=_('User groups name'), required=False) + assets_display = serializers.ListField(child=serializers.CharField(), label=_('Assets name'), required=False) + nodes_display = serializers.ListField(child=serializers.CharField(), label=_('Nodes name'), required=False) + system_users_display = serializers.ListField(child=serializers.CharField(), label=_('System users name'), required=False) class Meta: model = AssetPermission - mini_fields = ['id', 'name'] - small_fields = mini_fields + [ + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ 'is_active', 'is_expired', 'is_valid', 'actions', 'created_by', 'date_created', 'date_expired', 'date_start', 'comment' ] - m2m_fields = [ - 'users', 'user_groups', 'assets', 'nodes', 'system_users', + fields_m2m = [ + 'users', 'users_display', 'user_groups', 'user_groups_display', 'assets', 'assets_display', + 'nodes', 'nodes_display', 'system_users', 'system_users_display', 'users_amount', 'user_groups_amount', 'assets_amount', 'nodes_amount', 'system_users_amount', ] - fields = small_fields + m2m_fields + fields = fields_small + fields_m2m read_only_fields = ['created_by', 'date_created'] extra_kwargs = { 'is_expired': {'label': _('Is expired')}, @@ -71,11 +77,44 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related( - Prefetch('system_users', queryset=SystemUser.objects.only('id')), - Prefetch('user_groups', queryset=UserGroup.objects.only('id')), - Prefetch('users', queryset=User.objects.only('id')), - Prefetch('assets', queryset=Asset.objects.only('id')), - Prefetch('nodes', queryset=Node.objects.only('id')) - ) + queryset = queryset.prefetch_related('users', 'user_groups', 'assets', 'nodes', 'system_users') return queryset + + def to_internal_value(self, data): + # 系统用户是必填项 + system_users_display = data.pop('system_users_display', '') + for i in range(len(system_users_display)): + system_user = SystemUser.objects.filter(name=system_users_display[i]).first() + if system_user and system_user.id not in data['system_users']: + data['system_users'].append(system_user.id) + return super().to_internal_value(data) + + + def perform_display_create(self, instance, **kwargs): + # 用户 + users_to_set = User.objects.filter( + Q(name__in=kwargs.get('users_display')) | Q(username__in=kwargs.get('users_display')) + ).distinct() + instance.users.set(users_to_set) + # 用户组 + user_groups_to_set = UserGroup.objects.filter(name__in=kwargs.get('user_groups_display')).distinct() + instance.user_groups.set(user_groups_to_set) + # 资产 + assets_to_set = Asset.objects.filter( + Q(ip__in=kwargs.get('assets_display')) | Q(hostname__in=kwargs.get('assets_display')) + ).distinct() + instance.assets.set(assets_to_set) + # 节点 + nodes_to_set = Node.objects.filter(full_value__in=kwargs.get('nodes_display')).distinct() + instance.nodes.set(nodes_to_set) + + def create(self, validated_data): + display = { + 'users_display' : validated_data.pop('users_display', ''), + 'user_groups_display' : validated_data.pop('user_groups_display', ''), + 'assets_display' : validated_data.pop('assets_display', ''), + 'nodes_display' : validated_data.pop('nodes_display', '') + } + instance = super().create(validated_data) + self.perform_display_create(instance, **display) + return instance From 11e5a97f14152717c80572672febd408c0595fcc Mon Sep 17 00:00:00 2001 From: fit2cloud-jiangweidong <80373698+fit2cloud-jiangweidong@users.noreply.github.com> Date: Wed, 28 Apr 2021 17:03:20 +0800 Subject: [PATCH 25/39] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E6=9B=B4?= =?UTF-8?q?=E6=94=B9=E5=AF=86=E7=A0=81=E4=B8=8D=E5=8F=AF=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E5=89=8Dn=E6=AC=A1=E5=8E=86=E5=8F=B2=E5=AF=86=E7=A0=81?= =?UTF-8?q?=EF=BC=8C=E7=AE=A1=E7=90=86=E5=91=98=E5=8F=AF=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E5=AF=86=E7=A0=81=E9=87=8D=E5=A4=8D=E6=AC=A1?= =?UTF-8?q?=E6=95=B0=20(#6010)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数 * feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数, 判断是否为历史密码逻辑修改 * feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数, 提示内容更人性化 * fixs: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数, 最新国际化翻译文件 --- apps/jumpserver/conf.py | 1 + apps/jumpserver/settings/custom.py | 1 + apps/locale/zh/LC_MESSAGES/django.mo | Bin 75634 -> 72201 bytes apps/locale/zh/LC_MESSAGES/django.po | 37 +++++++++++++----- apps/settings/api/common.py | 1 + apps/settings/serializers/settings.py | 5 +++ .../migrations/0032_userpasswordhistory.py | 25 ++++++++++++ apps/users/models/user.py | 27 +++++++++++++ apps/users/serializers/profile.py | 9 ++++- apps/users/views/profile/reset.py | 8 ++++ 10 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 apps/users/migrations/0032_userpasswordhistory.py diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 179376828..b51c7dfba 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -259,6 +259,7 @@ class Config(dict): 'FTP_LOG_KEEP_DAYS': 200, 'ASSETS_PERM_CACHE_TIME': 3600 * 24, 'SECURITY_MFA_VERIFY_TTL': 3600, + 'OLD_PASSWORD_HISTORY_LIMIT_COUNT': 5, 'ASSETS_PERM_CACHE_ENABLE': HAS_XPACK, 'SYSLOG_ADDR': '', # '192.168.0.1:514' 'SYSLOG_FACILITY': 'user', diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 936b27582..ed37ea49c 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -38,6 +38,7 @@ 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_MIN_LENGTH = CONFIG.SECURITY_PASSWORD_MIN_LENGTH # Unit: bit +OLD_PASSWORD_HISTORY_LIMIT_COUNT = CONFIG.OLD_PASSWORD_HISTORY_LIMIT_COUNT SECURITY_PASSWORD_UPPER_CASE = CONFIG.SECURITY_PASSWORD_UPPER_CASE SECURITY_PASSWORD_LOWER_CASE = CONFIG.SECURITY_PASSWORD_LOWER_CASE SECURITY_PASSWORD_NUMBER = CONFIG.SECURITY_PASSWORD_NUMBER diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index f47606a0d8ee91e55b19356e5a7572a55bcd1b20..bdb351ceb42856cc8ad5002068b470d0ed9b3304 100644 GIT binary patch delta 22238 zcma*ub$r*=|M&6tXQN?s=jiT`QbME~QBoZ;a%`}Tjt|{P3>eMm9x*zlkq}V21ZAT| z1VK^cdOSbx7yh`e+x5rqefu8n=e$mzcWn68-@i{ZVQV7qt@MdzIb3J_9VaJ_&h0qq z5WVr}nHG*y*w1mwVq3~>TRF}YjKe`#>r=g#j3gDRD1m z#uJzqe=~E4`ET}ckYs5(@yG6j#CUvVmWMsrErqD z8?R7)fVc3_*N&4OmvnZVG`JO0;1NuPXU!Yt1JnjyVg~dJX5-B7WG0goi=!HpM-5QJ zY>evA7Ih-MFf~S^`i-*qB-BaGz$_Sp8fPaa$D^qBXE7~aMsG$k_sMAGZ_H#}+#O{= z4VWK`Vp-IV+hZ7pTKz5L@o^qw5&R7cW45mD!fRn-%B}HpY>&Dnaa}ooO%P8Y9X>~` zG;ue#oCb9wd97R$welLMXP`N1hh0$%i?sT2s9Q1z^(?Hm_#xDWe?+yv-;MLv!|=CN z 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 delta 25353 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 Xd@g;*n>xWgJKWFEp1>S_>C*o{s05_2 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 7afb36d4f..6124b8b26 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -2283,35 +2283,46 @@ msgstr "" "提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期" "提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户" -#: settings/serializers/settings.py:172 +#: settings/serializers/settings.py:168 +msgid "Number of repeated historical passwords" +msgstr "历史密码可重复次数" + +#: settings/serializers/settings.py:169 +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值即为输入框中填写的值)" + +#: settings/serializers/settings.py:173 msgid "Password minimum length" msgstr "密码最小长度" -#: settings/serializers/settings.py:175 +#: settings/serializers/settings.py:176 msgid "Must contain capital" msgstr "必须包含大写字符" -#: settings/serializers/settings.py:177 +#: settings/serializers/settings.py:178 msgid "Must contain lowercase" msgstr "必须包含小写字符" -#: settings/serializers/settings.py:178 +#: settings/serializers/settings.py:179 msgid "Must contain numeric" msgstr "必须包含数字" -#: settings/serializers/settings.py:179 +#: settings/serializers/settings.py:180 msgid "Must contain special" msgstr "必须包含特殊字符" -#: settings/serializers/settings.py:180 +#: settings/serializers/settings.py:181 msgid "Insecure command alert" msgstr "危险命令告警" -#: settings/serializers/settings.py:182 +#: settings/serializers/settings.py:183 msgid "Email recipient" msgstr "邮件收件人" -#: settings/serializers/settings.py:183 +#: settings/serializers/settings.py:184 msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" @@ -3721,7 +3732,11 @@ msgstr "旧密码错误" msgid "Password does not match security rules" msgstr "密码不满足安全规则" -#: users/serializers/profile.py:43 +#: users/serializers/profile.py:40 +msgid "The new password cannot be the last {} passwords" +msgstr "新密码不能是最近 {} 次的密码" + +#: users/serializers/profile.py:48 msgid "The newly set password is inconsistent" msgstr "两次密码不一致" @@ -4361,6 +4376,10 @@ msgstr "重置密码成功,返回到登录页面" msgid "Token invalid or expired" msgstr "Token错误或失效" +#: users/views/profile/reset.py:133 +msgid "* The new password cannot be the last {} passwords" +msgstr "* 新密码不能是最近 {} 次的密码" + #: users/views/profile/reset.py:120 msgid "User auth from {}, go there change password" msgstr "用户认证源来自 {}, 请去相应系统修改密码" diff --git a/apps/settings/api/common.py b/apps/settings/api/common.py index 8aafee063..1fa578c50 100644 --- a/apps/settings/api/common.py +++ b/apps/settings/api/common.py @@ -112,6 +112,7 @@ class PublicSettingApi(generics.RetrieveAPIView): "LOGIN_CONFIRM_ENABLE": settings.LOGIN_CONFIRM_ENABLE, "SECURITY_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA, "SECURITY_MFA_VERIFY_TTL": settings.SECURITY_MFA_VERIFY_TTL, + "OLD_PASSWORD_HISTORY_LIMIT_COUNT": settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT, "SECURITY_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION, "SECURITY_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME, "XPACK_LICENSE_IS_VALID": has_valid_xpack_license(), diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index 95757eb8b..b64b95cb6 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -167,6 +167,11 @@ class SecuritySettingSerializer(serializers.Serializer): label=_('User password expiration'), help_text=_('Tip: (unit: day) If the user does not update the password during the time, the user password will expire failure;The password expiration reminder mail will be automatic sent to the user by system within 5 days (daily) before the password expires') ) + 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)') + ) SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField( min_value=6, max_value=30, required=True, label=_('Password minimum length') diff --git a/apps/users/migrations/0032_userpasswordhistory.py b/apps/users/migrations/0032_userpasswordhistory.py new file mode 100644 index 000000000..8fcf1d2d1 --- /dev/null +++ b/apps/users/migrations/0032_userpasswordhistory.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1 on 2021-04-27 12:43 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0031_auto_20201118_1801'), + ] + + operations = [ + migrations.CreateModel( + name='UserPasswordHistory', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('password', models.CharField(max_length=128)), + ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history_passwords', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + ), + ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 1f8b2418a..9fd46e2cb 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -7,8 +7,11 @@ 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 from django.core.cache import cache from django.db import models from django.db.models import TextChoices @@ -70,6 +73,22 @@ class AuthMixin: def can_use_ssh_key_login(): return settings.TERMINAL_PUBLIC_KEY_AUTH + def is_history_password(self, password): + allow_history_password_count = settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT + history_passwords = self.history_passwords.all().order_by('-date_created')[:int(allow_history_password_count)] + + for history_password in history_passwords: + if check_password(password, history_password.password): + return True + 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. @@ -729,3 +748,11 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): if self.email and self.source == self.Source.local.value: return True return False + + +class UserPasswordHistory(models.Model): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + password = models.CharField(max_length=128) + 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")) diff --git a/apps/users/serializers/profile.py b/apps/users/serializers/profile.py index 68e387245..261dd6f01 100644 --- a/apps/users/serializers/profile.py +++ b/apps/users/serializers/profile.py @@ -30,12 +30,17 @@ class UserUpdatePasswordSerializer(serializers.ModelSerializer): raise serializers.ValidationError(msg) return value - @staticmethod - def validate_new_password(value): + def validate_new_password(self, value): from ..utils import check_password_rules if not check_password_rules(value): msg = _('Password does not match security rules') raise serializers.ValidationError(msg) + if self.instance.is_history_password(value): + 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/views/profile/reset.py b/apps/users/views/profile/reset.py index 73c34396b..8b1d9a102 100644 --- a/apps/users/views/profile/reset.py +++ b/apps/users/views/profile/reset.py @@ -128,6 +128,14 @@ class UserResetPasswordView(FormView): form.add_error('new_password', error) return self.form_invalid(form) + if user.is_history_password(password): + limit_count = settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT + 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) send_reset_password_success_mail(self.request, user) From e3511df4f8b47349cf63e8bd21257a22361e7e25 Mon Sep 17 00:00:00 2001 From: fit2cloud-jiangweidong <80373698+fit2cloud-jiangweidong@users.noreply.github.com> Date: Wed, 28 Apr 2021 19:25:30 +0800 Subject: [PATCH 26/39] =?UTF-8?q?feat:=20=E7=AE=A1=E7=90=86=E5=91=98?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E8=AE=BE=E7=BD=AE=E7=94=A8=E6=88=B7=E6=98=AF?= =?UTF-8?q?=E5=90=A6=E4=B8=8B=E6=AC=A1=E7=99=BB=E5=BD=95=E9=9C=80=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E5=AF=86=E7=A0=81=20(#6006)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 管理员可以设置用户是否下次登录需修改密码 * feat: 管理员可以设置用户下次是否需要更改密码,本次修改:字段命名规范化 * feat: 管理员可以设置用户下次是否需要更改密码,本次修改:字段命名规范化 * fix: 用户下次登录是否需要改密,函数名及变量名规范化 * fix: 管理员设置用户下次是否改密功能的国际化翻译文件 * fixs: 管理员设置用户下次登录是否需改密功能,逻辑修改 * fix: 管理员可设置用户下次登录是否需要改密,字段名称更改 --- apps/authentication/errors.py | 9 + apps/authentication/mixins.py | 10 + apps/authentication/urls/view_urls.py | 1 + apps/authentication/views/login.py | 19 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 72201 -> 76682 bytes apps/locale/zh/LC_MESSAGES/django.po | 507 ++++++++++-------- .../0033_user_need_update_password.py | 18 + apps/users/models/user.py | 2 + apps/users/serializers/user.py | 2 +- 9 files changed, 355 insertions(+), 213 deletions(-) create mode 100644 apps/users/migrations/0033_user_need_update_password.py diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index 8f9ba9307..46f3b8fc5 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -275,6 +275,15 @@ class PasswdTooSimple(JMSException): self.url = url +class PasswdNeedUpdate(JMSException): + default_code = 'passwd_need_update' + default_detail = _('The administrator require you to change your password this time') + + def __init__(self, url, *args, **kwargs): + super().__init__(*args, **kwargs) + self.url = url + + class PasswordRequireResetError(JMSException): default_code = 'passwd_has_expired' default_detail = _('Your password has expired, please reset before logging in') diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 89d7b85fd..b336307c7 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -194,6 +194,7 @@ class AuthMixin: self._check_login_acl(user, ip) self._check_password_require_reset_or_not(user) self._check_passwd_is_too_simple(user, password) + self._check_passwd_need_update(user) LoginBlockUtil(username, ip).clean_failed_count() request.session['auth_password'] = 1 @@ -224,6 +225,15 @@ class AuthMixin: ) raise errors.PasswdTooSimple(url) + @classmethod + def _check_passwd_need_update(cls, user: User): + if user.need_update_password: + url = cls.generate_reset_password_url_with_flash_msg( + user, 'authentication:passwd-need-update-flash-msg' + ) + raise errors.PasswdNeedUpdate(url) + + @classmethod def _check_password_require_reset_or_not(cls, user: User): if user.password_has_expired: diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index a95342fa6..062044047 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -22,6 +22,7 @@ urlpatterns = [ name='forgot-password-sendmail-success'), path('password/reset/', users_view.UserResetPasswordView.as_view(), name='reset-password'), path('password/too-simple-flash-msg/', views.FlashPasswdTooSimpleMsgView.as_view(), name='passwd-too-simple-flash-msg'), + path('password/need-update-flash-msg/', views.FlashPasswdNeedUpdateMsgView.as_view(), name='passwd-need-update-flash-msg'), path('password/has-expired-msg/', views.FlashPasswdHasExpiredMsgView.as_view(), name='passwd-has-expired-flash-msg'), path('password/reset/success/', users_view.UserResetPasswordSuccessView.as_view(), name='reset-password-success'), path('password/verify/', users_view.UserVerifyPasswordView.as_view(), name='user-verify-password'), diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index e1a3ab4b6..a43ddf5eb 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -31,7 +31,7 @@ from ..forms import get_user_login_form_cls __all__ = [ 'UserLoginView', 'UserLogoutView', 'UserLoginGuardView', 'UserLoginWaitConfirmView', - 'FlashPasswdTooSimpleMsgView', 'FlashPasswdHasExpiredMsgView' + 'FlashPasswdTooSimpleMsgView', 'FlashPasswdHasExpiredMsgView', 'FlashPasswdNeedUpdateMsgView' ] @@ -71,7 +71,7 @@ class UserLoginView(mixins.AuthMixin, FormView): context = self.get_context_data(form=new_form) self.request.session.set_test_cookie() return self.render_to_response(context) - except (errors.PasswdTooSimple, errors.PasswordRequireResetError) as e: + except (errors.PasswdTooSimple, errors.PasswordRequireResetError, errors.PasswdNeedUpdate) as e: return redirect(e.url) self.clear_rsa_key() return self.redirect_to_guard_view() @@ -241,6 +241,21 @@ class FlashPasswdTooSimpleMsgView(TemplateView): return self.render_to_response(context) +@method_decorator(never_cache, name='dispatch') +class FlashPasswdNeedUpdateMsgView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + context = { + 'title': _('Please change your password'), + 'messages': _('The administrator require you to change your password this time'), + 'interval': 8, + 'redirect_url': request.GET.get('redirect_url'), + 'auto_redirect': True, + } + return self.render_to_response(context) + + @method_decorator(never_cache, name='dispatch') class FlashPasswdHasExpiredMsgView(TemplateView): template_name = 'flash_message_standalone.html' diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index bdb351ceb42856cc8ad5002068b470d0ed9b3304..b661bf7fde6ed307d17345315bfec8f1a3006672 100644 GIT binary patch delta 25566 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 D-9Wyu delta 21441 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 Date: Thu, 29 Apr 2021 12:21:39 +0800 Subject: [PATCH 27/39] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E5=89=8D=E4=BF=AE=E6=94=B9=E5=AF=86=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/errors.py | 2 +- apps/authentication/mixins.py | 1 - apps/authentication/views/login.py | 15 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 76682 -> 72318 bytes apps/locale/zh/LC_MESSAGES/django.po | 689 +++++++++---------- apps/templates/flash_message_standalone.html | 8 +- 6 files changed, 341 insertions(+), 374 deletions(-) diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index 46f3b8fc5..03368baa8 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -277,7 +277,7 @@ class PasswdTooSimple(JMSException): class PasswdNeedUpdate(JMSException): default_code = 'passwd_need_update' - default_detail = _('The administrator require you to change your password this time') + default_detail = _('You should to change your password before login') def __init__(self, url, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index b336307c7..186f46911 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -233,7 +233,6 @@ class AuthMixin: ) raise errors.PasswdNeedUpdate(url) - @classmethod def _check_password_require_reset_or_not(cls, user: User): if user.password_has_expired: diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index a43ddf5eb..b57d8b5aa 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -31,7 +31,8 @@ from ..forms import get_user_login_form_cls __all__ = [ 'UserLoginView', 'UserLogoutView', 'UserLoginGuardView', 'UserLoginWaitConfirmView', - 'FlashPasswdTooSimpleMsgView', 'FlashPasswdHasExpiredMsgView', 'FlashPasswdNeedUpdateMsgView' + 'FlashPasswdTooSimpleMsgView', 'FlashPasswdHasExpiredMsgView', + 'FlashPasswdNeedUpdateMsgView' ] @@ -218,7 +219,7 @@ class UserLogoutView(TemplateView): context = { 'title': _('Logout success'), 'messages': _('Logout success, return login page'), - 'interval': 1, + 'interval': 3, 'redirect_url': reverse('authentication:login'), 'auto_redirect': True, } @@ -234,7 +235,7 @@ class FlashPasswdTooSimpleMsgView(TemplateView): context = { 'title': _('Please change your password'), 'messages': _('Your password is too simple, please change it for security'), - 'interval': 5, + 'interval': 3, 'redirect_url': request.GET.get('redirect_url'), 'auto_redirect': True, } @@ -248,10 +249,11 @@ class FlashPasswdNeedUpdateMsgView(TemplateView): def get(self, request, *args, **kwargs): context = { 'title': _('Please change your password'), - 'messages': _('The administrator require you to change your password this time'), - 'interval': 8, + 'messages': _('You should to change your password before login'), + 'interval': 3, 'redirect_url': request.GET.get('redirect_url'), 'auto_redirect': True, + 'confirm_button': _('Confirm') } return self.render_to_response(context) @@ -264,8 +266,9 @@ class FlashPasswdHasExpiredMsgView(TemplateView): context = { 'title': _('Please change your password'), 'messages': _('Your password has expired, please reset before logging in'), - 'interval': 5, + 'interval': 3, 'redirect_url': request.GET.get('redirect_url'), 'auto_redirect': True, + 'confirm_button': _('Confirm') } return self.render_to_response(context) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index b661bf7fde6ed307d17345315bfec8f1a3006672..c4b35117ec8a1141bf17650a9a19b576922d926f 100644 GIT binary patch delta 21543 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 t?c0gh$4$Ob)#=A8lE(=peHEh{{hkZf*1e* delta 25528 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 B#%2Hj diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index c6520c29b..cbe271ba3 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-04-27 18:00+0800\n" +"POT-Creation-Date: 2021-04-29 12:17+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -25,7 +25,7 @@ msgstr "" #: orgs/models.py:23 perms/models/base.py:49 settings/models.py:29 #: terminal/models/storage.py:23 terminal/models/storage.py:90 #: terminal/models/task.py:16 terminal/models/terminal.py:100 -#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:535 +#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:555 #: users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 @@ -61,7 +61,7 @@ msgstr "激活中" #: orgs/models.py:26 perms/models/base.py:57 settings/models.py:34 #: terminal/models/storage.py:29 terminal/models/storage.py:96 #: terminal/models/terminal.py:114 tickets/models/ticket.py:73 -#: users/models/group.py:16 users/models/user.py:568 +#: users/models/group.py:16 users/models/user.py:588 #: xpack/plugins/change_auth_plan/models.py:77 xpack/plugins/cloud/models.py:35 #: xpack/plugins/cloud/models.py:98 xpack/plugins/gathered_user/models.py:26 msgid "Comment" @@ -98,8 +98,9 @@ msgstr "动作" #: perms/models/base.py:50 templates/index.html:78 #: terminal/backends/command/models.py:18 #: terminal/backends/command/serializers.py:12 terminal/models/session.py:38 -#: tickets/models/comment.py:17 users/models/user.py:164 -#: users/models/user.py:712 users/serializers/group.py:20 +#: tickets/models/comment.py:17 users/models/user.py:184 +#: users/models/user.py:733 users/models/user.py:759 +#: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 #: users/templates/users/user_database_app_permission.html:37 @@ -137,7 +138,7 @@ msgstr "资产" msgid "Reviewers" msgstr "审批人" -#: acls/models/login_asset_acl.py:86 tickets/const.py:12 +#: acls/models/login_asset_acl.py:89 tickets/const.py:12 msgid "Login asset confirm" msgstr "登录资产复核" @@ -179,7 +180,7 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " #: applications/serializers/attrs/application_type/vmware_client.py:26 #: assets/models/base.py:251 assets/models/gathered_user.py:15 #: audits/models.py:100 authentication/forms.py:15 authentication/forms.py:17 -#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:533 +#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:553 #: users/templates/users/_select_user_modal.html:14 #: xpack/plugins/change_auth_plan/models.py:47 #: xpack/plugins/change_auth_plan/models.py:278 @@ -315,7 +316,7 @@ msgstr "目标URL" #: applications/serializers/attrs/application_type/mysql_workbench.py:34 #: applications/serializers/attrs/application_type/vmware_client.py:30 #: assets/models/base.py:252 assets/serializers/asset_user.py:71 -#: audits/signals_handler.py:46 authentication/forms.py:22 +#: audits/signals_handler.py:58 authentication/forms.py:22 #: authentication/templates/authentication/login.html:155 #: settings/serializers/settings.py:93 users/forms/profile.py:21 #: users/templates/users/user_otp_check_password.html:13 @@ -482,7 +483,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:24 -#: orgs/models.py:422 perms/models/base.py:55 users/models/user.py:576 +#: orgs/models.py:422 perms/models/base.py:55 users/models/user.py:596 #: users/serializers/group.py:35 xpack/plugins/change_auth_plan/models.py:81 #: xpack/plugins/cloud/models.py:104 xpack/plugins/gathered_user/models.py:30 msgid "Created by" @@ -496,7 +497,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:25 #: orgs/models.py:420 perms/models/base.py:56 users/models/group.py:18 -#: xpack/plugins/cloud/models.py:107 +#: users/models/user.py:760 xpack/plugins/cloud/models.py:107 msgid "Date created" msgstr "创建日期" @@ -542,7 +543,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:554 +#: assets/models/cluster.py:22 users/models/user.py:574 msgid "Phone" msgstr "手机" @@ -568,7 +569,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:724 +#: users/models/user.py:745 msgid "System" msgstr "系统" @@ -661,7 +662,7 @@ msgstr "新节点" msgid "empty" msgstr "空" -#: assets/models/node.py:546 perms/models/asset_permission.py:156 +#: assets/models/node.py:546 perms/models/asset_permission.py:176 msgid "Key" msgstr "键" @@ -669,7 +670,7 @@ msgstr "键" msgid "Full value" msgstr "全称" -#: assets/models/node.py:551 perms/models/asset_permission.py:157 +#: assets/models/node.py:551 perms/models/asset_permission.py:177 msgid "Parent key" msgstr "ssh私钥" @@ -786,7 +787,7 @@ msgstr "网域名称" msgid "Admin user name" msgstr "管理用户名称" -#: assets/serializers/asset.py:71 +#: assets/serializers/asset.py:71 perms/serializers/asset/permission.py:47 msgid "Nodes name" msgstr "节点名称" @@ -813,11 +814,11 @@ msgid "Backend" msgstr "后端" #: assets/serializers/asset_user.py:75 users/forms/profile.py:160 -#: users/models/user.py:565 users/templates/users/user_password_update.html:48 +#: users/models/user.py:585 users/templates/users/user_password_update.html:48 msgid "Public key" msgstr "SSH公钥" -#: assets/serializers/asset_user.py:79 users/models/user.py:562 +#: assets/serializers/asset_user.py:79 users/models/user.py:582 msgid "Private key" msgstr "ssh私钥" @@ -839,7 +840,7 @@ msgstr "网关数量" #: assets/serializers/label.py:13 assets/serializers/system_user.py:45 #: assets/serializers/system_user.py:166 -#: perms/serializers/asset/permission.py:66 +#: perms/serializers/asset/permission.py:72 msgid "Assets amount" msgstr "资产数量" @@ -861,7 +862,7 @@ msgid "The same level node name cannot be the same" msgstr "同级别节点名字不能重复" #: assets/serializers/system_user.py:44 assets/serializers/system_user.py:165 -#: perms/serializers/asset/permission.py:67 +#: perms/serializers/asset/permission.py:73 msgid "Nodes amount" msgstr "节点数量" @@ -1126,8 +1127,8 @@ msgstr "用户代理" #: audits/models.py:105 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/login_otp.html:6 -#: users/forms/profile.py:64 users/models/user.py:557 -#: users/serializers/profile.py:99 +#: users/forms/profile.py:64 users/models/user.py:577 +#: users/serializers/profile.py:104 msgid "MFA" msgstr "多因子认证" @@ -1195,11 +1196,11 @@ msgstr "运行用户(显示名称)" msgid "User for display" msgstr "用户(显示名称)" -#: audits/signals_handler.py:45 +#: audits/signals_handler.py:57 msgid "SSH Key" msgstr "SSH 密钥" -#: audits/signals_handler.py:47 +#: audits/signals_handler.py:59 msgid "SSO" msgstr "" @@ -1359,15 +1360,15 @@ msgstr "来源 IP 不被允许登录" msgid "SSO auth closed" msgstr "SSO 认证关闭了" -#: authentication/errors.py:271 authentication/views/login.py:236 +#: authentication/errors.py:271 authentication/views/login.py:237 msgid "Your password is too simple, please change it for security" msgstr "你的密码过于简单,为了安全,请修改" -#: authentication/errors.py:280 authentication/views/login.py:251 -msgid "The administrator require you to change your password this time" -msgstr "管理员要求您本次修改密码" +#: authentication/errors.py:280 authentication/views/login.py:252 +msgid "You should to change your password before login" +msgstr "登录完成前,请先修改密码" -#: authentication/errors.py:289 authentication/views/login.py:266 +#: authentication/errors.py:289 authentication/views/login.py:268 msgid "Your password has expired, please reset before logging in" msgstr "您的密码已过期,先修改再登录" @@ -1414,13 +1415,13 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:450 users/serializers/profile.py:96 +#: users/models/user.py:470 users/serializers/profile.py:101 #: users/templates/users/user_verify_mfa.html:32 msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:451 users/serializers/profile.py:97 +#: users/models/user.py:471 users/serializers/profile.py:102 msgid "Enable" msgstr "启用" @@ -1452,6 +1453,7 @@ msgid "Need MFA for view auth" msgstr "需要多因子认证来查看账号信息" #: authentication/templates/authentication/_mfa_confirm_modal.html:20 +#: authentication/views/login.py:256 authentication/views/login.py:272 #: templates/_modal.html:23 users/templates/users/user_password_verify.html:20 msgid "Confirm" msgstr "确认" @@ -1517,7 +1519,7 @@ msgid "Copy link" msgstr "复制链接" #: authentication/templates/authentication/login_wait_confirm.html:51 -#: templates/flash_message_standalone.html:34 +#: templates/flash_message_standalone.html:38 msgid "Return" msgstr "返回" @@ -1525,11 +1527,11 @@ msgstr "返回" msgid "Copy success" msgstr "复制成功" -#: authentication/views/login.py:57 +#: authentication/views/login.py:58 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:182 +#: authentication/views/login.py:183 msgid "" "Wait for {} confirm, You also can copy link to her/him
    \n" " Don't close this page" @@ -1537,19 +1539,20 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
    \n" " 不要关闭本页面" -#: authentication/views/login.py:187 +#: authentication/views/login.py:188 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:219 +#: authentication/views/login.py:220 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:220 +#: authentication/views/login.py:221 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/login.py:235 authentication/views/login.py:250 +#: authentication/views/login.py:236 authentication/views/login.py:251 +#: authentication/views/login.py:267 msgid "Please change your password" msgstr "请修改密码" @@ -1600,6 +1603,10 @@ msgstr "" msgid "Is referenced by other objects and cannot be deleted" msgstr "被其他对象关联,不能删除" +#: common/exceptions.py:47 +msgid "This action require verify your MFA" +msgstr "" + #: common/fields/model.py:80 msgid "Marshal dict data to char field" msgstr "" @@ -1873,7 +1880,7 @@ msgstr "组织审计员" msgid "GLOBAL" msgstr "全局组织" -#: orgs/models.py:419 users/models/user.py:545 +#: orgs/models.py:419 users/models/user.py:565 #: users/templates/users/_select_user_modal.html:15 msgid "Role" msgstr "角色" @@ -1886,7 +1893,7 @@ msgstr "管理员正在修改授权,请稍等" msgid "The authorization cannot be revoked for the time being" msgstr "该授权暂时不能撤销" -#: perms/models/application_permission.py:27 users/models/user.py:165 +#: perms/models/application_permission.py:27 users/models/user.py:185 msgid "Application" msgstr "应用程序" @@ -1927,7 +1934,7 @@ msgid "Clipboard copy paste" msgstr "剪贴板复制粘贴" #: perms/models/asset_permission.py:102 -#: perms/serializers/asset/permission.py:63 +#: perms/serializers/asset/permission.py:69 msgid "Actions" msgstr "动作" @@ -1936,16 +1943,16 @@ msgstr "动作" msgid "Asset permission" msgstr "资产授权" -#: perms/models/asset_permission.py:189 +#: perms/models/asset_permission.py:209 msgid "Ungrouped" msgstr "未分组" -#: perms/models/asset_permission.py:191 +#: perms/models/asset_permission.py:211 msgid "Favorite" msgstr "收藏夹" #: perms/models/base.py:51 templates/_nav.html:21 users/models/group.py:31 -#: users/models/user.py:541 users/templates/users/_select_user_modal.html:16 +#: users/models/user.py:561 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 @@ -1958,7 +1965,7 @@ msgstr "用户组" #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:77 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:43 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:81 -#: users/models/user.py:573 +#: users/models/user.py:593 msgid "Date expired" msgstr "失效日期" @@ -1969,24 +1976,48 @@ msgid "" msgstr "应用列表中包含与授权类型不同的应用。({})" #: perms/serializers/asset/permission.py:43 -#: perms/serializers/asset/permission.py:61 users/serializers/user.py:34 +#: perms/serializers/asset/permission.py:67 users/serializers/user.py:34 #: users/serializers/user.py:70 msgid "Is expired" msgstr "是否过期" -#: perms/serializers/asset/permission.py:62 users/serializers/user.py:69 +#: perms/serializers/asset/permission.py:44 +#, fuzzy +#| msgid "Username" +msgid "Users name" +msgstr "用户名" + +#: perms/serializers/asset/permission.py:45 +#, fuzzy +#| msgid "User groups amount" +msgid "User groups name" +msgstr "用户组数量" + +#: perms/serializers/asset/permission.py:46 +#, fuzzy +#| msgid "Asset num" +msgid "Assets name" +msgstr "资产数量" + +#: perms/serializers/asset/permission.py:48 +#, fuzzy +#| msgid "System users amount" +msgid "System users name" +msgstr "系统用户数量" + +#: perms/serializers/asset/permission.py:68 users/serializers/user.py:69 msgid "Is valid" msgstr "账户是否有效" -#: perms/serializers/asset/permission.py:64 users/serializers/group.py:36 +#: perms/serializers/asset/permission.py:70 users/serializers/group.py:36 msgid "Users amount" msgstr "用户数量" -#: perms/serializers/asset/permission.py:65 +#: perms/serializers/asset/permission.py:71 msgid "User groups amount" msgstr "用户组数量" -#: perms/serializers/asset/permission.py:68 +#: perms/serializers/asset/permission.py:74 msgid "System users amount" msgstr "系统用户数量" @@ -2198,7 +2229,9 @@ msgstr "密钥认证" msgid "" "Tips: If use other auth method, like AD/LDAP, you should disable this to " "avoid being able to log in after deleting" -msgstr "提示:如果你使用其它认证方式,如 AD/LDAP,你应该禁用此项,以避免第三方系统删除后,还可以登录" +msgstr "" +"提示:如果你使用其它认证方式,如 AD/LDAP,你应该禁用此项,以避免第三方系统删" +"除后,还可以登录" #: settings/serializers/settings.py:129 msgid "List sort by" @@ -2287,46 +2320,48 @@ msgstr "" "提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期" "提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户" -#: settings/serializers/settings.py:168 +#: settings/serializers/settings.py:172 msgid "Number of repeated historical passwords" msgstr "历史密码可重复次数" -#: settings/serializers/settings.py:169 +#: 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值即为输入框中填写的值)" +msgstr "" +"提示:用户重置密码时,不能为该用户前n次历史密码 (此处的n值即为输入框中填写的" +"值)" -#: settings/serializers/settings.py:173 +#: settings/serializers/settings.py:177 msgid "Password minimum length" msgstr "密码最小长度" -#: settings/serializers/settings.py:176 +#: settings/serializers/settings.py:180 msgid "Must contain capital" msgstr "必须包含大写字符" -#: settings/serializers/settings.py:178 +#: settings/serializers/settings.py:182 msgid "Must contain lowercase" msgstr "必须包含小写字符" -#: settings/serializers/settings.py:179 +#: settings/serializers/settings.py:183 msgid "Must contain numeric" msgstr "必须包含数字" -#: settings/serializers/settings.py:180 +#: settings/serializers/settings.py:184 msgid "Must contain special" msgstr "必须包含特殊字符" -#: settings/serializers/settings.py:181 +#: settings/serializers/settings.py:185 msgid "Insecure command alert" msgstr "危险命令告警" -#: settings/serializers/settings.py:183 +#: settings/serializers/settings.py:187 msgid "Email recipient" msgstr "邮件收件人" -#: settings/serializers/settings.py:184 +#: settings/serializers/settings.py:188 msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" @@ -2919,6 +2954,10 @@ msgstr "测试成功" msgid "Test failure: Account invalid" msgstr "测试失败: 账户无效" +#: terminal/backends/command/es.py:27 +msgid "Invalid elasticsearch config" +msgstr "" + #: terminal/backends/command/models.py:14 msgid "Ordinary" msgstr "普通" @@ -3651,7 +3690,7 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:101 users/models/user.py:537 +#: users/forms/profile.py:101 users/models/user.py:557 msgid "Email" msgstr "邮件" @@ -3683,48 +3722,48 @@ msgstr "复制你的公钥到这里" msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" -#: users/forms/profile.py:149 users/serializers/profile.py:71 -#: users/serializers/profile.py:145 users/serializers/profile.py:158 +#: users/forms/profile.py:149 users/serializers/profile.py:76 +#: users/serializers/profile.py:150 users/serializers/profile.py:163 msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/models/user.py:162 +#: users/models/user.py:182 msgid "System administrator" msgstr "系统管理员" -#: users/models/user.py:163 +#: users/models/user.py:183 msgid "System auditor" msgstr "系统审计员" -#: users/models/user.py:452 +#: users/models/user.py:472 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:517 +#: users/models/user.py:537 msgid "Local" msgstr "数据库" -#: users/models/user.py:548 +#: users/models/user.py:568 msgid "Avatar" msgstr "头像" -#: users/models/user.py:551 +#: users/models/user.py:571 msgid "Wechat" msgstr "微信" -#: users/models/user.py:581 +#: users/models/user.py:601 msgid "Source" msgstr "用户来源" -#: users/models/user.py:585 +#: users/models/user.py:605 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:720 +#: users/models/user.py:741 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:723 +#: users/models/user.py:744 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -3732,7 +3771,7 @@ msgstr "Administrator是初始的超级管理员" msgid "The old password is incorrect" msgstr "旧密码错误" -#: users/serializers/profile.py:37 users/serializers/user.py:113 +#: users/serializers/profile.py:36 users/serializers/user.py:113 msgid "Password does not match security rules" msgstr "密码不满足安全规则" @@ -3744,7 +3783,7 @@ msgstr "新密码不能是最近 {} 次的密码" msgid "The newly set password is inconsistent" msgstr "两次密码不一致" -#: users/serializers/profile.py:116 users/serializers/user.py:68 +#: users/serializers/profile.py:121 users/serializers/user.py:68 msgid "Is first login" msgstr "首次登录" @@ -3842,14 +3881,6 @@ msgstr "选择用户" msgid "Asset num" msgstr "资产数量" -#: users/templates/users/_user.html:21 -msgid "Auth" -msgstr "认证" - -#: users/templates/users/_user.html:27 -msgid "Security and Role" -msgstr "角色安全" - #: users/templates/users/_user_detail_nav_header.html:11 msgid "User detail" msgstr "用户详情" @@ -3967,229 +3998,6 @@ msgstr "包含" msgid "Exclude" msgstr "不包含" -#: users/templates/users/user_bulk_update.html:8 -msgid "Select properties that need to be modified" -msgstr "选择需要修改属性" - -#: users/templates/users/user_bulk_update.html:10 -msgid "Select all" -msgstr "全选" - -#: users/templates/users/user_create.html:4 -#: users/templates/users/user_list.html:7 -msgid "Create user" -msgstr "创建用户" - -#: users/templates/users/user_detail.html:80 -msgid "Force enabled" -msgstr "强制启用" - -#: users/templates/users/user_detail.html:101 -#: users/templates/users/user_profile.html:106 -msgid "Date joined" -msgstr "创建日期" - -#: users/templates/users/user_detail.html:105 -#: users/templates/users/user_profile.html:110 -msgid "Last login" -msgstr "最后登录" - -#: users/templates/users/user_detail.html:110 -#: users/templates/users/user_profile.html:115 -msgid "Last password updated" -msgstr "最后更新密码" - -#: users/templates/users/user_detail.html:126 -#: users/templates/users/user_profile.html:150 -msgid "Quick modify" -msgstr "快速修改" - -#: users/templates/users/user_detail.html:148 -msgid "Force enabled MFA" -msgstr "强制启用多因子认证" - -#: users/templates/users/user_detail.html:165 -msgid "Reset MFA" -msgstr "重置多因子认证" - -#: users/templates/users/user_detail.html:174 -msgid "Send reset password mail" -msgstr "发送重置密码邮件" - -#: users/templates/users/user_detail.html:177 -#: users/templates/users/user_detail.html:187 -msgid "Send" -msgstr "发送" - -#: users/templates/users/user_detail.html:184 -msgid "Send reset ssh key mail" -msgstr "发送重置密钥邮件" - -#: users/templates/users/user_detail.html:193 -#: users/templates/users/user_detail.html:490 -msgid "Unblock user" -msgstr "解除登录限制" - -#: users/templates/users/user_detail.html:196 -msgid "Unblock" -msgstr "解除" - -#: users/templates/users/user_detail.html:217 -msgid "Join user groups" -msgstr "添加到用户组" - -#: users/templates/users/user_detail.html:226 -msgid "Join" -msgstr "加入" - -#: users/templates/users/user_detail.html:356 -#: users/templates/users/user_detail.html:383 -msgid "Update successfully!" -msgstr "更新成功" - -#: users/templates/users/user_detail.html:365 -msgid "Goto profile page enable MFA" -msgstr "请去个人信息页面启用自己的多因子认证" - -#: users/templates/users/user_detail.html:401 -msgid "An e-mail has been sent to the user`s mailbox." -msgstr "已发送邮件到用户邮箱" - -#: users/templates/users/user_detail.html:411 -#: users/templates/users/user_detail.html:437 -#: users/templates/users/user_detail.html:505 -#: users/templates/users/user_list.html:178 -msgid "Are you sure?" -msgstr "你确认吗?" - -#: users/templates/users/user_detail.html:412 -msgid "This will reset the user password and send a reset mail" -msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" - -#: users/templates/users/user_detail.html:415 -#: users/templates/users/user_detail.html:441 -#: users/templates/users/user_detail.html:509 -#: users/templates/users/user_list.html:182 -msgid "Cancel" -msgstr "取消" - -#: users/templates/users/user_detail.html:427 -msgid "" -"The reset-ssh-public-key E-mail has been sent successfully. Please inform " -"the user to update his new ssh public key." -msgstr "重设密钥邮件将会发送到用户邮箱" - -#: users/templates/users/user_detail.html:428 -msgid "Reset SSH public key" -msgstr "重置SSH密钥" - -#: users/templates/users/user_detail.html:438 -msgid "This will reset the user public key and send a reset mail" -msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱" - -#: users/templates/users/user_detail.html:456 -msgid "Successfully updated the SSH public key." -msgstr "更新SSH密钥成功" - -#: users/templates/users/user_detail.html:457 -#: users/templates/users/user_detail.html:461 -msgid "User SSH public key update" -msgstr "SSH密钥" - -#: users/templates/users/user_detail.html:506 -msgid "After unlocking the user, the user can log in normally." -msgstr "解除用户登录限制后,此用户即可正常登录" - -#: users/templates/users/user_detail.html:520 -msgid "Reset user MFA success" -msgstr "重置用户多因子认证成功" - -#: users/templates/users/user_granted_remote_app.html:35 -msgid "App type" -msgstr "应用类型" - -#: users/templates/users/user_group_detail.html:17 -#: users/templates/users/user_group_granted_asset.html:18 -msgid "User group detail" -msgstr "用户组详情" - -#: users/templates/users/user_group_detail.html:81 -msgid "Add user" -msgstr "添加用户" - -#: users/templates/users/user_group_detail.html:87 -msgid "Add" -msgstr "添加" - -#: users/templates/users/user_group_list.html:7 -msgid "Create user group" -msgstr "创建用户组" - -#: users/templates/users/user_list.html:30 -msgid "Delete selected" -msgstr "批量删除" - -#: users/templates/users/user_list.html:32 -msgid "Remove selected" -msgstr "批量移除" - -#: users/templates/users/user_list.html:34 -msgid "Update selected" -msgstr "批量更新" - -#: users/templates/users/user_list.html:35 -msgid "Deactive selected" -msgstr "禁用所选" - -#: users/templates/users/user_list.html:36 -msgid "Active selected" -msgstr "激活所选" - -#: users/templates/users/user_list.html:106 -#: users/templates/users/user_list.html:110 -msgid "Remove" -msgstr "移除" - -#: users/templates/users/user_list.html:179 -msgid "This will delete the selected users !!!" -msgstr "删除选中用户 !!!" - -#: users/templates/users/user_list.html:190 -msgid "User Deleting failed." -msgstr "用户删除失败" - -#: users/templates/users/user_list.html:191 -msgid "User Delete" -msgstr "删除" - -#: users/templates/users/user_list.html:213 -msgid "This will remove the selected users !!" -msgstr "移除选中用户 !!!" - -#: users/templates/users/user_list.html:215 -msgid "User Removing failed." -msgstr "用户移除失败" - -#: users/templates/users/user_list.html:216 -msgid "User Remove" -msgstr "移除" - -#: users/templates/users/user_list.html:265 -msgid "Are you sure about removing it?" -msgstr "您确定移除吗?" - -#: users/templates/users/user_list.html:266 -msgid "Remove the success" -msgstr "移除成功" - -#: users/templates/users/user_list.html:271 -msgid "User is expired" -msgstr "用户已失效" - -#: users/templates/users/user_list.html:274 -msgid "User is inactive" -msgstr "用户已禁用" - #: users/templates/users/user_otp_check_password.html:6 #: users/templates/users/user_verify_mfa.html:6 msgid "Authenticate" @@ -4243,64 +4051,6 @@ msgstr "重置" msgid "Verify password" msgstr "校验密码" -#: users/templates/users/user_profile.html:97 -msgid "Administrator Settings force MFA login" -msgstr "管理员设置强制使用多因子认证" - -#: users/templates/users/user_profile.html:156 -msgid "Set MFA" -msgstr "设置多因子认证" - -#: users/templates/users/user_profile.html:178 -msgid "Update MFA" -msgstr "更改多因子认证" - -#: users/templates/users/user_profile.html:188 -msgid "Update password" -msgstr "更改密码" - -#: users/templates/users/user_profile.html:198 -msgid "Update SSH public key" -msgstr "更改SSH密钥" - -#: users/templates/users/user_profile.html:206 -msgid "Reset public key and download" -msgstr "重置并下载SSH密钥" - -#: users/templates/users/user_pubkey_update.html:55 -msgid "Old public key" -msgstr "原来SSH密钥" - -#: users/templates/users/user_pubkey_update.html:63 -msgid "Fingerprint" -msgstr "指纹" - -#: users/templates/users/user_pubkey_update.html:69 -msgid "Update public key" -msgstr "更新密钥" - -#: users/templates/users/user_pubkey_update.html:72 -msgid "Or reset by server" -msgstr "或者重置并下载密钥" - -#: users/templates/users/user_pubkey_update.html:98 -msgid "" -"The new public key has been set successfully, Please download the " -"corresponding private key." -msgstr "新的公钥已设置成功,请下载对应的私钥" - -#: users/templates/users/user_update.html:4 -msgid "Update user" -msgstr "更新用户" - -#: users/templates/users/user_update.html:22 users/views/profile/reset.py:120 -msgid "User auth from {}, go there change password" -msgstr "用户认证源来自 {}, 请去相应系统修改密码" - -#: users/templates/users/user_update.html:32 -msgid "User auth from {}, ssh key login is not supported" -msgstr "用户认证源来自 {}, 不支持使用 SSH Key 登录" - #: users/templates/users/user_verify_mfa.html:11 msgid "" "The account protection has been opened, please complete the following " @@ -4669,6 +4419,10 @@ msgstr "重置密码成功,返回到登录页面" msgid "Token invalid or expired" msgstr "Token错误或失效" +#: users/views/profile/reset.py:120 +msgid "User auth from {}, go there change password" +msgstr "用户认证源来自 {}, 请去相应系统修改密码" + #: users/views/profile/reset.py:133 msgid "* The new password cannot be the last {} passwords" msgstr "* 新密码不能是最近 {} 次的密码" @@ -5149,6 +4903,211 @@ msgstr "旗舰版" msgid "Community edition" msgstr "社区版" +#~ msgid "The administrator require you to change your password this time" +#~ msgstr "管理员要求您本次修改密码" + +#~ msgid "Auth" +#~ msgstr "认证" + +#~ msgid "Security and Role" +#~ msgstr "角色安全" + +#~ msgid "Select properties that need to be modified" +#~ msgstr "选择需要修改属性" + +#~ msgid "Select all" +#~ msgstr "全选" + +#~ msgid "Create user" +#~ msgstr "创建用户" + +#~ msgid "Force enabled" +#~ msgstr "强制启用" + +#~ msgid "Date joined" +#~ msgstr "创建日期" + +#~ msgid "Last login" +#~ msgstr "最后登录" + +#~ msgid "Last password updated" +#~ msgstr "最后更新密码" + +#~ msgid "Quick modify" +#~ msgstr "快速修改" + +#~ msgid "Force enabled MFA" +#~ msgstr "强制启用多因子认证" + +#~ msgid "Reset MFA" +#~ msgstr "重置多因子认证" + +#~ msgid "Send reset password mail" +#~ msgstr "发送重置密码邮件" + +#~ msgid "Send" +#~ msgstr "发送" + +#~ msgid "Send reset ssh key mail" +#~ msgstr "发送重置密钥邮件" + +#~ msgid "Unblock user" +#~ msgstr "解除登录限制" + +#~ msgid "Unblock" +#~ msgstr "解除" + +#~ msgid "Join user groups" +#~ msgstr "添加到用户组" + +#~ msgid "Join" +#~ msgstr "加入" + +#~ msgid "Update successfully!" +#~ msgstr "更新成功" + +#~ msgid "Goto profile page enable MFA" +#~ msgstr "请去个人信息页面启用自己的多因子认证" + +#~ msgid "An e-mail has been sent to the user`s mailbox." +#~ msgstr "已发送邮件到用户邮箱" + +#~ msgid "Are you sure?" +#~ msgstr "你确认吗?" + +#~ msgid "This will reset the user password and send a reset mail" +#~ msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" + +#~ msgid "Cancel" +#~ msgstr "取消" + +#~ msgid "" +#~ "The reset-ssh-public-key E-mail has been sent successfully. Please inform " +#~ "the user to update his new ssh public key." +#~ msgstr "重设密钥邮件将会发送到用户邮箱" + +#~ msgid "Reset SSH public key" +#~ msgstr "重置SSH密钥" + +#~ msgid "This will reset the user public key and send a reset mail" +#~ msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱" + +#~ msgid "Successfully updated the SSH public key." +#~ msgstr "更新SSH密钥成功" + +#~ msgid "User SSH public key update" +#~ msgstr "SSH密钥" + +#~ msgid "After unlocking the user, the user can log in normally." +#~ msgstr "解除用户登录限制后,此用户即可正常登录" + +#~ msgid "Reset user MFA success" +#~ msgstr "重置用户多因子认证成功" + +#~ msgid "App type" +#~ msgstr "应用类型" + +#~ msgid "User group detail" +#~ msgstr "用户组详情" + +#~ msgid "Add user" +#~ msgstr "添加用户" + +#~ msgid "Add" +#~ msgstr "添加" + +#~ msgid "Create user group" +#~ msgstr "创建用户组" + +#~ msgid "Delete selected" +#~ msgstr "批量删除" + +#~ msgid "Remove selected" +#~ msgstr "批量移除" + +#~ msgid "Update selected" +#~ msgstr "批量更新" + +#~ msgid "Deactive selected" +#~ msgstr "禁用所选" + +#~ msgid "Active selected" +#~ msgstr "激活所选" + +#~ msgid "Remove" +#~ msgstr "移除" + +#~ msgid "This will delete the selected users !!!" +#~ msgstr "删除选中用户 !!!" + +#~ msgid "User Deleting failed." +#~ msgstr "用户删除失败" + +#~ msgid "User Delete" +#~ msgstr "删除" + +#~ msgid "This will remove the selected users !!" +#~ msgstr "移除选中用户 !!!" + +#~ msgid "User Removing failed." +#~ msgstr "用户移除失败" + +#~ msgid "User Remove" +#~ msgstr "移除" + +#~ msgid "Are you sure about removing it?" +#~ msgstr "您确定移除吗?" + +#~ msgid "Remove the success" +#~ msgstr "移除成功" + +#~ msgid "User is expired" +#~ msgstr "用户已失效" + +#~ msgid "User is inactive" +#~ msgstr "用户已禁用" + +#~ msgid "Administrator Settings force MFA login" +#~ msgstr "管理员设置强制使用多因子认证" + +#~ msgid "Set MFA" +#~ msgstr "设置多因子认证" + +#~ msgid "Update MFA" +#~ msgstr "更改多因子认证" + +#~ msgid "Update password" +#~ msgstr "更改密码" + +#~ msgid "Update SSH public key" +#~ msgstr "更改SSH密钥" + +#~ msgid "Reset public key and download" +#~ msgstr "重置并下载SSH密钥" + +#~ msgid "Old public key" +#~ msgstr "原来SSH密钥" + +#~ msgid "Fingerprint" +#~ msgstr "指纹" + +#~ msgid "Update public key" +#~ msgstr "更新密钥" + +#~ msgid "Or reset by server" +#~ msgstr "或者重置并下载密钥" + +#~ msgid "" +#~ "The new public key has been set successfully, Please download the " +#~ "corresponding private key." +#~ msgstr "新的公钥已设置成功,请下载对应的私钥" + +#~ msgid "Update user" +#~ msgstr "更新用户" + +#~ msgid "User auth from {}, ssh key login is not supported" +#~ msgstr "用户认证源来自 {}, 不支持使用 SSH Key 登录" + #~ msgid "(Domain name support)" #~ msgstr "(支持域名)" diff --git a/apps/templates/flash_message_standalone.html b/apps/templates/flash_message_standalone.html index 84ef58cb5..bc27fa614 100644 --- a/apps/templates/flash_message_standalone.html +++ b/apps/templates/flash_message_standalone.html @@ -31,7 +31,13 @@ {% endif %} From fea0170c5e9d7bb41324c617c2d9ea0e8e54f153 Mon Sep 17 00:00:00 2001 From: Bai Date: Thu, 29 Apr 2021 11:30:01 +0800 Subject: [PATCH 28/39] =?UTF-8?q?perf:=20=E5=8F=AF=E4=BB=A5=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E5=8C=85=E5=90=AB=E5=AD=90=E5=AD=99=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E4=BD=86=E4=B8=8D=E5=8C=85=E5=90=AB=E5=AD=90=E5=AD=99=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E7=9A=84=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/node.py | 4 ++-- apps/assets/models/node.py | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index c756e9efd..a8164cd13 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -71,8 +71,8 @@ class NodeViewSet(OrgModelViewSet): if node.is_org_root(): error = _("You can't delete the root node ({})".format(node.value)) return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN) - if node.has_children_or_has_assets(): - error = _("Deletion failed and the node contains children or assets") + if node.has_offspring_assets(): + error = _("Deletion failed and the node contains assets") return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN) return super().destroy(request, *args, **kwargs) diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index 973df4b4a..2e3b890a5 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -38,8 +38,7 @@ def compute_parent_key(key): class NodeQuerySet(models.QuerySet): - def delete(self): - raise NotImplementedError + pass class FamilyMixin: @@ -622,14 +621,14 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin): tree_node = TreeNode(**data) return tree_node - def has_children_or_has_assets(self): - if self.children or self.get_assets().exists(): - return True - return False + def has_offspring_assets(self): + # 拥有后代资产 + return self.get_all_assets().exists() def delete(self, using=None, keep_parents=False): - if self.has_children_or_has_assets(): + if self.has_offspring_assets(): return + self.all_children.delete() return super().delete(using=using, keep_parents=keep_parents) def update_child_full_value(self): From 9a92e24e505a4cd06bc51b35f977c905c34baabb Mon Sep 17 00:00:00 2001 From: "fghbng@qq.com" Date: Thu, 29 Apr 2021 19:10:45 +0800 Subject: [PATCH 29/39] =?UTF-8?q?serializer=E4=BC=98=E5=8C=96&&=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E5=AF=BC=E5=85=A5=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/acls/serializers/login_acl.py | 11 +++- apps/acls/serializers/login_asset_acl.py | 12 ++-- apps/applications/serializers/application.py | 10 +++- apps/assets/serializers/admin_user.py | 10 +++- apps/assets/serializers/asset_user.py | 21 ++++--- apps/assets/serializers/cmd_filter.py | 16 ++++-- apps/assets/serializers/domain.py | 15 +++-- apps/assets/serializers/gathered_user.py | 11 ++-- apps/assets/serializers/label.py | 11 +++- apps/assets/serializers/system_user.py | 57 ++++++++++--------- apps/audits/serializers.py | 36 +++++++----- apps/ops/serializers/adhoc.py | 54 ++++++++++++------ apps/orgs/serializers.py | 13 ++++- .../serializers/application/permission.py | 8 +-- apps/perms/serializers/asset/permission.py | 11 ++-- apps/terminal/serializers/session.py | 15 +++-- apps/terminal/serializers/terminal.py | 25 +++++--- apps/tickets/serializers/comment.py | 8 ++- apps/tickets/serializers/ticket/ticket.py | 16 +++--- apps/users/serializers/user.py | 33 +++++++---- 20 files changed, 252 insertions(+), 141 deletions(-) diff --git a/apps/acls/serializers/login_acl.py b/apps/acls/serializers/login_acl.py index ee1a5ac25..b303db533 100644 --- a/apps/acls/serializers/login_acl.py +++ b/apps/acls/serializers/login_acl.py @@ -35,10 +35,15 @@ class LoginACLSerializer(BulkModelSerializer): class Meta: model = LoginACL - fields = [ - 'id', 'name', 'priority', 'ip_group', 'user', 'user_display', 'action', - 'action_display', 'is_active', 'comment', 'created_by', 'date_created', 'date_updated' + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ + 'priority', 'ip_group', 'action', 'action_display', + 'is_active', + 'date_created', 'date_updated', + 'comment', 'created_by', ] + fields_fk = ['user', 'user_display',] + fields = fields_small + fields_fk extra_kwargs = { 'priority': {'default': 50}, 'is_active': {'default': True}, diff --git a/apps/acls/serializers/login_asset_acl.py b/apps/acls/serializers/login_asset_acl.py index f451a912a..bbb31af94 100644 --- a/apps/acls/serializers/login_asset_acl.py +++ b/apps/acls/serializers/login_asset_acl.py @@ -76,11 +76,15 @@ class LoginAssetACLSerializer(BulkOrgResourceModelSerializer): class Meta: model = models.LoginAssetACL - fields = [ - 'id', 'name', 'priority', 'users', 'system_users', 'assets', 'action', 'action_display', - 'is_active', 'comment', 'reviewers', 'reviewers_amount', 'created_by', 'date_created', - 'date_updated', 'org_id' + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ + 'users', 'system_users', 'assets', + 'is_active', + 'date_created', 'date_updated', + 'priority', 'action', 'action_display', 'comment', 'created_by', 'org_id' ] + fields_m2m = ['reviewers', 'reviewers_amount'] + fields = fields_small + fields_m2m extra_kwargs = { "reviewers": {'allow_null': False, 'required': True}, 'priority': {'default': 50}, diff --git a/apps/applications/serializers/application.py b/apps/applications/serializers/application.py index 580ce9a12..bff8c270a 100644 --- a/apps/applications/serializers/application.py +++ b/apps/applications/serializers/application.py @@ -49,10 +49,14 @@ class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSeri class Meta: model = models.Application - fields = [ - 'id', 'name', 'category', 'category_display', 'type', 'type_display', 'attrs', - 'domain', 'created_by', 'date_created', 'date_updated', 'comment' + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ + 'category', 'category_display', 'type', 'type_display', 'attrs', + 'date_created', 'date_updated', + 'created_by', 'comment' ] + fields_fk = ['domain'] + fields = fields_small + fields_fk read_only_fields = [ 'created_by', 'date_created', 'date_updated', 'get_type_display', ] diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py index 0211d88cc..2e1913b47 100644 --- a/apps/assets/serializers/admin_user.py +++ b/apps/assets/serializers/admin_user.py @@ -16,10 +16,14 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class Meta: model = AdminUser - fields = [ - 'id', 'name', 'username', 'password', 'private_key', 'public_key', - 'comment', 'assets_amount', 'date_created', 'date_updated', 'created_by', + fields_mini = ['id', 'name', 'username'] + fields_write_only = ['password', 'private_key', 'public_key'] + fields_small = fields_mini + fields_write_only + [ + 'date_created', 'date_updated', + 'comment', 'created_by' ] + fields_fk = ['assets_amount'] + fields = fields_small + fields_fk read_only_fields = ['date_created', 'date_updated', 'created_by', 'assets_amount'] extra_kwargs = { diff --git a/apps/assets/serializers/asset_user.py b/apps/assets/serializers/asset_user.py index 7eba6ba41..19cb2adc7 100644 --- a/apps/assets/serializers/asset_user.py +++ b/apps/assets/serializers/asset_user.py @@ -22,10 +22,11 @@ class AssetUserWriteSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializ class Meta: model = AuthBook list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'username', 'password', 'private_key', "public_key", - 'asset', 'comment', - ] + fields_mini = ['id', 'username'] + fields_write_only = ['password', 'private_key', "public_key"] + fields_small = fields_mini + fields_write_only + ['comment'] + fields_fk = ['asset'] + fields = fields_small + fields_fk extra_kwargs = { 'username': {'required': True}, 'password': {'write_only': True}, @@ -52,11 +53,15 @@ class AssetUserReadSerializer(AssetUserWriteSerializer): 'date_created', 'date_updated', 'created_by', 'version', ) - fields = [ - 'id', 'username', 'password', 'private_key', "public_key", - 'asset', 'hostname', 'ip', 'backend', 'version', - 'date_created', "date_updated", 'comment', + fields_mini = ['id', 'username'] + fields_write_only = ['password', 'private_key', "public_key"] + fields_small = fields_mini + fields_write_only + [ + 'backend', 'version', + 'date_created', "date_updated", + 'comment' ] + fields_fk = ['asset', 'hostname', 'ip'] + fields = fields_small + fields_fk extra_kwargs = { 'username': {'required': True}, 'password': {'write_only': True}, diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py index 2e60b31d7..052452825 100644 --- a/apps/assets/serializers/cmd_filter.py +++ b/apps/assets/serializers/cmd_filter.py @@ -16,11 +16,16 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer): class Meta: model = CommandFilter list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'name', 'org_id', 'org_name', 'is_active', 'comment', - 'created_by', 'date_created', 'date_updated', 'rules', 'system_users' + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ + 'org_id', 'org_name', + 'is_active', + 'date_created', 'date_updated', + 'comment', 'created_by', ] - + fields_fk = ['rules'] + fields_m2m = ['system_users'] + fields = fields_small + fields_fk + fields_m2m extra_kwargs = { 'rules': {'read_only': True}, 'system_users': {'required': False}, @@ -38,7 +43,8 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer): fields_small = fields_mini + [ 'type', 'type_display', 'content', 'priority', 'action', 'action_display', 'reviewers', - 'comment', 'created_by', 'date_created', 'date_updated' + 'date_created', 'date_updated', + 'comment', 'created_by', ] fields_fk = ['filter'] fields = '__all__' diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index f7b402f0e..5f0a44ec2 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -48,11 +48,18 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class Meta: model = Gateway list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'name', 'ip', 'port', 'protocol', 'username', 'password', - 'private_key', 'public_key', 'domain', 'is_active', 'date_created', - 'date_updated', 'created_by', 'comment', + fields_mini = ['id', 'name'] + fields_write_only = [ + 'password', 'private_key', 'public_key', ] + fields_small = fields_mini + fields_write_only + [ + 'username', 'ip', 'port', 'protocol', + 'is_active', + 'date_created', 'date_updated', + 'created_by', 'comment', + ] + fields_fk = ['domain'] + fields = fields_small + fields_fk extra_kwargs = { 'password': {'validators': [NoSpecialChars()]} } diff --git a/apps/assets/serializers/gathered_user.py b/apps/assets/serializers/gathered_user.py index 2629a8327..86e7dcfae 100644 --- a/apps/assets/serializers/gathered_user.py +++ b/apps/assets/serializers/gathered_user.py @@ -10,11 +10,14 @@ from ..models import GatheredUser class GatheredUserSerializer(OrgResourceModelSerializerMixin): class Meta: model = GatheredUser - fields = [ - 'id', 'asset', 'hostname', 'ip', 'username', - 'date_last_login', 'ip_last_login', - 'present', 'date_created', 'date_updated' + fields_mini = ['id'] + fields_small = fields_mini + [ + 'username', 'ip_last_login', + 'present', + 'date_last_login', 'date_created', 'date_updated' ] + fields_fk = ['asset', 'hostname', 'ip'] + fields = fields_small + fields_fk read_only_fields = fields extra_kwargs = { 'hostname': {'label': _("Hostname")}, diff --git a/apps/assets/serializers/label.py b/apps/assets/serializers/label.py index a98d4757d..f922655ef 100644 --- a/apps/assets/serializers/label.py +++ b/apps/assets/serializers/label.py @@ -15,10 +15,15 @@ class LabelSerializer(BulkOrgResourceModelSerializer): class Meta: model = Label - fields = [ - 'id', 'name', 'value', 'category', 'is_active', 'comment', - 'date_created', 'asset_count', 'assets', 'category_display' + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ + 'value', 'category', 'category_display', + 'is_active', + 'date_created', + 'comment', ] + fields_m2m = ['asset_count', 'assets'] + fields = fields_small + fields_m2m read_only_fields = ( 'category', 'date_created', 'asset_count', ) diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index 1b3b5de5c..e1ada114d 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -26,16 +26,18 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class Meta: model = SystemUser list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'name', 'username', 'protocol', - 'password', 'public_key', 'private_key', - 'login_mode', 'login_mode_display', - 'priority', 'username_same_with_user', - 'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment', - 'auto_generate_key', 'sftp_root', 'token', - 'assets_amount', 'date_created', 'date_updated', 'created_by', - 'home', 'system_groups', 'ad_domain' + fields_mini = ['id', 'name', 'username'] + fields_write_only = ['password', 'public_key', 'private_key'] + fields_small = fields_mini + fields_write_only + [ + 'protocol', 'login_mode', 'login_mode_display', 'priority', + 'sudo', 'shell', 'sftp_root', 'token', + 'home', 'system_groups', 'ad_domain', + 'username_same_with_user', 'auto_push', 'auto_generate_key', + 'date_created', 'date_updated', + 'comment', 'created_by', ] + fields_m2m = [ 'cmd_filters', 'assets_amount'] + fields = fields_small + fields_m2m extra_kwargs = { 'password': {"write_only": True}, 'public_key': {"write_only": True}, @@ -147,17 +149,18 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class SystemUserListSerializer(SystemUserSerializer): class Meta(SystemUserSerializer.Meta): - fields = [ - 'id', 'name', 'username', 'protocol', - 'password', 'public_key', 'private_key', - 'login_mode', 'login_mode_display', - 'priority', "username_same_with_user", - 'auto_push', 'sudo', 'shell', 'comment', - "assets_amount", 'home', 'system_groups', - 'auto_generate_key', 'ad_domain', - 'sftp_root', 'created_by', 'date_created', - 'date_updated', + fields_mini = ['id', 'name', 'username'] + fields_write_only = ['password', 'public_key', 'private_key'] + fields_small = fields_mini + fields_write_only + [ + 'protocol', 'login_mode', 'login_mode_display', 'priority', + 'sudo', 'shell', 'home', 'system_groups', + 'ad_domain', 'sftp_root', + "username_same_with_user", 'auto_push', 'auto_generate_key', + 'date_created', 'date_updated', + 'comment', 'created_by', ] + fields_m2m = ["assets_amount",] + fields = fields_small + fields_m2m extra_kwargs = { 'password': {"write_only": True}, 'public_key': {"write_only": True}, @@ -178,15 +181,15 @@ class SystemUserListSerializer(SystemUserSerializer): class SystemUserWithAuthInfoSerializer(SystemUserSerializer): class Meta(SystemUserSerializer.Meta): - fields = [ - 'id', 'name', 'username', 'protocol', - 'password', 'public_key', 'private_key', - 'login_mode', 'login_mode_display', - 'priority', 'username_same_with_user', - 'auto_push', 'sudo', 'shell', 'comment', - 'auto_generate_key', 'sftp_root', 'token', - 'ad_domain', + fields_mini = ['id', 'name', 'username'] + fields_write_only = ['password', 'public_key', 'private_key'] + fields_small = fields_mini + fields_write_only + [ + 'protocol', 'login_mode', 'login_mode_display', 'priority', + 'sudo', 'shell', 'ad_domain', 'sftp_root', 'token', + "username_same_with_user", 'auto_push', 'auto_generate_key', + 'comment', ] + fields = fields_small extra_kwargs = { 'nodes_amount': {'label': _('Node')}, 'assets_amount': {'label': _('Asset')}, diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index 74eebbecc..d7ab3e996 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -16,10 +16,14 @@ class FTPLogSerializer(serializers.ModelSerializer): class Meta: model = models.FTPLog - fields = ( - 'id', 'user', 'remote_addr', 'asset', 'system_user', 'org_id', - 'operate', 'filename', 'is_success', 'date_start', 'operate_display' - ) + fields_mini = ['id'] + fields_small = fields_mini + [ + 'user', 'remote_addr', 'asset', 'system_user', 'org_id', + 'operate', 'filename', 'operate_display', + 'is_success', + 'date_start', + ] + fields = fields_small class UserLoginLogSerializer(serializers.ModelSerializer): @@ -29,11 +33,14 @@ class UserLoginLogSerializer(serializers.ModelSerializer): class Meta: model = models.UserLoginLog - fields = ( - 'id', 'username', 'type', 'type_display', 'ip', 'city', 'user_agent', - 'mfa', 'reason', 'status', 'status_display', 'datetime', 'mfa_display', - 'backend' - ) + fields_mini = ['id'] + fields_small = fields_mini + [ + 'username', 'type', 'type_display', 'ip', 'city', 'user_agent', + 'mfa', 'mfa_display', 'reason', 'backend', + 'status', 'status_display', + 'datetime', + ] + fields = fields_small extra_kwargs = { "user_agent": {'label': _('User agent')} } @@ -42,10 +49,13 @@ class UserLoginLogSerializer(serializers.ModelSerializer): class OperateLogSerializer(serializers.ModelSerializer): class Meta: model = models.OperateLog - fields = ( - 'id', 'user', 'action', 'resource_type', 'resource', - 'remote_addr', 'datetime', 'org_id' - ) + fields_mini = ['id'] + fields_small = fields_mini + [ + 'user', 'action', 'resource_type', 'resource', 'remote_addr', + 'datetime', + 'org_id' + ] + fields = fields_small class PasswordChangeLogSerializer(serializers.ModelSerializer): diff --git a/apps/ops/serializers/adhoc.py b/apps/ops/serializers/adhoc.py index 9327ce0a8..badbed2b4 100644 --- a/apps/ops/serializers/adhoc.py +++ b/apps/ops/serializers/adhoc.py @@ -14,11 +14,15 @@ class AdHocExecutionSerializer(serializers.ModelSerializer): class Meta: model = AdHocExecution - fields = [ - 'id', 'task', 'task_display', 'hosts_amount', 'adhoc', 'date_start', 'stat', - 'date_finished', 'timedelta', 'is_finished', 'is_success', 'result', 'summary', - 'short_id', 'adhoc_short_id', 'last_success', 'last_failure' + fields_mini = ['id'] + fields_small = fields_mini + [ + 'hosts_amount', 'timedelta', 'result', 'summary', 'short_id', + 'is_finished', 'is_success', + 'date_start', 'date_finished', ] + fields_fk = ['task', 'task_display', 'adhoc', 'adhoc_short_id',] + fields_custom = ['stat', 'last_success', 'last_failure'] + fields = fields_small + fields_fk + fields_custom @staticmethod def get_task(obj): @@ -52,11 +56,16 @@ class TaskSerializer(BulkOrgResourceModelSerializer): class Meta: model = Task - fields = [ - 'id', 'name', 'interval', 'crontab', 'is_periodic', - 'is_deleted', 'comment', 'date_created', - 'date_updated', 'latest_execution', 'summary', + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ + 'interval', 'crontab', + 'is_periodic', 'is_deleted', + 'date_created', 'date_updated', + 'comment', ] + fields_fk = ['latest_execution'] + fields_custom = ['summary'] + fields = fields_small + fields_fk + fields_custom read_only_fields = [ 'is_deleted', 'date_created', 'date_updated', 'latest_adhoc', 'latest_execution', 'total_run_amount', @@ -77,12 +86,16 @@ class AdHocSerializer(serializers.ModelSerializer): class Meta: model = AdHoc - fields = [ - "id", "task", 'tasks', "pattern", "options", - "hosts", "run_as_admin", "run_as", "become", - "date_created", "short_id", - "become_display", + fields_mini = ['id'] + fields_small = fields_mini + [ + 'tasks', "pattern", "options", "run_as", + "become", "become_display", "short_id", + "run_as_admin", + "date_created", ] + fields_fk = ["task"] + fields_m2m = ["hosts"] + fields = fields_small + fields_fk + fields_m2m read_only_fields = [ 'date_created' ] @@ -99,8 +112,8 @@ class AdHocExecutionNestSerializer(serializers.ModelSerializer): class Meta: model = AdHocExecution fields = ( - 'last_success', 'last_failure', 'last_run', 'timedelta', 'is_finished', - 'is_success' + 'last_success', 'last_failure', 'last_run', 'timedelta', + 'is_finished', 'is_success' ) @@ -120,10 +133,15 @@ class CommandExecutionSerializer(serializers.ModelSerializer): class Meta: model = CommandExecution - fields = [ - 'id', 'hosts', 'run_as', 'command', 'result', 'log_url', - 'is_finished', 'date_created', 'date_finished' + fields_mini = ['id'] + fields_small = fields_mini + [ + 'command', 'result', 'log_url', + 'is_finished', + 'date_created', 'date_finished' ] + fields_fk = ['run_as'] + fields_m2m = ['hosts'] + fields = fields_small + fields_fk + fields_m2m read_only_fields = [ 'result', 'is_finished', 'log_url', 'date_created', 'date_finished' diff --git a/apps/orgs/serializers.py b/apps/orgs/serializers.py index 482c622e1..1a6aca0b0 100644 --- a/apps/orgs/serializers.py +++ b/apps/orgs/serializers.py @@ -38,8 +38,10 @@ class OrgSerializer(ModelSerializer): list_serializer_class = AdaptedBulkListSerializer fields_mini = ['id', 'name'] fields_small = fields_mini + [ - 'is_default', 'is_root', 'comment', - 'created_by', 'date_created', 'resource_statistics' + 'resource_statistics', + 'is_default', 'is_root', + 'date_created', + 'comment', 'created_by', ] fields_m2m = ['users', 'admins', 'auditors'] @@ -79,7 +81,12 @@ class OrgMemberSerializer(BulkModelSerializer): class Meta: model = OrganizationMember - fields = ('id', 'org', 'user', 'role', 'org_display', 'user_display', 'role_display') + fields_mini = ['id'] + fields_small = fields_mini + [ + 'role', 'role_display' + ] + fields_fk = ['org', 'user', 'org_display', 'user_display',] + fields = fields_small + fields_fk use_model_bulk_create = True model_bulk_create_kwargs = {'ignore_conflicts': True} diff --git a/apps/perms/serializers/application/permission.py b/apps/perms/serializers/application/permission.py index 7be6ec9ca..ce820b858 100644 --- a/apps/perms/serializers/application/permission.py +++ b/apps/perms/serializers/application/permission.py @@ -20,16 +20,16 @@ class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer): class Meta: model = ApplicationPermission - mini_fields = ['id', 'name'] - small_fields = mini_fields + [ + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ 'category', 'category_display', 'type', 'type_display', 'is_active', 'is_expired', 'is_valid', 'created_by', 'date_created', 'date_expired', 'date_start', 'comment' ] - m2m_fields = [ + fields_m2m = [ 'users', 'user_groups', 'applications', 'system_users', 'users_amount', 'user_groups_amount', 'applications_amount', 'system_users_amount', ] - fields = small_fields + m2m_fields + fields = fields_small + fields_m2m read_only_fields = ['created_by', 'date_created'] @classmethod diff --git a/apps/perms/serializers/asset/permission.py b/apps/perms/serializers/asset/permission.py index 67a42af4c..c553663e4 100644 --- a/apps/perms/serializers/asset/permission.py +++ b/apps/perms/serializers/asset/permission.py @@ -3,8 +3,10 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ + from django.db.models import Prefetch, Q + from orgs.mixins.serializers import BulkOrgResourceModelSerializer from perms.models import AssetPermission, Action from assets.models import Asset, Node, SystemUser @@ -95,18 +97,18 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): users_to_set = User.objects.filter( Q(name__in=kwargs.get('users_display')) | Q(username__in=kwargs.get('users_display')) ).distinct() - instance.users.set(users_to_set) + instance.users.add(users_to_set) # 用户组 user_groups_to_set = UserGroup.objects.filter(name__in=kwargs.get('user_groups_display')).distinct() - instance.user_groups.set(user_groups_to_set) + instance.user_groups.add(user_groups_to_set) # 资产 assets_to_set = Asset.objects.filter( Q(ip__in=kwargs.get('assets_display')) | Q(hostname__in=kwargs.get('assets_display')) ).distinct() - instance.assets.set(assets_to_set) + instance.assets.add(assets_to_set) # 节点 nodes_to_set = Node.objects.filter(full_value__in=kwargs.get('nodes_display')).distinct() - instance.nodes.set(nodes_to_set) + instance.nodes.add(nodes_to_set) def create(self, validated_data): display = { @@ -118,3 +120,4 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): instance = super().create(validated_data) self.perform_display_create(instance, **display) return instance + diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py index d66c3cfa3..854bfd240 100644 --- a/apps/terminal/serializers/session.py +++ b/apps/terminal/serializers/session.py @@ -17,14 +17,17 @@ class SessionSerializer(BulkOrgResourceModelSerializer): class Meta: model = Session list_serializer_class = AdaptedBulkListSerializer - fields = [ - "id", "user", "asset", "system_user", + fields_mini = ["id"] + fields_small = fields_mini + [ + "user", "asset", "system_user", "user_id", "asset_id", "system_user_id", - "login_from", "login_from_display", "remote_addr", - "is_success", "is_finished", "has_replay", "can_replay", - "can_join", "can_terminate", "protocol", "date_start", "date_end", - "terminal", + "login_from", "login_from_display", "remote_addr", "protocol", + "is_success", "is_finished", "has_replay", + "date_start", "date_end", ] + fields_fk = ["terminal",] + fields_custom = ["can_replay", "can_join", "can_terminate",] + fields = fields_small + fields_fk + fields_custom extra_kwargs = { "protocol": {'label': _('Protocol')}, 'user_id': {'label': _('User ID')}, diff --git a/apps/terminal/serializers/terminal.py b/apps/terminal/serializers/terminal.py index 25b72ab2c..e4640e454 100644 --- a/apps/terminal/serializers/terminal.py +++ b/apps/terminal/serializers/terminal.py @@ -17,12 +17,15 @@ class StatusSerializer(serializers.ModelSerializer): ) class Meta: - fields = [ - 'id', + fields_mini = ['id'] + fields_write_only = ['sessions',] + fields_small = fields_mini + fields_write_only + [ 'cpu_load', 'memory_used', 'disk_used', - 'session_online', 'sessions', - 'terminal', 'date_created', + 'session_online', + 'date_created' ] + fields_fk = ['terminal'] + fields = fields_small + fields_fk extra_kwargs = { "cpu_load": {'default': 0}, "memory_used": {'default': 0}, @@ -40,12 +43,16 @@ class TerminalSerializer(BulkModelSerializer): class Meta: model = Terminal - fields = [ - 'id', 'name', 'type', 'remote_addr', 'http_port', 'ssh_port', - 'comment', 'is_accepted', "is_active", 'session_online', - 'is_alive', 'date_created', 'command_storage', 'replay_storage', - 'status', 'status_display', 'stat' + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ + 'type', 'remote_addr', 'http_port', 'ssh_port', + 'session_online', 'command_storage', 'replay_storage', + 'is_accepted', "is_active", 'is_alive', + 'date_created', + 'comment', ] + fields_fk = ['status', 'status_display', 'stat'] + fields = fields_small + fields_fk read_only_fields = ['type', 'date_created'] @staticmethod diff --git a/apps/tickets/serializers/comment.py b/apps/tickets/serializers/comment.py index 4eaf49cf7..5bf57180e 100644 --- a/apps/tickets/serializers/comment.py +++ b/apps/tickets/serializers/comment.py @@ -21,9 +21,13 @@ class CommentSerializer(serializers.ModelSerializer): class Meta: model = Comment - fields = [ - 'id', 'ticket', 'body', 'user', 'user_display', 'date_created', 'date_updated' + fields_mini = ['id'] + fields_small = fields_mini + [ + 'body', 'user_display', + 'date_created', 'date_updated' ] + fields_fk = ['ticket', 'user',] + fields = fields_small + fields_fk read_only_fields = [ 'user_display', 'date_created', 'date_updated' ] diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py index fd776caea..fdf281b73 100644 --- a/apps/tickets/serializers/ticket/ticket.py +++ b/apps/tickets/serializers/ticket/ticket.py @@ -27,15 +27,17 @@ class TicketSerializer(OrgResourceModelSerializerMixin): class Meta: model = Ticket - fields = [ - 'id', 'title', 'type', 'type_display', - 'meta', 'action', 'action_display', 'status', 'status_display', - 'applicant', 'applicant_display', 'processor', 'processor_display', - 'assignees', 'assignees_display', 'comment', + fields_mini = ['id', 'title'] + fields_small = fields_mini + [ + 'type', 'type_display', 'meta', 'body', + 'action', 'action_display', 'status', 'status_display', + 'applicant_display', 'processor_display', 'assignees_display', 'date_created', 'date_updated', - 'org_id', 'org_name', - 'body' + 'comment', 'org_id', 'org_name', ] + fields_fk = ['applicant', 'processor',] + fields_m2m = ['assignees'] + fields = fields_small + fields_fk + fields_m2m def get_meta_serializer(self): default_serializer = serializers.Serializer(read_only=True) diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index d7b535880..eeadcaa2d 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -44,24 +44,35 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): model = User # mini 是指能识别对象的最小单元 fields_mini = ['id', 'name', 'username'] + # 只能写的字段, 这个虽然无法在框架上生效,但是更多对我们是提醒 + fields_write_only = [ + 'password', 'public_key', + ] # small 指的是 不需要计算的直接能从一张表中获取到的数据 - fields_small = fields_mini + [ - 'password', 'email', 'public_key', 'wechat', 'phone', 'mfa_level', 'mfa_enabled', - 'mfa_level_display', 'mfa_force_enabled', 'role_display', 'org_role_display', - 'total_role_display', 'comment', 'source', 'is_valid', 'is_expired', - 'is_active', 'created_by', 'is_first_login', 'can_public_key_auth', - 'password_strategy', 'date_password_last_updated', 'date_expired', - 'avatar_url', 'source_display', 'date_joined', 'last_login', 'need_update_password' + fields_small = fields_mini + fields_write_only + [ + 'email', 'wechat', 'phone', 'mfa_level', + 'source', 'source_display', + 'mfa_enabled', 'is_valid', 'is_expired', 'is_active', # 布尔字段 + 'date_expired', 'date_joined', 'last_login', # 日期字段 + 'created_by', 'comment', # 通用字段 ] - fields = fields_small + [ - 'groups', 'role', 'groups_display', 'role_display', - 'can_update', 'can_delete', 'login_blocked', 'org_roles' + # 包含不太常用的字段,可以没有 + fields_verbose = fields_small + [ + 'total_role_display', 'org_role_display', + 'mfa_level_display', 'mfa_force_enabled', 'is_first_login', + 'date_password_last_updated', 'avatar_url', ] + # 外键的字段 + fields_fk = ['role', 'role_display'] + # 多对多字段 + fields_m2m = ['groups', 'groups_display', 'org_roles'] + # 在serializer 上定义的字段 + fields_custom = ['can_update', 'can_delete', 'login_blocked', 'password_strategy'] + fields = fields_verbose + fields_fk + fields_m2m + fields_custom read_only_fields = [ 'date_joined', 'last_login', 'created_by', 'is_first_login', ] - extra_kwargs = { 'password': {'write_only': True, 'required': False, 'allow_null': True, 'allow_blank': True}, 'public_key': {'write_only': True}, From 72f9d0d3719fa10c9adcb610a1c0dac8d7025212 Mon Sep 17 00:00:00 2001 From: "fghbng@qq.com" Date: Fri, 30 Apr 2021 11:32:33 +0800 Subject: [PATCH 30/39] =?UTF-8?q?serializer=E4=BC=98=E5=8C=96&&=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E6=8E=88=E6=9D=83=E5=AF=BC=E5=85=A5=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/perms/serializers/asset/permission.py | 9 ++++----- apps/users/serializers/user.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/perms/serializers/asset/permission.py b/apps/perms/serializers/asset/permission.py index c553663e4..dcaffb82f 100644 --- a/apps/perms/serializers/asset/permission.py +++ b/apps/perms/serializers/asset/permission.py @@ -91,24 +91,23 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): data['system_users'].append(system_user.id) return super().to_internal_value(data) - def perform_display_create(self, instance, **kwargs): # 用户 users_to_set = User.objects.filter( Q(name__in=kwargs.get('users_display')) | Q(username__in=kwargs.get('users_display')) ).distinct() - instance.users.add(users_to_set) + instance.users.add(*users_to_set) # 用户组 user_groups_to_set = UserGroup.objects.filter(name__in=kwargs.get('user_groups_display')).distinct() - instance.user_groups.add(user_groups_to_set) + instance.user_groups.add(*user_groups_to_set) # 资产 assets_to_set = Asset.objects.filter( Q(ip__in=kwargs.get('assets_display')) | Q(hostname__in=kwargs.get('assets_display')) ).distinct() - instance.assets.add(assets_to_set) + instance.assets.add(*assets_to_set) # 节点 nodes_to_set = Node.objects.filter(full_value__in=kwargs.get('nodes_display')).distinct() - instance.nodes.add(nodes_to_set) + instance.nodes.add(*nodes_to_set) def create(self, validated_data): display = { diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index eeadcaa2d..5b2c19d10 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -51,7 +51,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): # small 指的是 不需要计算的直接能从一张表中获取到的数据 fields_small = fields_mini + fields_write_only + [ 'email', 'wechat', 'phone', 'mfa_level', - 'source', 'source_display', + 'source', 'source_display', 'can_public_key_auth', 'need_update_password', 'mfa_enabled', 'is_valid', 'is_expired', 'is_active', # 布尔字段 'date_expired', 'date_joined', 'last_login', # 日期字段 'created_by', 'comment', # 通用字段 From 8ca2522c71c483f4f9f3d8143d9286ac20b8cf85 Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 29 Apr 2021 14:18:52 +0800 Subject: [PATCH 31/39] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9tokent=E4=B8=AD?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E4=B8=AD=E6=B2=A1=E6=9C=89=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=20Protocols=20=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: 优化protocols fix: session bpp token 时间加长 --- apps/authentication/api/connection_token.py | 4 ++-- apps/authentication/serializers.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index e15f20661..ceebf3bde 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -79,7 +79,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView }) key = self.CACHE_KEY_PREFIX.format(token) - cache.set(key, value, timeout=20) + cache.set(key, value, timeout=30*60) return token def create(self, request, *args, **kwargs): @@ -102,7 +102,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView 'desktopwidth:i': '1280', 'desktopheight:i': '800', 'use multimon:i': '1', - 'session bpp:i': '24', + 'session bpp:i': '32', 'audiomode:i': '0', 'disable wallpaper:i': '0', 'disable full window drag:i': '0', diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py index c86b48039..5f2bc231b 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -8,6 +8,7 @@ from users.models import User from assets.models import Asset, SystemUser, Gateway from applications.models import Application from users.serializers import UserProfileSerializer +from assets.serializers import ProtocolsField from perms.serializers.asset.permission import ActionsField from .models import AccessKey, LoginConfirmSetting, SSOToken @@ -150,9 +151,11 @@ class ConnectionTokenUserSerializer(serializers.ModelSerializer): class ConnectionTokenAssetSerializer(serializers.ModelSerializer): + protocols = ProtocolsField(label='Protocols', read_only=True) + class Meta: model = Asset - fields = ['id', 'hostname', 'ip', 'port', 'org_id'] + fields = ['id', 'hostname', 'ip', 'protocols', 'org_id'] class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer): From 7294f6e5e06ad372eb2d8e79daa8df085cfc724d Mon Sep 17 00:00:00 2001 From: xinwen Date: Fri, 7 May 2021 16:29:47 +0800 Subject: [PATCH 32/39] refactor: command es storage `IGNORE_VERIFY_CERTS` --- apps/terminal/backends/command/es.py | 2 +- apps/terminal/serializers/storage.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/terminal/backends/command/es.py b/apps/terminal/backends/command/es.py index e03847e0a..27631d5bc 100644 --- a/apps/terminal/backends/command/es.py +++ b/apps/terminal/backends/command/es.py @@ -34,7 +34,7 @@ class CommandStore(): self.index = config.get("INDEX") or 'jumpserver' self.doc_type = config.get("DOC_TYPE") or 'command_store' - ignore_verify_certs = kwargs.pop('ignore_verify_certs', False) + ignore_verify_certs = kwargs.pop('IGNORE_VERIFY_CERTS', False) if ignore_verify_certs: kwargs['verify_certs'] = None self.es = Elasticsearch(hosts=hosts, max_retries=0, **kwargs) diff --git a/apps/terminal/serializers/storage.py b/apps/terminal/serializers/storage.py index 7f5dec2fe..ff794eef9 100644 --- a/apps/terminal/serializers/storage.py +++ b/apps/terminal/serializers/storage.py @@ -181,9 +181,9 @@ class CommandStorageTypeESSerializer(serializers.Serializer): max_length=1024, default='jumpserver', label=_('Index'), allow_null=True ) DOC_TYPE = ReadableHiddenField(default='command', label=_('Doc type'), allow_null=True) - ignore_verify_certs = serializers.BooleanField( + IGNORE_VERIFY_CERTS = serializers.BooleanField( default=False, label=_('Ignore Certificate Verification'), - source='OTHER.ignore_verify_certs', allow_null=True, + source='OTHER.IGNORE_VERIFY_CERTS', allow_null=True, ) # mapping From 4d6d4cbc2289adc300bebf42489621a5d9204f1d Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 29 Apr 2021 17:15:46 +0800 Subject: [PATCH 33/39] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E7=99=BB?= =?UTF-8?q?=E5=BD=95=EF=BC=8Ccas,=20openid=20=E8=87=AA=E5=8A=A8=E7=99=BB?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/mixins.py | 32 ++++---- apps/authentication/urls/view_urls.py | 6 -- apps/authentication/views/login.py | 81 +++++++------------ apps/common/mixins/views.py | 4 +- apps/common/urls/view_urls.py | 10 +++ apps/common/utils/__init__.py | 1 + apps/common/utils/jumpserver.py | 31 +++++++ apps/common/views.py | 40 +++++++++ apps/jumpserver/urls.py | 1 + apps/jumpserver/views/other.py | 5 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 72318 -> 72376 bytes apps/locale/zh/LC_MESSAGES/django.po | 75 ++++++++++------- apps/templates/flash_message_standalone.html | 19 +++-- apps/users/views/profile/reset.py | 65 +++++++-------- 14 files changed, 217 insertions(+), 153 deletions(-) create mode 100644 apps/common/urls/view_urls.py create mode 100644 apps/common/utils/jumpserver.py create mode 100644 apps/common/views.py diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 186f46911..9b81d17fe 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -7,13 +7,14 @@ import time from django.conf import settings from django.contrib import auth +from django.utils.translation import ugettext as _ from django.contrib.auth import ( BACKEND_SESSION_KEY, _get_backends, PermissionDenied, user_login_failed, _clean_credentials ) from django.shortcuts import reverse -from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get +from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil from users.models import User from users.utils import LoginBlockUtil, MFABlockUtils from . import errors @@ -204,41 +205,40 @@ class AuthMixin: return user @classmethod - def generate_reset_password_url_with_flash_msg(cls, user: User, flash_view_name): + def generate_reset_password_url_with_flash_msg(cls, user, message): reset_passwd_url = reverse('authentication:reset-password') query_str = urlencode({ 'token': user.generate_reset_token() }) reset_passwd_url = f'{reset_passwd_url}?{query_str}' - flash_page_url = reverse(flash_view_name) - query_str = urlencode({ - 'redirect_url': reset_passwd_url - }) - return f'{flash_page_url}?{query_str}' + message_data = { + 'title': _('Please change your password'), + 'message': message, + 'interval': 3, + 'redirect_url': reset_passwd_url, + } + return FlashMessageUtil.gen_message_url(message_data) @classmethod def _check_passwd_is_too_simple(cls, user: User, password): if user.is_superuser and password == 'admin': - url = cls.generate_reset_password_url_with_flash_msg( - user, 'authentication:passwd-too-simple-flash-msg' - ) + message = _('Your password is too simple, please change it for security') + url = cls.generate_reset_password_url_with_flash_msg(user, message=message) raise errors.PasswdTooSimple(url) @classmethod def _check_passwd_need_update(cls, user: User): if user.need_update_password: - url = cls.generate_reset_password_url_with_flash_msg( - user, 'authentication:passwd-need-update-flash-msg' - ) + message = _('You should to change your password before login') + url = cls.generate_reset_password_url_with_flash_msg(user, message) raise errors.PasswdNeedUpdate(url) @classmethod def _check_password_require_reset_or_not(cls, user: User): if user.password_has_expired: - url = cls.generate_reset_password_url_with_flash_msg( - user, 'authentication:passwd-has-expired-flash-msg' - ) + message = _('Your password has expired, please reset before logging in') + url = cls.generate_reset_password_url_with_flash_msg(user, message) raise errors.PasswordRequireResetError(url) def check_user_auth_if_need(self, decrypt_passwd=False): diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index 062044047..c4e4de00a 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -18,13 +18,7 @@ urlpatterns = [ # 原来在users中的 path('password/forgot/', users_view.UserForgotPasswordView.as_view(), name='forgot-password'), - path('password/forgot/sendmail-success/', users_view.UserForgotPasswordSendmailSuccessView.as_view(), - name='forgot-password-sendmail-success'), path('password/reset/', users_view.UserResetPasswordView.as_view(), name='reset-password'), - path('password/too-simple-flash-msg/', views.FlashPasswdTooSimpleMsgView.as_view(), name='passwd-too-simple-flash-msg'), - path('password/need-update-flash-msg/', views.FlashPasswdNeedUpdateMsgView.as_view(), name='passwd-need-update-flash-msg'), - path('password/has-expired-msg/', views.FlashPasswdHasExpiredMsgView.as_view(), name='passwd-has-expired-flash-msg'), - path('password/reset/success/', users_view.UserResetPasswordSuccessView.as_view(), name='reset-password-success'), path('password/verify/', users_view.UserVerifyPasswordView.as_view(), name='user-verify-password'), # Profile diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index b57d8b5aa..2412ef54a 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -19,7 +19,7 @@ from django.conf import settings from django.urls import reverse_lazy from django.contrib.auth import BACKEND_SESSION_KEY -from common.utils import get_request_ip, get_object_or_none +from common.utils import get_request_ip, FlashMessageUtil from users.utils import ( redirect_user_first_login_or_index ) @@ -31,8 +31,6 @@ from ..forms import get_user_login_form_cls __all__ = [ 'UserLoginView', 'UserLogoutView', 'UserLoginGuardView', 'UserLoginWaitConfirmView', - 'FlashPasswdTooSimpleMsgView', 'FlashPasswdHasExpiredMsgView', - 'FlashPasswdNeedUpdateMsgView' ] @@ -44,12 +42,42 @@ class UserLoginView(mixins.AuthMixin, FormView): redirect_field_name = 'next' template_name = 'authentication/login.html' + def redirect_third_party_auth_if_need(self, request): + # show jumpserver login page if request http://{JUMP-SERVER}/?admin=1 + if self.request.GET.get("admin", 0): + return None + auth_type = '' + auth_url = '' + if settings.AUTH_OPENID: + auth_type = 'OIDC' + auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME) + elif settings.AUTH_CAS: + auth_type = 'CAS' + auth_url = reverse(settings.CAS_LOGIN_URL_NAME) + if not auth_url: + return None + + message_data = { + 'title': _('Redirecting'), + 'message': _("Redirecting to {} authentication").format(auth_type), + 'redirect_url': auth_url, + 'has_cancel': True, + 'cancel_url': reverse('authentication:login') + '?admin=1' + } + redirect_url = FlashMessageUtil.gen_message_url(message_data) + query_string = request.GET.urlencode() + redirect_url = "{}&{}".format(redirect_url, query_string) + return redirect_url + def get(self, request, *args, **kwargs): if request.user.is_staff: first_login_url = redirect_user_first_login_or_index( request, self.redirect_field_name ) return redirect(first_login_url) + redirect_url = self.redirect_third_party_auth_if_need(request) + if redirect_url: + return redirect(redirect_url) request.session.set_test_cookie() return super().get(request, *args, **kwargs) @@ -225,50 +253,3 @@ class UserLogoutView(TemplateView): } kwargs.update(context) return super().get_context_data(**kwargs) - - -@method_decorator(never_cache, name='dispatch') -class FlashPasswdTooSimpleMsgView(TemplateView): - template_name = 'flash_message_standalone.html' - - def get(self, request, *args, **kwargs): - context = { - 'title': _('Please change your password'), - 'messages': _('Your password is too simple, please change it for security'), - 'interval': 3, - 'redirect_url': request.GET.get('redirect_url'), - 'auto_redirect': True, - } - return self.render_to_response(context) - - -@method_decorator(never_cache, name='dispatch') -class FlashPasswdNeedUpdateMsgView(TemplateView): - template_name = 'flash_message_standalone.html' - - def get(self, request, *args, **kwargs): - context = { - 'title': _('Please change your password'), - 'messages': _('You should to change your password before login'), - 'interval': 3, - 'redirect_url': request.GET.get('redirect_url'), - 'auto_redirect': True, - 'confirm_button': _('Confirm') - } - return self.render_to_response(context) - - -@method_decorator(never_cache, name='dispatch') -class FlashPasswdHasExpiredMsgView(TemplateView): - template_name = 'flash_message_standalone.html' - - def get(self, request, *args, **kwargs): - context = { - 'title': _('Please change your password'), - 'messages': _('Your password has expired, please reset before logging in'), - 'interval': 3, - 'redirect_url': request.GET.get('redirect_url'), - 'auto_redirect': True, - 'confirm_button': _('Confirm') - } - return self.render_to_response(context) diff --git a/apps/common/mixins/views.py b/apps/common/mixins/views.py index 13f365662..1590f48ba 100644 --- a/apps/common/mixins/views.py +++ b/apps/common/mixins/views.py @@ -5,7 +5,7 @@ from django.contrib.auth.mixins import UserPassesTestMixin from django.utils import timezone -__all__ = ["DatetimeSearchMixin"] +__all__ = ["DatetimeSearchMixin", "PermissionsMixin"] from rest_framework import permissions @@ -51,4 +51,4 @@ class PermissionsMixin(UserPassesTestMixin): for permission_class in permission_classes: if not permission_class().has_permission(self.request, self): return False - return True \ No newline at end of file + return True diff --git a/apps/common/urls/view_urls.py b/apps/common/urls/view_urls.py new file mode 100644 index 000000000..bfbd97053 --- /dev/null +++ b/apps/common/urls/view_urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from .. import views + +app_name = 'common' + +urlpatterns = [ + # login + path('flash-message/', views.FlashMessageMsgView.as_view(), name='flash-message'), +] \ No newline at end of file diff --git a/apps/common/utils/__init__.py b/apps/common/utils/__init__.py index 8b4576221..0fdcf0dd1 100644 --- a/apps/common/utils/__init__.py +++ b/apps/common/utils/__init__.py @@ -8,3 +8,4 @@ from .http import * from .ipip import * from .crypto import * from .random import * +from .jumpserver import * \ No newline at end of file diff --git a/apps/common/utils/jumpserver.py b/apps/common/utils/jumpserver.py new file mode 100644 index 000000000..1ae806366 --- /dev/null +++ b/apps/common/utils/jumpserver.py @@ -0,0 +1,31 @@ +from django.core.cache import cache +from django.shortcuts import reverse + +from .random import random_string + + +__all__ = ['FlashMessageUtil'] + + +class FlashMessageUtil: + @staticmethod + def get_key(code): + key = 'MESSAGE_{}'.format(code) + return key + + @classmethod + def get_message_code(cls, message_data): + code = random_string(12) + key = cls.get_key(code) + cache.set(key, message_data, 60) + return code + + @classmethod + def get_message_by_code(cls, code): + key = cls.get_key(code) + return cache.get(key) + + @classmethod + def gen_message_url(cls, message_data): + code = cls.get_message_code(message_data) + return reverse('common:flash-message') + f'?code={code}' diff --git a/apps/common/views.py b/apps/common/views.py new file mode 100644 index 000000000..79d628248 --- /dev/null +++ b/apps/common/views.py @@ -0,0 +1,40 @@ +# +from django.utils.decorators import method_decorator +from django.views.decorators.cache import never_cache +from django.http import HttpResponse +from django.views.generic.base import TemplateView + +from common.utils import bulk_get, FlashMessageUtil + + +@method_decorator(never_cache, name='dispatch') +class FlashMessageMsgView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + code = request.GET.get('code') + if not code: + return HttpResponse('Not found the code') + + message_data = FlashMessageUtil.get_message_by_code(code) + if not message_data: + return HttpResponse('Message code error') + + title, message, redirect_url, confirm_button, cancel_url = bulk_get( + message_data, 'title', 'message', 'redirect_url', 'confirm_button', 'cancel_url' + ) + + interval = message_data.get('interval', 3) + auto_redirect = message_data.get('auto_redirect', True) + has_cancel = message_data.get('has_cancel', False) + context = { + 'title': title, + 'messages': message, + 'interval': interval, + 'redirect_url': redirect_url, + 'auto_redirect': auto_redirect, + 'confirm_button': confirm_button, + 'has_cancel': has_cancel, + 'cancel_url': cancel_url, + } + return self.render_to_response(context) \ No newline at end of file diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 759c6f271..ca9870043 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -29,6 +29,7 @@ api_v1 = [ app_view_patterns = [ path('auth/', include('authentication.urls.view_urls'), name='auth'), path('ops/', include('ops.urls.view_urls'), name='ops'), + path('common/', include('common.urls.view_urls'), name='common'), re_path(r'flower/(?P.*)', views.celery_flower_view, name='flower-view'), ] diff --git a/apps/jumpserver/views/other.py b/apps/jumpserver/views/other.py index 9ab561c4c..293177615 100644 --- a/apps/jumpserver/views/other.py +++ b/apps/jumpserver/views/other.py @@ -1,17 +1,15 @@ # -*- coding: utf-8 -*- # import re -import time from django.http import HttpResponseRedirect, JsonResponse, Http404 from django.conf import settings from django.views.generic import View from django.shortcuts import redirect from django.utils.translation import ugettext_lazy as _ -from rest_framework.views import APIView -from rest_framework.permissions import AllowAny from django.views.decorators.csrf import csrf_exempt from django.http import HttpResponse +from rest_framework.views import APIView from common.http import HttpResponseTemporaryRedirect @@ -85,3 +83,4 @@ class KokoView(View): "
    Koko is a separately deployed program, you need to deploy Koko, configure nginx for url distribution,
    " "If you see this page, prove that you are not accessing the nginx listening port. Good luck.") return HttpResponse(msg) + diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index c4b35117ec8a1141bf17650a9a19b576922d926f..2bc2e78e23f4cc398283cac0b6957f9ae0142639 100644 GIT binary patch delta 21120 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 cueiG~-i_~$+oaL%Z&-PMEQTr_kcXp?Hf+ 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 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index cbe271ba3..ee97bf8fd 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-04-29 12:17+0800\n" +"POT-Creation-Date: 2021-04-29 16:20+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -354,7 +354,9 @@ msgid "You can't delete the root node ({})" msgstr "不能删除根节点 ({})" #: assets/api/node.py:75 -msgid "Deletion failed and the node contains children or assets" +#, fuzzy +#| msgid "Deletion failed and the node contains children or assets" +msgid "Deletion failed and the node contains assets" msgstr "删除失败,节点包含子节点或资产" #: assets/backends/db.py:244 @@ -650,31 +652,31 @@ msgstr "资产组" msgid "Default asset group" msgstr "默认资产组" -#: assets/models/label.py:19 assets/models/node.py:547 settings/models.py:30 +#: assets/models/label.py:19 assets/models/node.py:546 settings/models.py:30 msgid "Value" msgstr "值" -#: assets/models/node.py:152 +#: assets/models/node.py:151 msgid "New node" msgstr "新节点" -#: assets/models/node.py:475 users/templates/users/_granted_assets.html:130 +#: assets/models/node.py:474 users/templates/users/_granted_assets.html:130 msgid "empty" msgstr "空" -#: assets/models/node.py:546 perms/models/asset_permission.py:176 +#: assets/models/node.py:545 perms/models/asset_permission.py:176 msgid "Key" msgstr "键" -#: assets/models/node.py:548 +#: assets/models/node.py:547 msgid "Full value" msgstr "全称" -#: assets/models/node.py:551 perms/models/asset_permission.py:177 +#: assets/models/node.py:550 perms/models/asset_permission.py:177 msgid "Parent key" msgstr "ssh私钥" -#: assets/models/node.py:560 assets/serializers/system_user.py:191 +#: assets/models/node.py:559 assets/serializers/system_user.py:191 #: users/templates/users/user_asset_permission.html:41 #: users/templates/users/user_asset_permission.html:73 #: users/templates/users/user_asset_permission.html:158 @@ -1360,15 +1362,15 @@ msgstr "来源 IP 不被允许登录" msgid "SSO auth closed" msgstr "SSO 认证关闭了" -#: authentication/errors.py:271 authentication/views/login.py:237 +#: authentication/errors.py:271 authentication/views/login.py:267 msgid "Your password is too simple, please change it for security" msgstr "你的密码过于简单,为了安全,请修改" -#: authentication/errors.py:280 authentication/views/login.py:252 +#: authentication/errors.py:280 authentication/views/login.py:282 msgid "You should to change your password before login" msgstr "登录完成前,请先修改密码" -#: authentication/errors.py:289 authentication/views/login.py:268 +#: authentication/errors.py:289 authentication/views/login.py:298 msgid "Your password has expired, please reset before logging in" msgstr "您的密码已过期,先修改再登录" @@ -1453,8 +1455,9 @@ msgid "Need MFA for view auth" msgstr "需要多因子认证来查看账号信息" #: authentication/templates/authentication/_mfa_confirm_modal.html:20 -#: authentication/views/login.py:256 authentication/views/login.py:272 -#: templates/_modal.html:23 users/templates/users/user_password_verify.html:20 +#: authentication/views/login.py:286 authentication/views/login.py:302 +#: authentication/views/login.py:318 templates/_modal.html:23 +#: users/templates/users/user_password_verify.html:20 msgid "Confirm" msgstr "确认" @@ -1519,7 +1522,6 @@ msgid "Copy link" msgstr "复制链接" #: authentication/templates/authentication/login_wait_confirm.html:51 -#: templates/flash_message_standalone.html:38 msgid "Return" msgstr "返回" @@ -1527,11 +1529,19 @@ msgstr "返回" msgid "Copy success" msgstr "复制成功" -#: authentication/views/login.py:58 +#: authentication/views/login.py:63 authentication/views/login.py:313 +msgid "Redirecting" +msgstr "跳转中" + +#: authentication/views/login.py:64 +msgid "Redirecting to {} authentication" +msgstr "正在跳转到 {} 认证" + +#: authentication/views/login.py:88 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:183 +#: authentication/views/login.py:213 msgid "" "Wait for {} confirm, You also can copy link to her/him
    \n" " Don't close this page" @@ -1539,23 +1549,27 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
    \n" " 不要关闭本页面" -#: authentication/views/login.py:188 +#: authentication/views/login.py:218 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:220 +#: authentication/views/login.py:250 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:221 +#: authentication/views/login.py:251 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/login.py:236 authentication/views/login.py:251 -#: authentication/views/login.py:267 +#: authentication/views/login.py:266 authentication/views/login.py:281 +#: authentication/views/login.py:297 msgid "Please change your password" msgstr "请修改密码" +#: authentication/views/login.py:314 +msgid "Redirect to third party auth" +msgstr "" + #: common/const/__init__.py:6 #, python-format msgid "%(name)s was created successfully" @@ -1671,7 +1685,7 @@ msgstr "JumpServer 开源堡垒机" msgid "

    Flow service unavailable, check it

    " msgstr "" -#: jumpserver/views/other.py:27 +#: jumpserver/views/other.py:25 msgid "" "
    Luna is a separately deployed program, you need to deploy Luna, koko, " "configure nginx for url distribution,
    If you see this page, " @@ -1680,11 +1694,11 @@ msgstr "" "
    Luna是单独部署的一个程序,你需要部署luna,koko,
    如果你看到了" "这个页面,证明你访问的不是nginx监听的端口,祝你好运
    " -#: jumpserver/views/other.py:71 +#: jumpserver/views/other.py:69 msgid "Websocket server run on port: {}, you should proxy it on nginx" msgstr "Websocket 服务运行在端口: {}, 请检查nginx是否代理是否设置" -#: jumpserver/views/other.py:85 +#: jumpserver/views/other.py:83 msgid "" "
    Koko is a separately deployed program, you need to deploy Koko, " "configure nginx for url distribution,
    If you see this page, " @@ -2760,6 +2774,14 @@ msgstr "确认删除" msgid "Are you sure delete" msgstr "您确定删除吗?" +#: templates/flash_message_standalone.html:28 +msgid "Cancel" +msgstr "取消" + +#: templates/flash_message_standalone.html:37 +msgid "Go" +msgstr "跳转" + #: templates/index.html:11 msgid "Total users" msgstr "用户总数" @@ -4978,9 +5000,6 @@ msgstr "社区版" #~ msgid "This will reset the user password and send a reset mail" #~ msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" -#~ msgid "Cancel" -#~ msgstr "取消" - #~ msgid "" #~ "The reset-ssh-public-key E-mail has been sent successfully. Please inform " #~ "the user to update his new ssh public key." diff --git a/apps/templates/flash_message_standalone.html b/apps/templates/flash_message_standalone.html index bc27fa614..5265f14f9 100644 --- a/apps/templates/flash_message_standalone.html +++ b/apps/templates/flash_message_standalone.html @@ -4,14 +4,6 @@ {% block html_title %} {{ title }} {% endblock %} {% block title %} {{ title }}{% endblock %} -{% block custom_head_css_js %} - -{% endblock %} - {% block content %}
    {% if errors %} @@ -30,12 +22,19 @@

    {% endif %}
    -
    + {% if has_cancel %} + + {% endif %} + diff --git a/apps/users/views/profile/reset.py b/apps/users/views/profile/reset.py index 8b1d9a102..356694020 100644 --- a/apps/users/views/profile/reset.py +++ b/apps/users/views/profile/reset.py @@ -1,18 +1,15 @@ # ~*~ coding: utf-8 ~*~ from __future__ import unicode_literals -from django.shortcuts import render from django.views.generic import RedirectView -from django.core.files.storage import default_storage from django.shortcuts import reverse, redirect from django.utils.translation import ugettext as _ from django.views.generic.base import TemplateView from django.conf import settings from django.urls import reverse_lazy -from formtools.wizard.views import SessionWizardView from django.views.generic import FormView -from common.utils import get_object_or_none +from common.utils import get_object_or_none, FlashMessageUtil from common.permissions import IsValidUser from common.mixins.views import PermissionsMixin from ...models import User @@ -24,9 +21,7 @@ from ... import forms __all__ = [ - 'UserLoginView', 'UserForgotPasswordSendmailSuccessView', - 'UserResetPasswordSuccessView', 'UserResetPasswordSuccessView', - 'UserResetPasswordView', 'UserForgotPasswordView', 'UserFirstLoginView', + 'UserLoginView', 'UserResetPasswordView', 'UserForgotPasswordView', 'UserFirstLoginView', ] @@ -39,6 +34,17 @@ class UserForgotPasswordView(FormView): template_name = 'users/forgot_password.html' form_class = forms.UserForgotPasswordForm + @staticmethod + def get_redirect_message_url(): + message_data = { + 'title': _('Send reset password message'), + 'message': _('Send reset password mail success, ' + 'login your mail box and follow it '), + 'redirect_url': reverse('authentication:login'), + } + url = FlashMessageUtil.gen_message_url(message_data) + return url + def form_valid(self, form): email = form.cleaned_data['email'] user = get_object_or_none(User, email=email) @@ -53,37 +59,9 @@ class UserForgotPasswordView(FormView): ).format(user.get_source_display()) form.add_error('email', error) return self.form_invalid(form) - send_reset_password_mail(user) - return redirect('authentication:forgot-password-sendmail-success') - - -class UserForgotPasswordSendmailSuccessView(TemplateView): - template_name = 'flash_message_standalone.html' - - def get_context_data(self, **kwargs): - context = { - 'title': _('Send reset password message'), - 'messages': _('Send reset password mail success, ' - 'login your mail box and follow it '), - 'redirect_url': reverse('authentication:login'), - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class UserResetPasswordSuccessView(TemplateView): - template_name = 'flash_message_standalone.html' - - def get_context_data(self, **kwargs): - context = { - 'title': _('Reset password success'), - 'messages': _('Reset password success, return to login page'), - 'redirect_url': reverse('authentication:login'), - 'auto_redirect': True, - } - kwargs.update(context) - return super().get_context_data(**kwargs) + url = self.get_redirect_message_url() + return redirect(url) class UserResetPasswordView(FormView): @@ -139,7 +117,18 @@ class UserResetPasswordView(FormView): user.reset_password(password) User.expired_reset_password_token(token) send_reset_password_success_mail(self.request, user) - return redirect('authentication:reset-password-success') + url = self.get_redirect_url() + return redirect(url) + + @staticmethod + def get_redirect_url(): + message_data = { + 'title': _('Reset password success'), + 'message': _('Reset password success, return to login page'), + 'redirect_url': reverse('authentication:login'), + 'auto_redirect': True, + } + return FlashMessageUtil.gen_message_url(message_data) class UserFirstLoginView(PermissionsMixin, TemplateView): From 54f5e65d368c98235db9506910006145f9953402 Mon Sep 17 00:00:00 2001 From: xinwen Date: Fri, 7 May 2021 19:40:04 +0800 Subject: [PATCH 34/39] =?UTF-8?q?feat:=20=E6=A3=80=E6=9F=A5=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E6=8E=88=E6=9D=83=E8=BF=87=E6=9C=9F=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=BF=87=E6=9C=9F=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/application/user_permission/common.py | 28 +++++----- .../perms/api/asset/user_permission/common.py | 33 +++++------- apps/perms/utils/application/permission.py | 54 +++++++++++++++++++ apps/perms/utils/asset/permission.py | 50 ++++++++++++++++- 4 files changed, 127 insertions(+), 38 deletions(-) diff --git a/apps/perms/api/application/user_permission/common.py b/apps/perms/api/application/user_permission/common.py index 4e7d0b5f2..2a64d122d 100644 --- a/apps/perms/api/application/user_permission/common.py +++ b/apps/perms/api/application/user_permission/common.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- # -import uuid +import time + from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator from rest_framework.views import APIView, Response +from rest_framework import status from rest_framework.generics import ( ListAPIView, get_object_or_404 ) @@ -12,7 +14,8 @@ from orgs.utils import tmp_to_root_org from applications.models import Application from perms.utils.application.permission import ( has_application_system_permission, - get_application_system_user_ids + get_application_system_user_ids, + validate_permission, ) from perms.api.asset.user_permission.mixin import RoleAdminMixin, RoleUserMixin from common.permissions import IsOrgAdminOrAppUser @@ -61,18 +64,13 @@ class ValidateUserApplicationPermissionApi(APIView): application_id = request.query_params.get('application_id', '') system_user_id = request.query_params.get('system_user_id', '') - try: - user_id = uuid.UUID(user_id) - application_id = uuid.UUID(application_id) - system_user_id = uuid.UUID(system_user_id) - except ValueError: - return Response({'msg': False}, status=403) + if not all((user_id, application_id, system_user_id)): + return Response({'has_permission': False, 'expire_at': int(time.time())}) - user = get_object_or_404(User, id=user_id) - application = get_object_or_404(Application, id=application_id) - system_user = get_object_or_404(SystemUser, id=system_user_id) + user = User.objects.get(id=user_id) + application = Application.objects.get(id=application_id) + system_user = SystemUser.objects.get(id=system_user_id) - if has_application_system_permission(user, application, system_user): - return Response({'msg': True}, status=200) - - return Response({'msg': False}, status=403) + has_permission, expire_at = validate_permission(user, application, system_user) + status_code = status.HTTP_200_OK if has_permission else status.HTTP_403_FORBIDDEN + return Response({'has_permission': has_permission, 'expire_at': int(expire_at)}, status=status_code) diff --git a/apps/perms/api/asset/user_permission/common.py b/apps/perms/api/asset/user_permission/common.py index 88b5422d9..1d09274c8 100644 --- a/apps/perms/api/asset/user_permission/common.py +++ b/apps/perms/api/asset/user_permission/common.py @@ -1,22 +1,23 @@ # -*- coding: utf-8 -*- # import uuid +import time from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator from rest_framework.views import APIView, Response +from rest_framework import status from rest_framework.generics import ( ListAPIView, get_object_or_404, RetrieveAPIView, DestroyAPIView ) from orgs.utils import tmp_to_root_org -from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user +from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user, validate_permission from common.permissions import IsOrgAdminOrAppUser, IsOrgAdmin, IsValidUser from common.utils import get_logger, lazyproperty from perms.hands import User, Asset, SystemUser from perms import serializers -from perms.models import Action logger = get_logger(__name__) @@ -65,32 +66,22 @@ class ValidateUserAssetPermissionApi(APIView): def get_cache_policy(self): return 0 - def get_user(self): - user_id = self.request.query_params.get('user_id', '') - user = get_object_or_404(User, id=user_id) - return user - def get(self, request, *args, **kwargs): + user_id = self.request.query_params.get('user_id', '') asset_id = request.query_params.get('asset_id', '') system_id = request.query_params.get('system_user_id', '') action_name = request.query_params.get('action_name', '') - try: - asset_id = uuid.UUID(asset_id) - system_id = uuid.UUID(system_id) - except ValueError: - return Response({'msg': False}, status=403) + if not all((user_id, asset_id, system_id, action_name)): + return Response({'has_permission': False, 'expire_at': int(time.time())}) - asset = get_object_or_404(Asset, id=asset_id, is_active=True) - system_user = get_object_or_404(SystemUser, id=system_id) + user = User.objects.get(id=user_id) + asset = Asset.objects.valid().get(id=asset_id) + system_user = SystemUser.objects.get(id=system_id) - system_users_actions = get_asset_system_user_ids_with_actions_by_user(self.get_user(), asset) - actions = system_users_actions.get(system_user.id) - if actions is None: - return Response({'msg': False}, status=403) - if action_name in Action.value_to_choices(actions): - return Response({'msg': True}, status=200) - return Response({'msg': False}, status=403) + has_permission, expire_at = validate_permission(user, asset, system_user, action_name) + status_code = status.HTTP_200_OK if has_permission else status.HTTP_403_FORBIDDEN + return Response({'has_permission': has_permission, 'expire_at': int(expire_at)}, status=status_code) # TODO 删除 diff --git a/apps/perms/utils/application/permission.py b/apps/perms/utils/application/permission.py index 4e4c8113d..c4ebb5bdb 100644 --- a/apps/perms/utils/application/permission.py +++ b/apps/perms/utils/application/permission.py @@ -1,3 +1,5 @@ +import time + from django.db.models import Q from common.utils import get_logger @@ -6,6 +8,58 @@ from perms.models import ApplicationPermission logger = get_logger(__file__) +def get_user_all_app_perm_ids(user) -> set: + app_perm_ids = set() + user_perm_id = ApplicationPermission.users.through.objects \ + .filter(user_id=user.id) \ + .values_list('applicationpermission_id', flat=True) \ + .distinct() + app_perm_ids.update(user_perm_id) + + group_ids = user.groups.through.objects \ + .filter(user_id=user.id) \ + .values_list('usergroup_id', flat=True) \ + .distinct() + group_ids = list(group_ids) + groups_perm_id = ApplicationPermission.user_groups.through.objects \ + .filter(usergroup_id__in=group_ids) \ + .values_list('applicationpermission_id', flat=True) \ + .distinct() + app_perm_ids.update(groups_perm_id) + + app_perm_ids = ApplicationPermission.objects.filter( + id__in=app_perm_ids).valid().values_list('id', flat=True) + app_perm_ids = set(app_perm_ids) + return app_perm_ids + + +def validate_permission(user, application, system_user): + app_perm_ids = get_user_all_app_perm_ids(user) + app_perm_ids = ApplicationPermission.applications.through.objects.filter( + applicationpermission_id__in=app_perm_ids, + application_id=application.id + ).values_list('applicationpermission_id', flat=True) + + app_perm_ids = set(app_perm_ids) + + app_perm_ids = ApplicationPermission.system_users.through.objects.filter( + applicationpermission_id__in=app_perm_ids, + systemuser_id=system_user.id + ).values_list('applicationpermission_id', flat=True) + + app_perm_ids = set(app_perm_ids) + + app_perm = ApplicationPermission.objects.filter( + id__in=app_perm_ids + ).order_by('-date_expired').first() + + app_perm: ApplicationPermission + if app_perm: + return True, app_perm.date_expired.timestamp() + else: + return False, time.time() + + def get_application_system_user_ids(user, application): queryset = ApplicationPermission.objects.valid()\ .filter( diff --git a/apps/perms/utils/asset/permission.py b/apps/perms/utils/asset/permission.py index 197abc9d9..949ee6873 100644 --- a/apps/perms/utils/asset/permission.py +++ b/apps/perms/utils/asset/permission.py @@ -1,15 +1,61 @@ +import time from collections import defaultdict from django.db.models import Q from common.utils import get_logger -from perms.models import AssetPermission -from perms.hands import Asset, User, UserGroup, SystemUser +from perms.models import AssetPermission, Action +from perms.hands import Asset, User, UserGroup, SystemUser, Node from perms.utils.asset.user_permission import get_user_all_asset_perm_ids logger = get_logger(__file__) +def validate_permission(user, asset, system_user, action_name): + + if not system_user.protocol in asset.protocols_as_dict.keys(): + return False, time.time() + + asset_perm_ids = get_user_all_asset_perm_ids(user) + + asset_perm_ids_from_asset = AssetPermission.assets.through.objects.filter( + assetpermission_id__in=asset_perm_ids, + asset_id=asset.id + ).values_list('assetpermission_id', flat=True) + + nodes = asset.get_nodes() + node_keys = set() + for node in nodes: + ancestor_keys = node.get_ancestor_keys(with_self=True) + node_keys.update(ancestor_keys) + node_ids = Node.objects.filter(key__in=node_keys).values_list('id', flat=True) + + node_ids = set(node_ids) + + asset_perm_ids_from_node = AssetPermission.nodes.through.objects.filter( + assetpermission_id__in=asset_perm_ids, + node_id__in=node_ids + ).values_list('assetpermission_id', flat=True) + + asset_perm_ids = {*asset_perm_ids_from_asset, *asset_perm_ids_from_node} + + asset_perm_ids = AssetPermission.system_users.through.objects.filter( + assetpermission_id__in=asset_perm_ids, + systemuser_id=system_user.id + ).values_list('assetpermission_id', flat=True) + + asset_perm_ids = set(asset_perm_ids) + + asset_perms = AssetPermission.objects.filter( + id__in=asset_perm_ids + ).order_by('-date_expired') + + for asset_perm in asset_perms: + if action_name in Action.value_to_choices(asset_perm.actions): + return True, asset_perm.date_expired.timestamp() + return False, time.time() + + def get_asset_system_user_ids_with_actions(asset_perm_ids, asset: Asset): nodes = asset.get_nodes() node_keys = set() From 340547c889156f229bee16a0419d8426d415756e Mon Sep 17 00:00:00 2001 From: jym503558564 <503558564@qq.com> Date: Wed, 12 May 2021 10:44:08 +0800 Subject: [PATCH 35/39] =?UTF-8?q?perf(README):=20=E7=99=BD=E7=9A=AE?= =?UTF-8?q?=E4=B9=A6=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fdcf28b11..8f5da3631 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,10 @@ - [ENGLISH](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md) -|![notification](https://raw.githubusercontent.com/goharbor/website/master/docs/img/readme/bell-outline-badged.svg)安全通知| + +|《新一代堡垒机建设指南》开放下载| |------------------| -|2021年1月15日 JumpServer 发现远程执行漏洞,请速度修复 [详见](https://github.com/jumpserver/jumpserver/issues/5533), 非常感谢 **reactivity of Alibaba Hackerone bug bounty program**(瑞典) 向我们报告了此 BUG| +|本白皮书由JumpServer开源项目组编著而成。编写团队从企业实践和技术演进的双重视角出发,结合自身在身份与访问安全领域长期研发及落地经验组织撰写,同时积极听取行业内专家的意见和建议,在此基础上完成了本白皮书的编写任务。下载链接:https://jinshuju.net/f/E0qAl8| -------------------------- From c16319ec48390bee6367ea7a7d33e4ed4612568a Mon Sep 17 00:00:00 2001 From: xinwen Date: Wed, 24 Mar 2021 19:01:35 +0800 Subject: [PATCH 36/39] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BC=81?= =?UTF-8?q?=E4=B8=9A=E5=BE=AE=E4=BF=A1=EF=BC=8C=E9=92=89=E9=92=89=E6=89=AB?= =?UTF-8?q?=E7=A0=81=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/audits/signals_handler.py | 2 + apps/authentication/api/__init__.py | 3 + apps/authentication/api/dingtalk.py | 35 + apps/authentication/api/password.py | 26 + apps/authentication/api/wecom.py | 35 + apps/authentication/backends/api.py | 18 + apps/authentication/errors.py | 27 + apps/authentication/mixins.py | 66 +- apps/authentication/serializers.py | 9 +- .../templates/authentication/login.html | 24 +- apps/authentication/urls/api_urls.py | 7 + apps/authentication/urls/view_urls.py | 16 + apps/authentication/views/__init__.py | 2 + apps/authentication/views/dingtalk.py | 243 ++++++ apps/authentication/views/login.py | 19 +- apps/authentication/views/wecom.py | 241 ++++++ apps/common/message/__init__.py | 0 apps/common/message/backends/__init__.py | 0 .../message/backends/dingtalk/__init__.py | 168 +++++ apps/common/message/backends/exceptions.py | 28 + apps/common/message/backends/mixin.py | 94 +++ apps/common/message/backends/utils.py | 78 ++ .../common/message/backends/wecom/__init__.py | 194 +++++ apps/common/mixins/api.py | 23 + apps/common/request_log.py | 15 + apps/common/utils/random.py | 1 - apps/jumpserver/conf.py | 10 + apps/jumpserver/context_processor.py | 2 + apps/jumpserver/settings/auth.py | 19 + apps/jumpserver/urls.py | 2 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 72376 -> 74427 bytes apps/locale/zh/LC_MESSAGES/django.po | 693 +++++++++++------- apps/ops/apps.py | 2 + apps/orgs/signals_handler/common.py | 2 - apps/perms/api/asset/user_permission/mixin.py | 18 +- apps/settings/api/__init__.py | 2 + apps/settings/api/common.py | 8 +- apps/settings/api/dingtalk.py | 38 + apps/settings/api/wecom.py | 38 + apps/settings/serializers/settings.py | 20 +- apps/settings/urls/api_urls.py | 2 + apps/static/img/login_dingtalk_log.png | Bin 0 -> 5062 bytes apps/static/img/login_wecom_log.png | Bin 0 -> 137407 bytes apps/terminal/apps.py | 2 + .../migrations/0034_auto_20210506_1448.py | 23 + apps/users/models/user.py | 15 +- apps/users/permissions.py | 10 + apps/users/serializers/user.py | 1 + 48 files changed, 1984 insertions(+), 297 deletions(-) create mode 100644 apps/authentication/api/dingtalk.py create mode 100644 apps/authentication/api/password.py create mode 100644 apps/authentication/api/wecom.py create mode 100644 apps/authentication/views/dingtalk.py create mode 100644 apps/authentication/views/wecom.py create mode 100644 apps/common/message/__init__.py create mode 100644 apps/common/message/backends/__init__.py create mode 100644 apps/common/message/backends/dingtalk/__init__.py create mode 100644 apps/common/message/backends/exceptions.py create mode 100644 apps/common/message/backends/mixin.py create mode 100644 apps/common/message/backends/utils.py create mode 100644 apps/common/message/backends/wecom/__init__.py create mode 100644 apps/common/request_log.py create mode 100644 apps/settings/api/dingtalk.py create mode 100644 apps/settings/api/wecom.py create mode 100644 apps/static/img/login_dingtalk_log.png create mode 100644 apps/static/img/login_wecom_log.png create mode 100644 apps/users/migrations/0034_auto_20210506_1448.py create mode 100644 apps/users/permissions.py diff --git a/apps/audits/signals_handler.py b/apps/audits/signals_handler.py index c99c52bc9..930a1f03d 100644 --- a/apps/audits/signals_handler.py +++ b/apps/audits/signals_handler.py @@ -57,6 +57,8 @@ class AuthBackendLabelMapping(LazyObject): backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _('SSH Key') backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _('Password') backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO') + backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom') + backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk') return backend_label_mapping def _setup(self): diff --git a/apps/authentication/api/__init__.py b/apps/authentication/api/__init__.py index 12b83421f..6ef54b09b 100644 --- a/apps/authentication/api/__init__.py +++ b/apps/authentication/api/__init__.py @@ -7,3 +7,6 @@ from .mfa import * from .access_key import * from .login_confirm import * from .sso import * +from .wecom import * +from .dingtalk import * +from .password import * diff --git a/apps/authentication/api/dingtalk.py b/apps/authentication/api/dingtalk.py new file mode 100644 index 000000000..e4b2ea85b --- /dev/null +++ b/apps/authentication/api/dingtalk.py @@ -0,0 +1,35 @@ +from rest_framework.views import APIView +from rest_framework.request import Request +from rest_framework.response import Response + +from users.permissions import IsAuthPasswdTimeValid +from users.models import User +from common.utils import get_logger +from common.permissions import IsOrgAdmin +from common.mixins.api import RoleUserMixin, RoleAdminMixin +from authentication import errors + +logger = get_logger(__file__) + + +class DingTalkQRUnBindBase(APIView): + user: User + + def post(self, request: Request, **kwargs): + user = self.user + + if not user.dingtalk_id: + raise errors.DingTalkNotBound + + user.dingtalk_id = '' + user.save() + return Response() + + +class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase): + permission_classes = (IsAuthPasswdTimeValid,) + + +class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase): + user_id_url_kwarg = 'user_id' + permission_classes = (IsOrgAdmin,) diff --git a/apps/authentication/api/password.py b/apps/authentication/api/password.py new file mode 100644 index 000000000..af8b41358 --- /dev/null +++ b/apps/authentication/api/password.py @@ -0,0 +1,26 @@ +from rest_framework.generics import CreateAPIView +from rest_framework.response import Response + +from authentication.serializers import PasswordVerifySerializer +from common.permissions import IsValidUser +from authentication.mixins import authenticate +from authentication.errors import PasswdInvalid +from authentication.mixins import AuthMixin + + +class UserPasswordVerifyApi(AuthMixin, CreateAPIView): + permission_classes = (IsValidUser,) + serializer_class = PasswordVerifySerializer + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + password = serializer.validated_data['password'] + user = self.request.user + + user = authenticate(request=request, username=user.username, password=password) + if not user: + raise PasswdInvalid + + self.set_passwd_verify_on_session(user) + return Response() diff --git a/apps/authentication/api/wecom.py b/apps/authentication/api/wecom.py new file mode 100644 index 000000000..1ab5ff725 --- /dev/null +++ b/apps/authentication/api/wecom.py @@ -0,0 +1,35 @@ +from rest_framework.views import APIView +from rest_framework.request import Request +from rest_framework.response import Response + +from users.permissions import IsAuthPasswdTimeValid +from users.models import User +from common.utils import get_logger +from common.permissions import IsOrgAdmin +from common.mixins.api import RoleUserMixin, RoleAdminMixin +from authentication import errors + +logger = get_logger(__file__) + + +class WeComQRUnBindBase(APIView): + user: User + + def post(self, request: Request, **kwargs): + user = self.user + + if not user.wecom_id: + raise errors.WeComNotBound + + user.wecom_id = '' + user.save() + return Response() + + +class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase): + permission_classes = (IsAuthPasswdTimeValid,) + + +class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase): + user_id_url_kwarg = 'user_id' + permission_classes = (IsOrgAdmin,) diff --git a/apps/authentication/backends/api.py b/apps/authentication/backends/api.py index 1fd315abb..63356eff6 100644 --- a/apps/authentication/backends/api.py +++ b/apps/authentication/backends/api.py @@ -205,3 +205,21 @@ class SSOAuthentication(ModelBackend): def authenticate(self, request, sso_token=None, **kwargs): pass + + +class WeComAuthentication(ModelBackend): + """ + 什么也不做呀😺 + """ + + def authenticate(self, request, **kwargs): + pass + + +class DingTalkAuthentication(ModelBackend): + """ + 什么也不做呀😺 + """ + + def authenticate(self, request, **kwargs): + pass diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index 03368baa8..bcd83c97d 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -19,6 +19,7 @@ reason_user_inactive = 'user_inactive' reason_user_expired = 'user_expired' reason_backend_not_match = 'backend_not_match' reason_acl_not_allow = 'acl_not_allow' +only_local_users_are_allowed = 'only_local_users_are_allowed' reason_choices = { reason_password_failed: _('Username/password check failed'), @@ -32,6 +33,7 @@ reason_choices = { reason_user_expired: _("This account is expired"), reason_backend_not_match: _("Auth backend not match"), reason_acl_not_allow: _("ACL is not allowed"), + only_local_users_are_allowed: _("Only local users are allowed") } old_reason_choices = { '0': '-', @@ -291,3 +293,28 @@ class PasswordRequireResetError(JMSException): def __init__(self, url, *args, **kwargs): super().__init__(*args, **kwargs) self.url = url + + +class WeComCodeInvalid(JMSException): + default_code = 'wecom_code_invalid' + default_detail = 'Code invalid, can not get user info' + + +class WeComBindAlready(JMSException): + default_code = 'wecom_bind_already' + default_detail = 'WeCom already binded' + + +class WeComNotBound(JMSException): + default_code = 'wecom_not_bound' + default_detail = 'WeCom is not bound' + + +class DingTalkNotBound(JMSException): + default_code = 'dingtalk_not_bound' + default_detail = 'DingTalk is not bound' + + +class PasswdInvalid(JMSException): + default_code = 'passwd_invalid' + default_detail = _('Your password is invalid') diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 9b81d17fe..5eeceb7c3 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -5,6 +5,7 @@ from urllib.parse import urlencode from functools import partial import time +from django.core.cache import cache from django.conf import settings from django.contrib import auth from django.utils.translation import ugettext as _ @@ -12,7 +13,7 @@ from django.contrib.auth import ( BACKEND_SESSION_KEY, _get_backends, PermissionDenied, user_login_failed, _clean_credentials ) -from django.shortcuts import reverse +from django.shortcuts import reverse, redirect from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil from users.models import User @@ -82,6 +83,8 @@ class AuthMixin: request = None partial_credential_error = None + key_prefix_captcha = "_LOGIN_INVALID_{}" + def get_user_from_session(self): if self.request.session.is_empty(): raise errors.SessionEmptyError() @@ -110,11 +113,7 @@ class AuthMixin: ip = ip or get_request_ip(self.request) return ip - def check_is_block(self, raise_exception=True): - if hasattr(self.request, 'data'): - username = self.request.data.get("username") - else: - username = self.request.POST.get("username") + def _check_is_block(self, username, raise_exception=True): ip = self.get_request_ip() if LoginBlockUtil(username, ip).is_block(): logger.warn('Ip was blocked' + ': ' + username + ':' + ip) @@ -124,6 +123,13 @@ class AuthMixin: else: return exception + def check_is_block(self, raise_exception=True): + if hasattr(self.request, 'data'): + username = self.request.data.get("username") + else: + username = self.request.POST.get("username") + self._check_is_block(username, raise_exception) + def decrypt_passwd(self, raw_passwd): # 获取解密密钥,对密码进行解密 rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY) @@ -140,6 +146,9 @@ class AuthMixin: def raise_credential_error(self, error): raise self.partial_credential_error(error=error) + def _set_partial_credential_error(self, username, ip, request): + self.partial_credential_error = partial(errors.CredentialError, username=username, ip=ip, request=request) + def get_auth_data(self, decrypt_passwd=False): request = self.request if hasattr(request, 'data'): @@ -151,7 +160,7 @@ class AuthMixin: username, password, challenge, public_key, auto_login = bulk_get(data, *items, default='') password = password + challenge.strip() ip = self.get_request_ip() - self.partial_credential_error = partial(errors.CredentialError, username=username, ip=ip, request=request) + self._set_partial_credential_error(username=username, ip=ip, request=request) if decrypt_passwd: password = self.decrypt_passwd(password) @@ -184,6 +193,21 @@ class AuthMixin: if not is_allowed: raise errors.LoginIPNotAllowed(username=user.username, request=self.request) + def set_login_failed_mark(self): + ip = self.get_request_ip() + cache.set(self.key_prefix_captcha.format(ip), 1, 3600) + + def set_passwd_verify_on_session(self, user: User): + self.request.session['user_id'] = str(user.id) + self.request.session['auth_password'] = 1 + self.request.session['auth_password_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS + + def check_is_need_captcha(self): + # 最近有登录失败时需要填写验证码 + ip = get_request_ip(self.request) + need = cache.get(self.key_prefix_captcha.format(ip)) + return need + def check_user_auth(self, decrypt_passwd=False): self.check_is_block() request = self.request @@ -204,6 +228,27 @@ class AuthMixin: request.session['auth_backend'] = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL) return user + def _check_is_local_user(self, user: User): + if user.source != User.Source.local: + raise self.raise_credential_error(error=errors.only_local_users_are_allowed) + + def check_oauth2_auth(self, user: User, auth_backend): + ip = self.get_request_ip() + request = self.request + + self._set_partial_credential_error(user.username, ip, request) + self._check_is_local_user(user) + self._check_is_block(user.username) + self._check_login_acl(user, ip) + + LoginBlockUtil(user.username, ip).clean_failed_count() + MFABlockUtils(user.username, ip).clean_failed_count() + + request.session['auth_password'] = 1 + request.session['user_id'] = str(user.id) + request.session['auth_backend'] = auth_backend + return user + @classmethod def generate_reset_password_url_with_flash_msg(cls, user, message): reset_passwd_url = reverse('authentication:reset-password') @@ -354,3 +399,10 @@ class AuthMixin: sender=self.__class__, username=username, request=self.request, reason=reason ) + + def redirect_to_guard_view(self): + guard_url = reverse('authentication:login-guard') + args = self.request.META.get('QUERY_STRING', '') + if args: + guard_url = "%s?%s" % (guard_url, args) + return redirect(guard_url) diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py index 5f2bc231b..72b54e3ee 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -10,13 +10,14 @@ from applications.models import Application from users.serializers import UserProfileSerializer from assets.serializers import ProtocolsField from perms.serializers.asset.permission import ActionsField -from .models import AccessKey, LoginConfirmSetting, SSOToken +from .models import AccessKey, LoginConfirmSetting __all__ = [ 'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer', 'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer', - 'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'RDPFileSerializer' + 'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'RDPFileSerializer', + 'PasswordVerifySerializer', ] @@ -31,6 +32,10 @@ class OtpVerifySerializer(serializers.Serializer): code = serializers.CharField(max_length=6, min_length=6) +class PasswordVerifySerializer(serializers.Serializer): + password = serializers.CharField() + + class BearerTokenSerializer(serializers.Serializer): username = serializers.CharField(allow_null=True, required=False, write_only=True) password = serializers.CharField(write_only=True, allow_null=True, diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html index 722ef5d16..0104d0e44 100644 --- a/apps/authentication/templates/authentication/login.html +++ b/apps/authentication/templates/authentication/login.html @@ -117,6 +117,15 @@ float: right; margin: 10px 10px 0 0; } + .more-login-item { + border-right: 1px dashed #dedede; + padding-left: 5px; + padding-right: 5px; + } + + .more-login-item:last-child { + border: none; + } @@ -182,10 +191,10 @@
    - {% if AUTH_OPENID or AUTH_CAS %} + {% if AUTH_OPENID or AUTH_CAS or AUTH_WECOM or AUTH_DINGTALK %}
    - {% trans "More login options" %} + {% trans "More login options" %} {% if AUTH_OPENID %} {% endif %} + {% if AUTH_WECOM %} + + {% endif %} + {% if AUTH_DINGTALK %} + + {% endif %} +
    {% else %}
    diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index d9b302800..0849cc82a 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -14,10 +14,17 @@ router.register('connection-token', api.UserConnectionTokenViewSet, 'connection- urlpatterns = [ # path('token/', api.UserToken.as_view(), name='user-token'), + path('wecom/qr/unbind/', api.WeComQRUnBindForUserApi.as_view(), name='wecom-qr-unbind'), + path('wecom/qr/unbind//', api.WeComQRUnBindForAdminApi.as_view(), name='wecom-qr-unbind-for-admin'), + + path('dingtalk/qr/unbind/', api.DingTalkQRUnBindForUserApi.as_view(), name='dingtalk-qr-unbind'), + path('dingtalk/qr/unbind//', api.DingTalkQRUnBindForAdminApi.as_view(), name='dingtalk-qr-unbind-for-admin'), + path('auth/', api.TokenCreateApi.as_view(), name='user-auth'), path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'), path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'), path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'), + path('password/verify/', api.UserPasswordVerifyApi.as_view(), name='user-password-verify'), path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'), path('login-confirm-settings//', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update') ] diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index c4e4de00a..8e754340c 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -21,6 +21,22 @@ urlpatterns = [ path('password/reset/', users_view.UserResetPasswordView.as_view(), name='reset-password'), path('password/verify/', users_view.UserVerifyPasswordView.as_view(), name='user-verify-password'), + path('wecom/bind/success-flash-msg/', views.FlashWeComBindSucceedMsgView.as_view(), name='wecom-bind-success-flash-msg'), + path('wecom/bind/failed-flash-msg/', views.FlashWeComBindFailedMsgView.as_view(), name='wecom-bind-failed-flash-msg'), + path('wecom/bind/start/', views.WeComEnableStartView.as_view(), name='wecom-bind-start'), + path('wecom/qr/bind/', views.WeComQRBindView.as_view(), name='wecom-qr-bind'), + path('wecom/qr/login/', views.WeComQRLoginView.as_view(), name='wecom-qr-login'), + path('wecom/qr/bind//callback/', views.WeComQRBindCallbackView.as_view(), name='wecom-qr-bind-callback'), + path('wecom/qr/login/callback/', views.WeComQRLoginCallbackView.as_view(), name='wecom-qr-login-callback'), + + path('dingtalk/bind/success-flash-msg/', views.FlashDingTalkBindSucceedMsgView.as_view(), name='dingtalk-bind-success-flash-msg'), + path('dingtalk/bind/failed-flash-msg/', views.FlashDingTalkBindFailedMsgView.as_view(), name='dingtalk-bind-failed-flash-msg'), + path('dingtalk/bind/start/', views.DingTalkEnableStartView.as_view(), name='dingtalk-bind-start'), + path('dingtalk/qr/bind/', views.DingTalkQRBindView.as_view(), name='dingtalk-qr-bind'), + path('dingtalk/qr/login/', views.DingTalkQRLoginView.as_view(), name='dingtalk-qr-login'), + path('dingtalk/qr/bind//callback/', views.DingTalkQRBindCallbackView.as_view(), name='dingtalk-qr-bind-callback'), + path('dingtalk/qr/login/callback/', views.DingTalkQRLoginCallbackView.as_view(), name='dingtalk-qr-login-callback'), + # Profile path('profile/pubkey/generate/', users_view.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'), path('profile/otp/enable/start/', users_view.UserOtpEnableStartView.as_view(), name='user-otp-enable-start'), diff --git a/apps/authentication/views/__init__.py b/apps/authentication/views/__init__.py index 5a1a40f7a..0467e321a 100644 --- a/apps/authentication/views/__init__.py +++ b/apps/authentication/views/__init__.py @@ -2,3 +2,5 @@ # from .login import * from .mfa import * +from .wecom import * +from .dingtalk import * diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py new file mode 100644 index 000000000..521a93c26 --- /dev/null +++ b/apps/authentication/views/dingtalk.py @@ -0,0 +1,243 @@ +import urllib + +from django.http.response import HttpResponseRedirect +from django.utils.decorators import method_decorator +from django.utils.translation import ugettext as _ +from django.views.decorators.cache import never_cache +from django.views.generic import TemplateView +from django.views import View +from django.conf import settings +from django.http.request import HttpRequest +from rest_framework.permissions import IsAuthenticated, AllowAny + +from users.views import UserVerifyPasswordView +from users.utils import is_auth_password_time_valid +from users.models import User +from common.utils import get_logger +from common.utils.random import random_string +from common.utils.django import reverse, get_object_or_none +from common.message.backends.dingtalk import URL +from common.mixins.views import PermissionsMixin +from authentication import errors +from authentication.mixins import AuthMixin +from common.message.backends.dingtalk import DingTalk + +logger = get_logger(__file__) + + +DINGTALK_STATE_SESSION_KEY = '_dingtalk_state' + + +class DingTalkQRMixin(PermissionsMixin, View): + def verify_state(self): + state = self.request.GET.get('state') + session_state = self.request.session.get(DINGTALK_STATE_SESSION_KEY) + if state != session_state: + return False + return True + + def get_verify_state_failed_response(self, redirect_uri): + msg = _("You've been hacked") + return self.get_failed_reponse(redirect_uri, msg, msg) + + def get_qr_url(self, redirect_uri): + state = random_string(16) + self.request.session[DINGTALK_STATE_SESSION_KEY] = state + + params = { + 'appid': settings.DINGTALK_APPKEY, + 'response_type': 'code', + 'scope': 'snsapi_login', + 'state': state, + 'redirect_uri': redirect_uri, + } + url = URL.QR_CONNECT + '?' + urllib.parse.urlencode(params) + return url + + def get_success_reponse(self, redirect_url, title, msg): + ok_flash_msg_url = reverse('authentication:dingtalk-bind-success-flash-msg') + ok_flash_msg_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url, + 'title': title, + 'msg': msg + }) + return HttpResponseRedirect(ok_flash_msg_url) + + def get_failed_reponse(self, redirect_url, title, msg): + failed_flash_msg_url = reverse('authentication:dingtalk-bind-failed-flash-msg') + failed_flash_msg_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url, + 'title': title, + 'msg': msg + }) + return HttpResponseRedirect(failed_flash_msg_url) + + def get_already_bound_response(self, redirect_url): + msg = _('DingTalk is already bound') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + +class DingTalkQRBindView(DingTalkQRMixin, View): + permission_classes = (IsAuthenticated,) + + def get(self, request: HttpRequest): + user = request.user + redirect_url = request.GET.get('redirect_url') + + if not is_auth_password_time_valid(request.session): + msg = _('Please verify your password first') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + redirect_uri = reverse('authentication:dingtalk-qr-bind-callback', kwargs={'user_id': user.id}, external=True) + redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url}) + + url = self.get_qr_url(redirect_uri) + return HttpResponseRedirect(url) + + +class DingTalkQRBindCallbackView(DingTalkQRMixin, View): + permission_classes = (IsAuthenticated,) + + def get(self, request: HttpRequest, user_id): + code = request.GET.get('code') + redirect_url = request.GET.get('redirect_url') + + if not self.verify_state(): + return self.get_verify_state_failed_response(redirect_url) + + user = get_object_or_none(User, id=user_id) + if user is None: + logger.error(f'DingTalkQR bind callback error, user_id invalid: user_id={user_id}') + msg = _('Invalid user_id') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + if user.dingtalk_id: + response = self.get_already_bound_response(redirect_url) + return response + + dingtalk = DingTalk( + appid=settings.DINGTALK_APPKEY, + appsecret=settings.DINGTALK_APPSECRET, + agentid=settings.DINGTALK_AGENTID + ) + userid = dingtalk.get_userid_by_code(code) + + if not userid: + msg = _('DingTalk query user failed') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + user.dingtalk_id = userid + user.save() + + msg = _('Binding DingTalk successfully') + response = self.get_success_reponse(redirect_url, msg, msg) + return response + + +class DingTalkEnableStartView(UserVerifyPasswordView): + + def get_success_url(self): + referer = self.request.META.get('HTTP_REFERER') + redirect_url = self.request.GET.get("redirect_url") + + success_url = reverse('authentication:dingtalk-qr-bind') + + success_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url or referer + }) + + return success_url + + +class DingTalkQRLoginView(DingTalkQRMixin, View): + permission_classes = (AllowAny,) + + def get(self, request: HttpRequest): + redirect_url = request.GET.get('redirect_url') + + redirect_uri = reverse('authentication:dingtalk-qr-login-callback', external=True) + redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url}) + + url = self.get_qr_url(redirect_uri) + return HttpResponseRedirect(url) + + +class DingTalkQRLoginCallbackView(AuthMixin, DingTalkQRMixin, View): + permission_classes = (AllowAny,) + + def get(self, request: HttpRequest): + code = request.GET.get('code') + redirect_url = request.GET.get('redirect_url') + login_url = reverse('authentication:login') + + if not self.verify_state(): + return self.get_verify_state_failed_response(redirect_url) + + dingtalk = DingTalk( + appid=settings.DINGTALK_APPKEY, + appsecret=settings.DINGTALK_APPSECRET, + agentid=settings.DINGTALK_AGENTID + ) + userid = dingtalk.get_userid_by_code(code) + if not userid: + # 正常流程不会出这个错误,hack 行为 + msg = _('Failed to get user from DingTalk') + response = self.get_failed_reponse(login_url, title=msg, msg=msg) + return response + + user = get_object_or_none(User, dingtalk_id=userid) + if user is None: + title = _('DingTalk is not bound') + msg = _('Please login with a password and then bind the WoCom') + response = self.get_failed_reponse(login_url, title=title, msg=msg) + return response + + try: + self.check_oauth2_auth(user, settings.AUTH_BACKEND_DINGTALK) + except errors.AuthFailedError as e: + self.set_login_failed_mark() + msg = e.msg + response = self.get_failed_reponse(login_url, title=msg, msg=msg) + return response + + return self.redirect_to_guard_view() + + +@method_decorator(never_cache, name='dispatch') +class FlashDingTalkBindSucceedMsgView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + title = request.GET.get('title') + msg = request.GET.get('msg') + + context = { + 'title': title or _('Binding DingTalk successfully'), + 'messages': msg or _('Binding DingTalk successfully'), + 'interval': 5, + 'redirect_url': request.GET.get('redirect_url'), + 'auto_redirect': True, + } + return self.render_to_response(context) + + +@method_decorator(never_cache, name='dispatch') +class FlashDingTalkBindFailedMsgView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + title = request.GET.get('title') + msg = request.GET.get('msg') + + context = { + 'title': title or _('Binding DingTalk failed'), + 'messages': msg or _('Binding DingTalk failed'), + 'interval': 5, + 'redirect_url': request.GET.get('redirect_url'), + 'auto_redirect': True, + } + return self.render_to_response(context) diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 2412ef54a..083418940 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import os import datetime -from django.core.cache import cache from django.contrib.auth import login as auth_login, logout as auth_logout from django.http import HttpResponse from django.shortcuts import reverse, redirect @@ -38,7 +37,6 @@ __all__ = [ @method_decorator(csrf_protect, name='dispatch') @method_decorator(never_cache, name='dispatch') class UserLoginView(mixins.AuthMixin, FormView): - key_prefix_captcha = "_LOGIN_INVALID_{}" redirect_field_name = 'next' template_name = 'authentication/login.html' @@ -90,10 +88,9 @@ class UserLoginView(mixins.AuthMixin, FormView): try: self.check_user_auth(decrypt_passwd=True) except errors.AuthFailedError as e: - e = self.check_is_block(raise_exception=False) or e form.add_error(None, e.msg) - ip = self.get_request_ip() - cache.set(self.key_prefix_captcha.format(ip), 1, 3600) + self.set_login_failed_mark() + form_cls = get_user_login_form_cls(captcha=True) new_form = form_cls(data=form.data) new_form._errors = form.errors @@ -105,16 +102,8 @@ class UserLoginView(mixins.AuthMixin, FormView): self.clear_rsa_key() return self.redirect_to_guard_view() - def redirect_to_guard_view(self): - guard_url = reverse('authentication:login-guard') - args = self.request.META.get('QUERY_STRING', '') - if args: - guard_url = "%s?%s" % (guard_url, args) - return redirect(guard_url) - def get_form_class(self): - ip = get_request_ip(self.request) - if cache.get(self.key_prefix_captcha.format(ip)): + if self.check_is_need_captcha(): return get_user_login_form_cls(captcha=True) else: return get_user_login_form_cls() @@ -142,6 +131,8 @@ class UserLoginView(mixins.AuthMixin, FormView): 'demo_mode': os.environ.get("DEMO_MODE"), 'AUTH_OPENID': settings.AUTH_OPENID, 'AUTH_CAS': settings.AUTH_CAS, + 'AUTH_WECOM': settings.AUTH_WECOM, + 'AUTH_DINGTALK': settings.AUTH_DINGTALK, 'rsa_public_key': rsa_public_key, 'forgot_password_url': forgot_password_url } diff --git a/apps/authentication/views/wecom.py b/apps/authentication/views/wecom.py new file mode 100644 index 000000000..5dc683f87 --- /dev/null +++ b/apps/authentication/views/wecom.py @@ -0,0 +1,241 @@ +import urllib + +from django.http.response import HttpResponseRedirect +from django.utils.decorators import method_decorator +from django.utils.translation import ugettext as _ +from django.views.decorators.cache import never_cache +from django.views.generic import TemplateView +from django.views import View +from django.conf import settings +from django.http.request import HttpRequest +from rest_framework.permissions import IsAuthenticated, AllowAny + +from users.views import UserVerifyPasswordView +from users.utils import is_auth_password_time_valid +from users.models import User +from common.utils import get_logger +from common.utils.random import random_string +from common.utils.django import reverse, get_object_or_none +from common.message.backends.wecom import URL +from common.message.backends.wecom import WeCom +from common.mixins.views import PermissionsMixin +from authentication import errors +from authentication.mixins import AuthMixin + +logger = get_logger(__file__) + + +WECOM_STATE_SESSION_KEY = '_wecom_state' + + +class WeComQRMixin(PermissionsMixin, View): + def verify_state(self): + state = self.request.GET.get('state') + session_state = self.request.session.get(WECOM_STATE_SESSION_KEY) + if state != session_state: + return False + return True + + def get_verify_state_failed_response(self, redirect_uri): + msg = _("You've been hacked") + return self.get_failed_reponse(redirect_uri, msg, msg) + + def get_qr_url(self, redirect_uri): + state = random_string(16) + self.request.session[WECOM_STATE_SESSION_KEY] = state + + params = { + 'appid': settings.WECOM_CORPID, + 'agentid': settings.WECOM_AGENTID, + 'state': state, + 'redirect_uri': redirect_uri, + } + url = URL.QR_CONNECT + '?' + urllib.parse.urlencode(params) + return url + + def get_success_reponse(self, redirect_url, title, msg): + ok_flash_msg_url = reverse('authentication:wecom-bind-success-flash-msg') + ok_flash_msg_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url, + 'title': title, + 'msg': msg + }) + return HttpResponseRedirect(ok_flash_msg_url) + + def get_failed_reponse(self, redirect_url, title, msg): + failed_flash_msg_url = reverse('authentication:wecom-bind-failed-flash-msg') + failed_flash_msg_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url, + 'title': title, + 'msg': msg + }) + return HttpResponseRedirect(failed_flash_msg_url) + + def get_already_bound_response(self, redirect_url): + msg = _('WeCom is already bound') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + +class WeComQRBindView(WeComQRMixin, View): + permission_classes = (IsAuthenticated,) + + def get(self, request: HttpRequest): + user = request.user + redirect_url = request.GET.get('redirect_url') + + if not is_auth_password_time_valid(request.session): + msg = _('Please verify your password first') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + redirect_uri = reverse('authentication:wecom-qr-bind-callback', kwargs={'user_id': user.id}, external=True) + redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url}) + + url = self.get_qr_url(redirect_uri) + return HttpResponseRedirect(url) + + +class WeComQRBindCallbackView(WeComQRMixin, View): + permission_classes = (IsAuthenticated,) + + def get(self, request: HttpRequest, user_id): + code = request.GET.get('code') + redirect_url = request.GET.get('redirect_url') + + if not self.verify_state(): + return self.get_verify_state_failed_response(redirect_url) + + user = get_object_or_none(User, id=user_id) + if user is None: + logger.error(f'WeComQR bind callback error, user_id invalid: user_id={user_id}') + msg = _('Invalid user_id') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + if user.wecom_id: + response = self.get_already_bound_response(redirect_url) + return response + + wecom = WeCom( + corpid=settings.WECOM_CORPID, + corpsecret=settings.WECOM_CORPSECRET, + agentid=settings.WECOM_AGENTID + ) + wecom_userid, __ = wecom.get_user_id_by_code(code) + if not wecom_userid: + msg = _('WeCom query user failed') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + user.wecom_id = wecom_userid + user.save() + + msg = _('Binding WeCom successfully') + response = self.get_success_reponse(redirect_url, msg, msg) + return response + + +class WeComEnableStartView(UserVerifyPasswordView): + + def get_success_url(self): + referer = self.request.META.get('HTTP_REFERER') + redirect_url = self.request.GET.get("redirect_url") + + success_url = reverse('authentication:wecom-qr-bind') + + success_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url or referer + }) + + return success_url + + +class WeComQRLoginView(WeComQRMixin, View): + permission_classes = (AllowAny,) + + def get(self, request: HttpRequest): + redirect_url = request.GET.get('redirect_url') + + redirect_uri = reverse('authentication:wecom-qr-login-callback', external=True) + redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url}) + + url = self.get_qr_url(redirect_uri) + return HttpResponseRedirect(url) + + +class WeComQRLoginCallbackView(AuthMixin, WeComQRMixin, View): + permission_classes = (AllowAny,) + + def get(self, request: HttpRequest): + code = request.GET.get('code') + redirect_url = request.GET.get('redirect_url') + login_url = reverse('authentication:login') + + if not self.verify_state(): + return self.get_verify_state_failed_response(redirect_url) + + wecom = WeCom( + corpid=settings.WECOM_CORPID, + corpsecret=settings.WECOM_CORPSECRET, + agentid=settings.WECOM_AGENTID + ) + wecom_userid, __ = wecom.get_user_id_by_code(code) + if not wecom_userid: + # 正常流程不会出这个错误,hack 行为 + msg = _('Failed to get user from WeCom') + response = self.get_failed_reponse(login_url, title=msg, msg=msg) + return response + + user = get_object_or_none(User, wecom_id=wecom_userid) + if user is None: + title = _('WeCom is not bound') + msg = _('Please login with a password and then bind the WoCom') + response = self.get_failed_reponse(login_url, title=title, msg=msg) + return response + + try: + self.check_oauth2_auth(user, settings.AUTH_BACKEND_WECOM) + except errors.AuthFailedError as e: + self.set_login_failed_mark() + msg = e.msg + response = self.get_failed_reponse(login_url, title=msg, msg=msg) + return response + + return self.redirect_to_guard_view() + + +@method_decorator(never_cache, name='dispatch') +class FlashWeComBindSucceedMsgView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + title = request.GET.get('title') + msg = request.GET.get('msg') + + context = { + 'title': title or _('Binding WeCom successfully'), + 'messages': msg or _('Binding WeCom successfully'), + 'interval': 5, + 'redirect_url': request.GET.get('redirect_url'), + 'auto_redirect': True, + } + return self.render_to_response(context) + + +@method_decorator(never_cache, name='dispatch') +class FlashWeComBindFailedMsgView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + title = request.GET.get('title') + msg = request.GET.get('msg') + + context = { + 'title': title or _('Binding WeCom failed'), + 'messages': msg or _('Binding WeCom failed'), + 'interval': 5, + 'redirect_url': request.GET.get('redirect_url'), + 'auto_redirect': True, + } + return self.render_to_response(context) diff --git a/apps/common/message/__init__.py b/apps/common/message/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/common/message/backends/__init__.py b/apps/common/message/backends/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/common/message/backends/dingtalk/__init__.py b/apps/common/message/backends/dingtalk/__init__.py new file mode 100644 index 000000000..0ca9d5dc5 --- /dev/null +++ b/apps/common/message/backends/dingtalk/__init__.py @@ -0,0 +1,168 @@ +import time +import hmac +import base64 + +from common.message.backends.utils import request +from common.message.backends.utils import digest +from common.message.backends.mixin import BaseRequest + + +def sign(secret, data): + + digest = hmac.HMAC( + key=secret.encode('utf8'), + msg=data.encode('utf8'), + digestmod=hmac._hashlib.sha256).digest() + signature = base64.standard_b64encode(digest).decode('utf8') + # signature = urllib.parse.quote(signature, safe='') + # signature = signature.replace('+', '%20').replace('*', '%2A').replace('~', '%7E').replace('/', '%2F') + return signature + + +class ErrorCode: + INVALID_TOKEN = 88 + + +class URL: + QR_CONNECT = 'https://oapi.dingtalk.com/connect/qrconnect' + GET_USER_INFO_BY_CODE = 'https://oapi.dingtalk.com/sns/getuserinfo_bycode' + GET_TOKEN = 'https://oapi.dingtalk.com/gettoken' + SEND_MESSAGE_BY_TEMPLATE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/sendbytemplate' + SEND_MESSAGE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2' + GET_SEND_MSG_PROGRESS = 'https://oapi.dingtalk.com/topapi/message/corpconversation/getsendprogress' + GET_USERID_BY_UNIONID = 'https://oapi.dingtalk.com/topapi/user/getbyunionid' + + +class DingTalkRequests(BaseRequest): + invalid_token_errcode = ErrorCode.INVALID_TOKEN + + def __init__(self, appid, appsecret, agentid, timeout=None): + self._appid = appid + self._appsecret = appsecret + self._agentid = agentid + + super().__init__(timeout=timeout) + + def get_access_token_cache_key(self): + return digest(self._appid, self._appsecret) + + def request_access_token(self): + # https://developers.dingtalk.com/document/app/obtain-orgapp-token?spm=ding_open_doc.document.0.0.3a256573JEWqIL#topic-1936350 + params = {'appkey': self._appid, 'appsecret': self._appsecret} + data = self.raw_request('get', url=URL.GET_TOKEN, params=params) + + access_token = data['access_token'] + expires_in = data['expires_in'] + return access_token, expires_in + + @request + def get(self, url, params=None, + with_token=False, with_sign=False, + check_errcode_is_0=True, + **kwargs): + pass + + @request + def post(self, url, json=None, params=None, + with_token=False, with_sign=False, + check_errcode_is_0=True, + **kwargs): + pass + + def _add_sign(self, params: dict): + timestamp = str(int(time.time() * 1000)) + signature = sign(self._appsecret, timestamp) + accessKey = self._appid + + params['timestamp'] = timestamp + params['signature'] = signature + params['accessKey'] = accessKey + + def request(self, method, url, params=None, + with_token=False, with_sign=False, + check_errcode_is_0=True, + **kwargs): + if not isinstance(params, dict): + params = {} + + if with_token: + params['access_token'] = self.access_token + + if with_sign: + self._add_sign(params) + + data = self.raw_request(method, url, params=params, **kwargs) + if check_errcode_is_0: + self.check_errcode_is_0(data) + + return data + + +class DingTalk: + def __init__(self, appid, appsecret, agentid, timeout=None): + self._appid = appid + self._appsecret = appsecret + self._agentid = agentid + + self._request = DingTalkRequests( + appid=appid, appsecret=appsecret, agentid=agentid, + timeout=timeout + ) + + def get_userinfo_bycode(self, code): + # https://developers.dingtalk.com/document/app/obtain-the-user-information-based-on-the-sns-temporary-authorization?spm=ding_open_doc.document.0.0.3a256573y8Y7yg#topic-1995619 + body = { + "tmp_auth_code": code + } + + data = self._request.post(URL.GET_USER_INFO_BY_CODE, json=body, with_sign=True) + return data['user_info'] + + def get_userid_by_code(self, code): + user_info = self.get_userinfo_bycode(code) + unionid = user_info['unionid'] + userid = self.get_userid_by_unionid(unionid) + return userid + + def get_userid_by_unionid(self, unionid): + body = { + 'unionid': unionid + } + data = self._request.post(URL.GET_USERID_BY_UNIONID, json=body, with_token=True) + userid = data['result']['userid'] + return userid + + def send_by_template(self, template_id, user_ids, dept_ids, data): + body = { + 'agent_id': self._agentid, + 'template_id': template_id, + 'userid_list': ','.join(user_ids), + 'dept_id_list': ','.join(dept_ids), + 'data': data + } + data = self._request.post(URL.SEND_MESSAGE_BY_TEMPLATE, json=body, with_token=True) + + def send_text(self, user_ids, msg): + body = { + 'agent_id': self._agentid, + 'userid_list': ','.join(user_ids), + # 'dept_id_list': '', + 'to_all_user': False, + 'msg': { + 'msgtype': 'text', + 'text': { + 'content': msg + } + } + } + data = self._request.post(URL.SEND_MESSAGE, json=body, with_token=True) + return data + + def get_send_msg_progress(self, task_id): + body = { + 'agent_id': self._agentid, + 'task_id': task_id + } + + data = self._request.post(URL.GET_SEND_MSG_PROGRESS, json=body, with_token=True) + return data diff --git a/apps/common/message/backends/exceptions.py b/apps/common/message/backends/exceptions.py new file mode 100644 index 000000000..f72e8694d --- /dev/null +++ b/apps/common/message/backends/exceptions.py @@ -0,0 +1,28 @@ +from django.utils.translation import gettext_lazy as _ + +from rest_framework.exceptions import APIException + + +class HTTPNot200(APIException): + default_code = 'http_not_200' + default_detail = 'HTTP status is not 200' + + +class ErrCodeNot0(APIException): + default_code = 'errcode_not_0' + default_detail = 'Error code is not 0' + + +class ResponseDataKeyError(APIException): + default_code = 'response_data_key_error' + default_detail = 'Response data key error' + + +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 new file mode 100644 index 000000000..3beb60272 --- /dev/null +++ b/apps/common/message/backends/mixin.py @@ -0,0 +1,94 @@ +import requests +from requests import exceptions as req_exce +from rest_framework.exceptions import PermissionDenied +from django.core.cache import cache + +from .utils import DictWrapper +from common.utils.common import get_logger +from common.utils import lazyproperty +from common.message.backends.utils import set_default + +from . import exceptions as exce + +logger = get_logger(__name__) + + +class RequestMixin: + def check_errcode_is_0(self, data: DictWrapper): + errcode = data['errcode'] + if errcode != 0: + # 如果代码写的对,配置没问题,这里不该出错,系统性错误,直接抛异常 + errmsg = data['errmsg'] + logger.error(f'Response 200 but errcode is not 0: ' + f'errcode={errcode} ' + f'errmsg={errmsg} ') + raise exce.ErrCodeNot0(detail=str(data.raw_data)) + + def check_http_is_200(self, response): + if response.status_code != 200: + # 正常情况下不会返回非 200 响应码 + logger.error(f'Response error: ' + f'status_code={response.status_code} ' + f'url={response.url}' + f'\ncontent={response.content}') + raise exce.HTTPNot200 + + +class BaseRequest(RequestMixin): + invalid_token_errcode = -1 + + def __init__(self, timeout=None): + self._request_kwargs = { + 'timeout': timeout + } + self.init_access_token() + + def request_access_token(self): + raise NotImplementedError + + def get_access_token_cache_key(self): + raise NotImplementedError + + def is_token_invalid(self, data): + errcode = data['errcode'] + if errcode == self.invalid_token_errcode: + return True + return False + + @lazyproperty + def access_token_cache_key(self): + return self.get_access_token_cache_key() + + def init_access_token(self): + access_token = cache.get(self.access_token_cache_key) + if access_token: + self.access_token = access_token + return + self.refresh_access_token() + + def refresh_access_token(self): + access_token, expires_in = self.request_access_token() + self.access_token = access_token + cache.set(self.access_token_cache_key, access_token, expires_in) + + def raw_request(self, method, url, **kwargs): + set_default(kwargs, self._request_kwargs) + raw_data = '' + for i in range(3): + # 循环为了防止 access_token 失效 + try: + response = getattr(requests, method)(url, **kwargs) + self.check_http_is_200(response) + raw_data = response.json() + data = DictWrapper(raw_data) + + if self.is_token_invalid(data): + self.refresh_access_token() + continue + + return data + except req_exce.ReadTimeout as e: + logger.exception(e) + raise exce.NetError + logger.error(f'Get access_token error, check config: url={url} data={raw_data}') + raise PermissionDenied(raw_data) diff --git a/apps/common/message/backends/utils.py b/apps/common/message/backends/utils.py new file mode 100644 index 000000000..6c6f2b593 --- /dev/null +++ b/apps/common/message/backends/utils.py @@ -0,0 +1,78 @@ +import hashlib +import inspect +from inspect import Parameter + +from common.utils.common import get_logger +from common.message.backends import exceptions as exce + +logger = get_logger(__name__) + + +def digest(corpid, corpsecret): + md5 = hashlib.md5() + md5.update(corpid.encode()) + md5.update(corpsecret.encode()) + digest = md5.hexdigest() + return digest + + +def update_values(default: dict, others: dict): + for key in default.keys(): + if key in others: + default[key] = others[key] + + +def set_default(data: dict, default: dict): + for key in default.keys(): + if key not in data: + data[key] = default[key] + + +class DictWrapper: + def __init__(self, data:dict): + self.raw_data = data + + def __getitem__(self, item): + # 网络请求返回的数据,不能完全信任,所以字典操作包在异常里 + try: + return self.raw_data[item] + 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) + + def __getattr__(self, item): + return getattr(self.raw_data, item) + + def __contains__(self, item): + return item in self.raw_data + + def __str__(self): + return str(self.raw_data) + + def __repr__(self): + return str(self.raw_data) + + +def request(func): + def inner(*args, **kwargs): + signature = inspect.signature(func) + bound_args = signature.bind(*args, **kwargs) + bound_args.apply_defaults() + + arguments = bound_args.arguments + self = arguments['self'] + request_method = func.__name__ + + parameters = {} + for k, v in signature.parameters.items(): + if k == 'self': + continue + if v.kind is Parameter.VAR_KEYWORD: + parameters.update(arguments[k]) + continue + parameters[k] = arguments[k] + + response = self.request(request_method, **parameters) + return response + return inner diff --git a/apps/common/message/backends/wecom/__init__.py b/apps/common/message/backends/wecom/__init__.py new file mode 100644 index 000000000..257da47a0 --- /dev/null +++ b/apps/common/message/backends/wecom/__init__.py @@ -0,0 +1,194 @@ +from typing import Iterable, AnyStr + +from django.utils.translation import ugettext_lazy as _ +from rest_framework.exceptions import APIException +from requests.exceptions import ReadTimeout +import requests +from django.core.cache import cache + +from common.utils.common import get_logger +from common.message.backends.utils import digest, DictWrapper, update_values, set_default +from common.message.backends.utils import request +from common.message.backends.mixin import RequestMixin, BaseRequest + +logger = get_logger(__name__) + + +class WeComError(APIException): + default_code = 'wecom_error' + default_detail = _('WeCom error, please contact system administrator') + + +class URL: + GET_TOKEN = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken' + SEND_MESSAGE = 'https://qyapi.weixin.qq.com/cgi-bin/message/send' + QR_CONNECT = 'https://open.work.weixin.qq.com/wwopen/sso/qrConnect' + + # https://open.work.weixin.qq.com/api/doc/90000/90135/91437 + GET_USER_ID_BY_CODE = 'https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo' + GET_USER_DETAIL = 'https://qyapi.weixin.qq.com/cgi-bin/user/get' + + +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/api/doc/90000/90135/91437 + INVALID_CODE = 40029 + + INVALID_TOKEN = 40014 # 无效的 access_token + + +class WeComRequests(BaseRequest): + """ + 处理系统级错误,抛出 API 异常,直接生成 HTTP 响应,业务代码无需关心这些错误 + - 确保 status_code == 200 + - 确保 access_token 无效时重试 + """ + invalid_token_errcode = ErrorCode.INVALID_TOKEN + + def __init__(self, corpid, corpsecret, agentid, timeout=None): + self._corpid = corpid + self._corpsecret = corpsecret + self._agentid = agentid + + super().__init__(timeout=timeout) + + def get_access_token_cache_key(self): + return digest(self._corpid, self._corpsecret) + + def request_access_token(self): + params = {'corpid': self._corpid, 'corpsecret': self._corpsecret} + data = self.raw_request('get', url=URL.GET_TOKEN, params=params) + + access_token = data['access_token'] + expires_in = data['expires_in'] + return access_token, expires_in + + @request + def get(self, url, params=None, with_token=True, + check_errcode_is_0=True, **kwargs): + # self.request ... + pass + + @request + def post(self, url, params=None, json=None, + with_token=True, check_errcode_is_0=True, + **kwargs): + # self.request ... + pass + + def request(self, method, url, + params=None, + with_token=True, + check_errcode_is_0=True, + **kwargs): + + if not isinstance(params, dict): + params = {} + + if with_token: + params['access_token'] = self.access_token + + data = self.raw_request(method, url, params=params, **kwargs) + if check_errcode_is_0: + self.check_errcode_is_0(data) + return data + + +class WeCom(RequestMixin): + """ + 非业务数据导致的错误直接抛异常,说明是系统配置错误,业务代码不用理会 + """ + + def __init__(self, corpid, corpsecret, agentid, timeout=None): + self._corpid = corpid + self._corpsecret = corpsecret + self._agentid = agentid + + self._requests = WeComRequests( + corpid=corpid, + corpsecret=corpsecret, + agentid=agentid, + timeout=timeout + ) + + def send_text(self, users: Iterable, msg: AnyStr, **kwargs): + """ + https://open.work.weixin.qq.com/api/doc/90000/90135/90236 + + 对于业务代码,只需要关心由 用户id 或 消息不对 导致的错误,其他错误不予理会 + """ + users = tuple(users) + + extra_params = { + "safe": 0, + "enable_id_trans": 0, + "enable_duplicate_check": 0, + "duplicate_check_interval": 1800 + } + update_values(extra_params, kwargs) + + body = { + "touser": '|'.join(users), + "msgtype": "text", + "agentid": self._agentid, + "text": { + "content": msg + }, + **extra_params + } + data = self._requests.post(URL.SEND_MESSAGE, json=body, check_errcode_is_0=False) + + errcode = data['errcode'] + if errcode == ErrorCode.RECIPIENTS_INVALID: + # 全部接收人无权限或不存在 + return users + self.check_errcode_is_0(data) + + invaliduser = data['invaliduser'] + if not invaliduser: + return () + + if isinstance(invaliduser, str): + logger.error(f'WeCom send text 200, but invaliduser is not str: invaliduser={invaliduser}') + raise WeComError + + invalid_users = invaliduser.split('|') + return invalid_users + + def get_user_id_by_code(self, code): + # # https://open.work.weixin.qq.com/api/doc/90000/90135/91437 + + params = { + 'code': code, + } + data = self._requests.get(URL.GET_USER_ID_BY_CODE, params=params, check_errcode_is_0=False) + + errcode = data['errcode'] + if errcode == ErrorCode.INVALID_CODE: + logger.warn(f'WeCom get_user_id_by_code invalid code: code={code}') + return None, None + + self.check_errcode_is_0(data) + + USER_ID = 'UserId' + OPEN_ID = 'OpenId' + + if USER_ID in data: + return data[USER_ID], USER_ID + elif OPEN_ID in data: + return data[OPEN_ID], OPEN_ID + else: + logger.error(f'WeCom response 200 but get field from json error: fields=UserId|OpenId') + raise WeComError + + def get_user_detail(self, id): + # https://open.work.weixin.qq.com/api/doc/90000/90135/90196 + + params = { + 'userid': id, + } + + data = self._requests.get(URL.GET_USER_DETAIL, params) + return data diff --git a/apps/common/mixins/api.py b/apps/common/mixins/api.py index ea629bb84..a0d5875c6 100644 --- a/apps/common/mixins/api.py +++ b/apps/common/mixins/api.py @@ -6,10 +6,12 @@ from threading import Thread from collections import defaultdict from itertools import chain +from django.conf import settings from django.db.models.signals import m2m_changed from django.core.cache import cache from django.http import JsonResponse from django.utils.translation import ugettext as _ +from django.contrib.auth import get_user_model from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.decorators import action @@ -25,6 +27,9 @@ __all__ = [ ] +UserModel = get_user_model() + + class JSONResponseMixin(object): """JSON mixin""" @staticmethod @@ -332,3 +337,21 @@ class AllowBulkDestoryMixin: """ query = str(filtered.query) return '`id` IN (' in query or '`id` =' in query + + +class RoleAdminMixin: + kwargs: dict + user_id_url_kwarg = 'pk' + + @lazyproperty + def user(self): + user_id = self.kwargs.get(self.user_id_url_kwarg) + return UserModel.objects.get(id=user_id) + + +class RoleUserMixin: + request: Request + + @lazyproperty + def user(self): + return self.request.user diff --git a/apps/common/request_log.py b/apps/common/request_log.py new file mode 100644 index 000000000..c35e6b84a --- /dev/null +++ b/apps/common/request_log.py @@ -0,0 +1,15 @@ +from django.http.request import HttpRequest +from django.http.response import HttpResponse + +from orgs.utils import current_org + + +class RequestLogMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request: HttpRequest): + print(f'Request {request.method} --> ', request.get_raw_uri()) + response: HttpResponse = self.get_response(request) + print(f'Response {current_org.name} {request.method} {response.status_code} --> ', request.get_raw_uri()) + return response diff --git a/apps/common/utils/random.py b/apps/common/utils/random.py index 055966947..a9ef0421f 100644 --- a/apps/common/utils/random.py +++ b/apps/common/utils/random.py @@ -4,7 +4,6 @@ import struct import random import socket import string -import secrets string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_{}~' diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index b51c7dfba..57c754942 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -216,6 +216,16 @@ class Config(dict): 'AUTH_SSO': False, 'AUTH_SSO_AUTHKEY_TTL': 60 * 15, + 'AUTH_WECOM': False, + 'WECOM_CORPID': '', + 'WECOM_AGENTID': '', + 'WECOM_CORPSECRET': '', + + 'AUTH_DINGTALK': False, + 'DINGTALK_AGENTID': '', + 'DINGTALK_APPKEY': '', + 'DINGTALK_APPSECRET': '', + 'OTP_VALID_WINDOW': 2, 'OTP_ISSUER_NAME': 'JumpServer', 'EMAIL_SUFFIX': 'jumpserver.org', diff --git a/apps/jumpserver/context_processor.py b/apps/jumpserver/context_processor.py index ad8c8c90f..fbd7016d3 100644 --- a/apps/jumpserver/context_processor.py +++ b/apps/jumpserver/context_processor.py @@ -14,6 +14,8 @@ def jumpserver_processor(request): 'LOGIN_IMAGE_URL': static('img/login_image.png'), 'FAVICON_URL': static('img/facio.ico'), 'LOGIN_CAS_LOGO_URL': static('img/login_cas_logo.png'), + 'LOGIN_WECOM_LOGO_URL': static('img/login_wecom_log.png'), + 'LOGIN_DINGTALK_LOGO_URL': static('img/login_dingtalk_log.png'), 'JMS_TITLE': _('JumpServer Open Source Bastion Host'), 'VERSION': settings.VERSION, 'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2021', diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 579a0e66f..4fb77cb47 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -101,6 +101,19 @@ CAS_CHECK_NEXT = lambda _next_page: True AUTH_SSO = CONFIG.AUTH_SSO AUTH_SSO_AUTHKEY_TTL = CONFIG.AUTH_SSO_AUTHKEY_TTL +# WECOM Auth +AUTH_WECOM = CONFIG.AUTH_WECOM +WECOM_CORPID = CONFIG.WECOM_CORPID +WECOM_AGENTID = CONFIG.WECOM_AGENTID +WECOM_CORPSECRET = CONFIG.WECOM_CORPSECRET + +# DingDing auth +AUTH_DINGTALK = CONFIG.AUTH_DINGTALK +DINGTALK_AGENTID = CONFIG.DINGTALK_AGENTID +DINGTALK_APPKEY = CONFIG.DINGTALK_APPKEY +DINGTALK_APPSECRET = CONFIG.DINGTALK_APPSECRET + + # Other setting TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE @@ -115,6 +128,8 @@ AUTH_BACKEND_OIDC_CODE = 'jms_oidc_rp.backends.OIDCAuthCodeBackend' AUTH_BACKEND_RADIUS = 'authentication.backends.radius.RadiusBackend' AUTH_BACKEND_CAS = 'authentication.backends.cas.CASBackend' AUTH_BACKEND_SSO = 'authentication.backends.api.SSOAuthentication' +AUTH_BACKEND_WECOM = 'authentication.backends.api.WeComAuthentication' +AUTH_BACKEND_DINGTALK = 'authentication.backends.api.DingTalkAuthentication' AUTHENTICATION_BACKENDS = [AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY] @@ -128,6 +143,10 @@ if AUTH_RADIUS: AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_RADIUS) if AUTH_SSO: AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_SSO) +if AUTH_WECOM: + AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_WECOM) +if AUTH_DINGTALK: + AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_DINGTALK) ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index ca9870043..687b7f2ae 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -23,7 +23,7 @@ api_v1 = [ path('applications/', include('applications.urls.api_urls', namespace='api-applications')), path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')), path('acls/', include('acls.urls.api_urls', namespace='api-acls')), - path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()) + path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()), ] app_view_patterns = [ diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 2bc2e78e23f4cc398283cac0b6957f9ae0142639..c7bd94308a579bcb334dd636c31e63416167f69f 100644 GIT binary patch delta 23230 zcma*v1)NpYyZ`Y$L3hUx2N=3bkd#nrC~1)xU}zYa8A{>|APpi6-QDR>GNhzRNh_%` zGk~aoAO_(5e9wO7ck$lWKlbY`-fKOp*WUY_8Sr=a?GB7P5$L_0IdHDS^&r4;@?ekr zj?*%k<7}y=sN+0t^ zrc$2R$#J4FQ)kDyfwS=}{Reh&oc)gDb!vnnNaP6Fe4U1Z&osu$fU+bW^2@r zx}XLO$Kp5|wd3^|j{7ksmgwgWToKj2I`TLU%t6{vysqIPr|)&8c{|B4zn`9L@BL2alas{OmDThzfz zMgt8-br^?Q@eS7wV+qFsoBQYD!9m@G@rJD(8pxvl@dC)wIWhvi6olv@A?f`{RuU&1-ip@~{x}%=@ zNYu%WK;6=4)NB0-YM#ZYjjr~RQO9joaR>`iK56j>sGaG zod;E48Z~hZ)F)_jtcZhAC$kp$pz=Cf$mkZF#$0&Oe1y73$v<+BDi3y}Tol!ADr%=| zPy=p7oyacKksn8md(pg!9?A)*ekn$3gWUg&Wb`!W!R%NL^_nz5?I;A*p@WsXp(Y4N zO*k60;~A)Xx&(9MdMkf{TF6<{c-K(l-NO`m|9>WG_o(q7q89WTy~?BR#o=##kFQ@Kn@9F{q>vmyd)YIG%bE3Y+weyLn@jfxd4lkI>wxQ)qIU=mu{@PAP=g(7;3>)EZzvUv6fykT5)@;2(^kn zn3wn0@_*2-MT=MJ;41Y9X^wJ6?zycLVCg4xrkfMLiqWk@j9EfsEeg zUr?`K$_ehu@}ur;S=3G%q8`@Pr~x`-73`0ia191xtho=>?jYvDuPuHbHO|kzGWY)l z8BOpKb;Kzrx*rtTP`BnS)C9Fr_07=t@L@8_15ig9Y4P!>c5~4mSD9;38`^*x_b6s! ze&;JPTIo$Jh!4$lllTUsTn_W&K-7_bg2{0e=EIG+2~VS*naIg@D^LquXyxTrUXNvo zZ$)o&GPlT-!_rgSThJMGB114GjzyioRMbKjqKS;fYYIn`NYd*jf#Ghhu{0sFA z1W$EOtn5_ozdkB!6G(+0pc?c*Eg&2<@krEDJQY*pVbp|QVM)AX@qbW1!lj<({sK}4 zbwU+TC)?7D!ZeiUPviV`l&cA7XPZzD;Xc&F=TRMRqjusq-TmN5h1z*xRDD%c`;M3v z`=Z(pL)`+eIRUlsX_yA*s6zL21!@7iFg1Q*<+G>--9UX}Jw9M!J{ zs(nY)iAJC%9A)JRsCF|^8}Ke7la|a@)Kk6R8k{k&p%(Z9=D^=kCzk#bcLCW@A1uKb zfbXFuZj73^H5SKCI1?wKHjsk?<9eM!WVC}Ss1gR(C z7EeGA<=;^o%JHeYkYG$txeRLIHO>0y`~Gi2MjssQPy>F5x@QsQaMVLI2K9O^MZL#y z=4I5E)6b~>{IT*F$(x`sbumaY`QW$~daXIR>{RZ`x{DNLhoH@qrSP-@IqUakCb@bJ+D!z*v zXbkEocc5P1y{Ls>wDL_XO8Et9{QPs=Pr|aOx1s|2#+}3c*Ff)DMF?u*)|eSXF)K!4 zCY*vAa51XgX4H=Np}q?aqQ-lUn&>5F!N9rhvy>B64n{4Y>RirWM_7-5I)tF_sWm&H zChCdWX(VRFPf)jJHEP2Br~!{!{Tb9a-(xy_h?@8h)LWExo@=m|Oc4S#Fe7$1qpZOM z^blW)dWsKWSv-q6kymDh`R>USKs|KjQQxXfQ5)-p8b92OMxCU07MWl&D^d6Uqd(91M@rQ$y6Y4 z2Q_f!MedK=!I+8h~;rPs@*rJ6S|9~@CACc zlHeuoQI|%2!c|92)WPhET2L?40tcf88ikrD2K5>)L_K87Q2n=~-kv?Edw&cy&Q;Wg z?=Io|^%Op`ikGMbC12_`%zzpomz9g4o{4f+u7x`Kh8Ayu>faePZZFi%hgo?dYGL!t zrAs+~HC#g=7w*Emcn<61BMidw%iIMuMJ=EmYDZzH1q?@>Of+i3X_y-qp>Dx$%!Y}m z`Cg&grSmR#SDXj6)553$D_Fc5>WJ%M5p0Ay>OrWZ9D>@h#?g^Z#!5H`wbL&xegUm^lWu;|Zvbu6B>C7Um(|4z=Pab0+E`-GsV_r?E1Aiv=;m8uuhB zpdPk5SPWZY0sIJy;u7?BB6FOKEV$P9j~Grg=BNA+t6=JN{M3SVu`Vu0Ju5$83Cy*g z&kU@O!!a7e@K02ImksXk7gMna zG8xGPqjp*u^+{C^(_;tJz`arJqRm;T9j`(?8(UB(lIk<}Br~BVEQI=GEsM#qk=X*d zRs8!8869~9>Y``Eufy+1hv3c7>b=x8#|1; zWmhnT-v94>8J}PA@VVa5!qkldvStvhqRHJwAmR@GfeACzuig_qYp4XBNiF#A~8X zHWHKJY}7qpfZFI<^wuV`gNz1xjB5BN>R$bWI^yJe-J{Kp+Ce_lLQ7yjtb}@vSD+Sl z0yWNcEQhx(o?)MR^aW81DY}pISEdqyT381)aWuY-ldu9F#RB*YdCr{dpS!=ooHid} zS>hS@yYKZoSeSAf)HowCC+@~-cn))8syNO+8=2rZw_`OdL%F$?$DkgT7|i3x(P1IV zyAQZK{1$Z*cTfX9K;5FigYL;?!z`2wqi*rrsPP)2PN=Duj2@y^)*uXZgi)x0CSkB9 z#$WN^A$EdWk8p%|@~HbsdFcziOPta%{`Le{qQ{@pJkCEtP%e6sPgBK_p`74Tyb1L8 z{y=6kfmL7f_cH{VeZ@BvXx3zyjrvWIrmG!gF3PPe4r<`1SPWmHCN6x@{cfm^YB$f^h-!ZTHPJ~d zif7H|X5LGjzi%gGn$aKxE8=3*Le66ryos6dG3LO4%kBh0s2vtYwXbFICRT1^b~D4x zQJ9|gQ!aD=^#QYhfF{_0D#xLA__g^x>XYmRs$KRgZhdjgM7g$=TbjMh(HKO1jFq>W zC(IjHIDa*GVu5s5-5nJ|Eu;o&XCY<>)QR=9`T?kgjkfXkPHP zY;!s4b6^XW#8X%hf5S4E{X6%4eiyZnwx|g^nSD_wGtBDeS$rw#q1$AB?yKkipY~;V z+Rdw|0dJumwuj~`tIv4D-ANAA4udcU7Di266SLxnsGSeS+&IjY&E<>H@F4V@3nwM{J{}o6ipaK3u4G?(SZJ5T) zhi?%tk2=!c=2+Ci7ojHHWaT5MaW0tGP~+c5E#$t%pWo*E6?jQNKN6+7<95u7MJNZM z>g$@#(f9hG7T(|DUURy+&|HUFNUW9jqZV??;@>27ylxHRt>P(ar+=b4WWMVTlm}HV zgj!f>)CAS6T*qvN`ef{gdKd?wz5~W#1Kfn_=Y2{>JNXkeaN6(P9cD*OP{hg=QT6q( z6E;OX)tgZRowWM1s1v$|>Yr%!&#eA$%tt)g55DpE{f~?~ltJyd7V2KMLk&CtHBgi} z*8Id=jM~5kGtT1YP~+XO@-x%#M|ZsRn4IU&3G!w5kU_mp#Ze2YV(|v31vJ9~*wGwq zE=Mi&sCn8vkLq{T{L$i%QRBZ*ocWy;_uPhA%>r16co}>f-?#X5)IGXVg(YGgBnEN1PkA(~7A6 z?^wBkm0M$e;vFqM67?)iPT>55$;>8D3iqNGbPx5uXG?Tn!w}3)ITCeGr=p&b)u;u1 zZk{pkU=Z=AsEN|wcNdt`ENoV|&-o{(L2Ux^JyeILR&IxyptHpXqjoyd%9G96sQydL z)#fHt`<+-651}@kVDU8G2evZQQ5Qzt^UBx~!*K~7#qHSnp?d=9e{%a3MNM4E%GIsh z(8|qGC-xy~BSS1c+4RmOqXAZ7UfhU!uTP=A)qX%N>@U<#Qa^G(DRZF81+fa2MYZdN z8gH062GdfWfLh3GOof|~c3x+PsF#%onj=xSXtI@8pmw|! z)qbye$UK21h@Zif_yWE9l5n25UlN&79gCtSE@M_V8={`xHdg)!^-xVheO@fG_yN>2 z^d$!1J=6)^M-TcxbzkQ~PdWcI1ga6xgzupSY=!F3)8f&nFO3`%yc;in;|qx_X_5R`CRNZ(pN!l=2U^LlCM%F;u*)S=Vf1_C_sWq&dyv zi%}=M4RhjIGXXQ}{r|@Tng4VrENNCnP5dtEXhTptABI}!a?FAot$fftWqyNdf5Qy; z%l#|flBl<+1L{9;bHfzrHm2MRKaq54-weSp=la&I#nb@W3~ z^UXAuyyX1VaU%g8=?+wRzj@Rep0@H8)BYqwkg)vt_|YoW$#f*Pk6Y9Y~9 zKgpcqwaiLXhiz8=3f1A7m2aCrpJ&L}#qxl9lhGZb_o~#QfV#=J@$`mH{qJ|{RLn=kd@Jw4Qk0KjL40iWS^V4qE1)Kze7e!g2-A64HK_1gAAwOfW7XET;ye&-Mw zHGE(V9-}6FW$`QlZhdal#05|Tl|-FbRV%kcjne})K|hO+F{h#0FF>8pUiAIcvxvz%E2Q`4>?YT#C=g?2Xwn8Q#H<5<)}=LY(@@Bb14 zns_y87wUA+`j$_RE7T^IT!Cu0!|L~2{FHeO^Dw`YNJjTOFqLZ# zRL2r#CDcT-`^3ra5lG_wXgY zjrI5k9=(>MQJ>v0SPkQ_D*lf8&M2GCwF&B>>xJ!bE{?;Wu@VkS?=E01YTR?^d;jlQ z;2ChY$4{&Bro$0B7S*s9QKDqw6el4f>88bz(!=EsE+;2Q@%Tv$NUH9FDrz6HpUQ$8em7Cop+tKPL*$pl)TIEPlRU zb~|U`{hvr+6oG~qkkvi<5Y$ml#1Pzt?a?or`x*ZMYQlM_4J^g1xC1lc3G*6i;SW&l zez$U}>~8<;UNX8Dc~M7P(aIsHqwa?K%$|gL2G*hO`8m{1ADYkbBg(H)3mcKceJG<* z?Pj5VB3f+a&rt2W2gvA%kD-p{lKB|*l&0ff8?~@lph4Q)Ye+ev1xg+XJYAmkC)u>Ov z2Kn8GvK#859fLZF)u>OrJy-(oSUk1I&-c4!UJviTp5~SW)L{VXZHPv7oQGQ2Vbp@o zAP=~62{qBf0`5*S6m(B23u>IaW;s;m3FzS&VhyLD7P1ia z7Zlr(d3{%2&hDze`yV1KA~;lM$@LlemDr6GM$!g~({3U4HL*2G-wRhsx*8GR%=`|Y zE&AV%`>x>xkC0~2*msfNLHd@o#bS>zCFNa7ZLF#8ZPGQ3dR@EK*UkEVuIRvA$%xHC zUHbm<{f7l7ZLmPfRTboFMk9U%^j%B%b1CUKod=TMCh5{~>RN2&YmCEBYrek;dt!Z) zTbN&GHp5@{##`o!BZXgiXw$2NEo z%1dZ7T5U+v$m`O>sjDRQ0mOEsbj!}9n4|Twc*oH+h$=oC9e!2e)eN+X^bKhc z<;tX0*5*7hzS?}(3H*q75b0C;)^o#79`f%JD~(@T>@QPa((41b8J+38hDQFRD>N)h zc_aBFScmkDb;wODmb8MTD?Mp|dH5`*R9^-lN?`(%YoV^lypX ziF>_t7)YitfwIkj`U;d=ztONO`EJzR zCH|b$h_-#Leh=mMsoO)!N%}+|9UqhVg$A>+E{%1KCVvFqqyB3JxLS~^Qy)&*F%%)`>PUJ(dQRJ~sC)DBQ1*)4q!g6vQf%=!ry}vYq@=4VF@6B}&((#x+VnY0EaHv08dRY!jJmVr$61e1Yjc74SYM2< z=z|n=&7iW1?=vwuk=lg^U=i}cjGt|T8#{5okid0js+)oH6w)F{N8kS<${-*Nbx z5q_g4&9vA%^ebWgU*dR@hZvXFiK9_v(gZr_n(E^0B9@NWVH#f{pLCric9gZ(YahNQR1m&6IA;-m-Gj$c8X)%1NzeFtJGC|B2KVhsv6Gy>N&k{*+1)+vWAsyN13 zOh1z8%3$S>87S%cj{H&Db|Bp%pNq7P{3$F=dXM}^q;mAlgp-rLPB+8Gi(qz1IW#d2Bn=K*qt|r0 zr50ReX_S$&uBsS9>TBg-+Lt8X3=5M6kb07Iy=`q4<95dQocK{(Li&b$MXOiaAN8F$ zTsf}(RJ@?kz&9GWHLTt;`>C5xI>tb0uodl(k#s#IH7E9fx(4J^kWWqi4z|R%a2!6c z_7Svcr_aPPRHUckHWkANt|IRtf1LQ6*9Zzf68{^klRh8~rToqt?Hbc}3TX`S9{4SF zvDlKf4e=!=UHd3!OuGL)>F@!8nRJLIKh6fdM67dC5B!c4M!l{=q$1>x;bT%Q`usq; zPM@SJ>l^te1ozmu{GElycT;z=@{sn}ta>YShaMw@6b=AW?7-Rr;qCB4X zeA3^YL;omh75e>a7{NV*CUJVScs79IYo z$oChMD)zNO{Y`7)Y@>Zf3cc|M%ub)en9Nfq)pOw zSw-P_6&;L#|e4oND8Vw{rm^9tjg&!knT$lVY>Nk)kl0Qg&EZ(DhlllPA zx>&p*@q*<0QTKv)e#(a^mmpQMf&M06pLj_t|3bS1#M1J@?$`jfgb*n;vwTupf!)}q}qYxA{D zxQ_C7)F)lu3*_RdRQ)oGM$n)Rfftn9k}6PMVFS&_8`Nzl9#8y78>|`SV${{b52;T} zyb<{T@=eLdQr8315Ysh;bV=ub+E-+UH1s1SUHNESgK}vC&GDQq;Dln-eUD>_Jtg0Y zr0X;DE$TK|d&QR#pFmoyf?dNXj|wQpDrtC!4xOmTMQ}PvS0Q4DaV9CPpYMlda&2k- z2Q9i%JAksT9F#j#{>m--{_IICExBH#L!{jFYwoMzy`t_0<(c#;Oui~M#6G0P#C82c zI_b;ts~e6Y?#)If8;yP?owCaJO)-Y}1sZN9--i4bHdu9>!6dqF5Z5&V+ma3tUr&7= zi`S-niZqefDPjw-4`~N~o5?@U^2KZor;uMv(zT5am5KKt-wppHUW$~Rd`e#nzKn4) z=?rb>;7`=w!34}p(v^$4W;XsKH{`50ulVz_cC&`PXs9cO#=VHWB0q#!f^~L?Z6LN+ zlabC_`+S(f@|kH9N&Cu_zr@|7->iRsYoj@Ijir3e%OCqFL}O7Jo*=EY4zn3-5p@Tw z+?)Jh+Rd|aamuY}Q=V8EQqon7`WP$Rp}r~UJ!0?DCK|sYC6W>-=Tp1J6oN4eCS9G# zf9@YUv1JYaKo3_;Xq#+ll}Wl{3brW}bD(X?|3hu#Hu?WAwFBD}QEic=!DNkY5w%jM zelwDkk9pLl&cEm0c%Z<)1GGr_pXRSy<%^lvzOZJ{vj4kcNBfHZUHDgxE%)EeZI%BH zR_W06Ut_rYf8DB3?5S4m{9@7`%o-cgwyuBd;tosFW#|<)Fv`=YzEek;Mm=IH_bTa^ zu1VMkr_P{3t-`uTghj=E*gJQ!lwnbko`InQ!eS!^{NtA_w0~GcR7|(<$dEn*d-NID z%Tu2}T8H-U=ZPHBy?a<>WX~b}`;XxNEvZ}EF05Yo08h`*KK;Xb{Ey(9?PD*6FY}8X zKDb&y{)j>05us6i!Ut+?^}_$VXlvRYRW+b$|1e(*cU({Cz#g8c-eI1B;XT4U-NOe) zh4vX3=?RUDWa&=*uz@3-q(#O)7&gW~T~f(YXGm1?k0h6BZE>9#P6OsDD^!q)pU4$`d&vGAe9VI|9y7}? zw$t2m0U0A`gW`6V;6MlfY)f3`e;rae5q zdlTDn>7vZ3Bf^IavfK8{_HuqHle)zQ?JVinfP3P&TmJvwl6C0AsNqqbZs9`)_K2V*lZvlw(UB&gK_;YJVxe*t7>d{xOI5r;M$0q;uNB z@%!f_&e_1dEoRSMap!-BVwat%>{r6#Jd(wDBq6D3{Pu|n^Cl_iJ`=@bFP;rbmiGSc zkMHeYAOGo$_^tC}A6?w-mpp#f)caf4#mu=J+8}OL{I*4&2eYT}f1ZTNpT6jLuZ&-_^Zu^2R^fTG#`?tmc6oze?5e970}A`PX(M__8}(puleXU9 zK9x;d7vy^`YH-CTI8U}UZKi#fl3#nhHh38+PH`f@O{LbxUu|M5=8JH$<(Z~OJg6}@) znX+i&!C8rMOCHQwcz^qLEn)lK`(x)M#>VOY;+7|FTbcOj#Q0eYV&8q5J|G}5ZdYvc z=jj46&@5qfbi(qr>?(1^*w~JLc>Q8u|5+?BL&B07@l#jEFWit2z5f3AgRzBQ&kxAt zjx}dP!sNZ~ZJ58`&u@9=#JI_co43c$I(RQ`jeAS>w)gidooVloEPnO;Qn4|&!`{7F z`6uyKZ*IwXv+)01y_4|S%7i6T{`Y;|`*k+I=Kh%z#%@HxDcwknkiO_tYSNv8G?#RvfYKl#X^?_S z34#*$>wSGc{BG|1`}iN9=X3g;Z)~9d-<~50c5X}Hy_r10EQjk{pyQ;+l^Go;b&%ua zEu&J$nONU(as)WeBy3K+rJ>_|fr%SA&Jdi3X+4e;+1PQK&@O)y$4Npwu9@RZrhX=! zMq-8*j<c<0VtP!1Im}qIJZb}VFa@^2NalBXkx7VSticr20CUWhs193D zCvpUn;z?A$YgT>>by5#7HNHfRldQAjgkT0#`y7}I3!pcWOgS=Id1JFJYDYa#0}jPp zI0?05ANIzhR$r_Ow~x3I=EQ241N)*DJ`aO%12#n;>Xsz#%K2-8GF=@f0&AjH+S=kS zs3RL}@i^4V=b)Z}HK-jPKrQT~)&Gpzz+=?2;B<5QMWHsF2i3k@H_l%VLqn?=fEsu* zYNBPR9c)E)IBNA*Py;`<^3d+?1~Q=97eL*Dil}i~qT2UFEqFZYA&>Wx(Ms21LfnN~ z;UU!PcnNi{pP?2O(8F=kVOmtXqNoXKpayJ-8n3U#qfiT)WAQqR524-;??p0c$XvAs z&oGKOq^CPU4642)YJvt<-W7FEhhrifgSwTUV`7|-F}NB-@dW0>OBjL)KJ}l3*GWo7 zJ4lDYm>0va2qwYusDbLBI`%_7E5k7Wr=j}IM4iw)RKLv@@58Lb7qBS4#3Go#m*bSu z``@07cDx96Bs)Y3<( z8fU1~`#+kDI!?k2I0LogO{jZ(0X5L?sDb}N4e$~*;5*a_rtagOOir_uSirYFU*Mp z`f~o-`C$g2_uLPT6`XLi*BJ_qqnGqr-^eHoF6q#2@J<_W<4xG+!l3G(^37` zU{X9B$NB4s&rzU`cTrFE3)D(qqwZZ`Klh=EKuwepbpqK@{R&xmdCWpw-O77n1o1G` zgi|pEE<*L&;!0>>NnbA?+h}!=krhvSD?O#o3RLy=#G~ZIRUSel8lZx8*1PpW*Lkou8Qi|6}6+jm;#4mDx86OJJz5!umRP6 zx5eM0#y^WX@|&m=_zOez{=Xrkhbz%%?#i>G7Lp$|URoz!vj5~|&A*bE<{PNdRc&R;)d8jz`gy;1R2EQe=M9g_`l--@)Di#QYN1gfJZ zY>s*e+hQ8*i`waARR5Xg0@T~F64n3M5YAsu_hkz7Q|$p}$Kavv?{fLD5OHnPQF>8F zz7Eyz0BYj%r~z-GZoxA%V3^x30=1wRRDE95+f;fO=dX&|6lf=nQ7dkRD(__FpQ1j^ z{jGi`>K-nzcnzxm7F55T79X_wW2jql7Gv=`>Q+X0hr0t6LG7R%YKQeux1=LB!LM*N zzQsMbYJ~gYYR3k&kb$UYWDIH{Q*C3jP&;0P8h1VF#15j`d(V;4!*Ct7@&}j}pJFOZ zGSXdGR@A*Mf&o|ywWGSIhqWbYoK9FA`=KUYhk7`7n+H(szQgo-|Id?AgGZ=={&a+mn@|IP zj~eJ4YNfYOPwf*kax{|@7spIE0ClvpP)ENO_2-Gr_%)uv3^-(rd%|;33tXz0`JL5P zun`Loe}i@LHWtL#vF@$tggTL7sD(~Moxlv#LYE>(>TE>49VbxjZkqSZ$EaKV3cY%t zJ>%SmArD3o7en2O`c~c&HF0OugmI{+c$C#|M@@7H^WbqSe}P$vJ>%U!A7n$FOfJlV z)y8xGmFYo&CYXxBI3Km6WvGX66Kdk4sP-38J9vrd(3#-wI6bPq0IGch)ID#939%Oj z;XrfP1kPV8A5DQ4Fa>ol=b#p_4)vPsviLjHg3hBR{vFjXXren|1ZF3WM)j+TYTp2L zqTMhN_Op1Hmy8;YN8Pg-s1>h7J%n4V{#)}jYJpcUHU5dg7&OUUKoZPMoDKuA42EEN zOoTNt7dFCa=p8{uJ4ia&9XJZLgZ!ukHO6e%75%peLy703UawWCg>J)?c+ARgU^MZc zs0}5Z;w~f|>eggK7VdRo-HcNj6H`$E6JQz^$-m}y;fhMUf-?e35+DZ zjq3jjwUO}8-CLFgRbLiUF~8H$3c8zK)Xu)d5L}5`&_>j~^r4ROIBK9DExwOQh@YU^ zzs15B_ys?3uoxD?4yf037G~A^zm<$8yoBm_7q#Pu=pPV6iQi%g44LW30_m~*3TYP^S=dYuDZ4HB_yGI*lrb11S3AMw#sD)HT-HKMI ziTa^7GQ{e~p~jt$VYmXdfGw!E=7@P?I_ICAg69+@!}K#;^Pw7+#b|7TN%1o*fa5U& zx0~N%IPq1~ci{o*r)cm@_XMI*qT1K+l8GVH0(H+7%WbdR>8Cftph@LSZqI){3wZeRr7 zL$!Z}YVY~dy%nLTr#dsLeOXj}HH+(`Zc$6r&O2jaz5fHrXyCP&6Zc>yyp1~Y1hd=$ zQ=sm328&}*3(SwvSPHeUmZ%f#fNI~(%KM<^8D!;?F_GT?8DuokeAIwzQ7hes+Q~sw z$Kx1@*HHsMF<+wkzr$!uI@>++0;q+w#zNQu)ovE1!j+g;@Bbb$I;#7qqkn?>vb{t9 zj_0_>pca$|wZNj73d^A;YKpqItx*qKdsP2^sCzvG^$bivjWZv;+VM&|b0O&(7PZpKsC(B4wSX=b_e4$PMYS7m@hsFv7NRCxY4JW(|0AfA@E*5{ ztEhYT8)~A*7=?ig-JNDcO_b9tfZA~})LT&jwScCmaXMRle=8q}dW)u^#$ARf^#1Q9 zqaB|$uc20W7j;V>qK^0_>fU=6xp87teI%-XX4I|9i`qb8)Iutw`ZvTJ*b1X@tY7Z` zDl$5vqnI9lMy>F*nR>DNFcwDLvwBz*+o68hoP*l=Zq$+ffI0CBX2yUe?!+NlV_51E@}YGde1_orSn)Wg^Z>*H6r5noyTnpN&=dJuI|S5Xh!bJUT) zL7i0M)$Who^r&Z~Aga7F>RD*An){!TOjoOzfEr*f>fWwKJ!C%A1P3qy9z#6~Cvg;B z!spoRE63qfojPmWiT}cE!~tvhLks4_^0)x=;f=MNzwTAyula){#$pQ`iY4$824VO* zcfgdWXC)V=#tNuUa0}Fe`=XwK5ttaKq82e+}zoyZK-QO?7}xDM0e4h+U~=1-_w^)u?k|HVw`O}oK;Xi8!<1szcf z7>A*_2sO|e)Xp}cCOUvxzzOq5)B>+zJG_J1SXCYzoj_|;`}SscWPGm^M<$Gl;n)eM zU>V0jG+3+*eK#Q;xUPe7k$+x-pI3Gq5SH%3-%;E{Cg)YSO z9zI;CdG>F2PvW=joWFK@p8^f|6q8}t4)jD1dBlQIja(nP= zq{_*6h&O=xhq#V--M73_#7)2Bc_40Z)LlrzWA1{ZP`5S{>RHHxT6nCNOiD6U%vP9* zIL;c(M0Hq+xp4#PUjBr7`V)Wee&chYPOu(EV-HmO&r$7mU@^Ri{GXHL2fnK4y+}q! zHR-tfC7X|W9k-YVQ1z!QzKMG1{xDye!6)2@E(NMS%FJQLn&nXsXB}kYUZ=4Y^gule zai{@?phe9nBJzx~z#f#=D6nTF`0Kv+*Zt!T)&41d>ViqdP$)YNeSiE@Rd}z28kS55{2@ zT#Whg5GKRNs0DZ~xZ@=Tz?CcKE+>EEb{!!FuJk@j9EGa23EJg9-oqZZW2>|n;3qcDv6>E>c9UyoYA z9`lHK#=K(QLM`M^4A=Yrw?D&Ee97*;8Dqv`IXYCrlsFDGzyi#T%TW`bvhvHQb~nwx zQ70FA+1+4jGq=?HUxthZsD~Q3g*9kr_QztBPe2{ze)BTwVSS96Q2%w3;#8<{vYYu( zFQr60AqE`hC$b@1WtLTo|iTk4J=bEcf6KqA@s>4=((frMPWWGi%B;<-aPBPR& zGN8(HT;cpRL4Ipc+A6AH8p`XUI&`)2IEx3P`i-&j=@!p1SE4@QTd*o#K~0$Xs=M%< zsBueQ<@~jy$`ok8#;CZxHHgF3#KSNOuc8KejvC-Ks(tV^w|^>BeO6R`Zq!>4YjHzV z`_`xpe(EKodp91{aUp6UE6mO2KJz&0=r5ZOto#jXz|iY%|Ey*~vkYqd8fJaeTjXs< zMl0%U6$4QV7>@e#O*YqC{b}YMI~#9%?ni(A~);-RRWu1Af(8?~`R=zsrjS;aHd(f*6t zS=i5RhbUB>+2Z_|iMXhhH%8s-_Nbp~JuxrNKrLuL>h*nu*)jVs?r&PPF^!jkj%4%< z3`Z>>-dt_&!gQ3ML``%bwVU`#MNK>f^?J=k?f9UT-!Y$}o`HABJ$Ax>bN?QpIxZw$fSa(yE$+Xb+WWWMfjzg~ ziIbuxN@;Nxi*upc7eno&u9depd!rUU3^U*ujKXD@8uy|Wb^+Dy_HE8z6FsCrU%;2B zIPi}9S24*@9jc)^G&9?v+I2=Ps2}R6KezhX<|0(PmF5=AM7$r>{-)O|?xQApZU)_T zCrpX@xt<==Vm?&Isu+Ov%|@tas3`{Frz{!?S#R^ zgHRKCaXHRIP4Llux4wbd+-z%h!Sb~0g*9KB&RsCjb!?w&|#WFG$eUn{6iLrv}s1-KB%-GG! zr=T99xv1ZCe5jMzfzfylQ{hugj3E!*2~wiQ%YtfO)XM8(M&ec&qW6C&86E97REGtq zhiapF%*ubk5XzsKZ%`+a@DF$ANl`mZjT$Eg^-LAF@~UPdt8a&1HRx{@W6c?ulk&wD zA49G90{Rz#I_fu8AN;2~aSBwsXp28aEu@ykZOopihkEdzoWC-YtzrSH!y3$udr%9! zi=p@ebuym6+)rv0P9v_5I>BF2U&?<_pWYOY+_(}JA#Q8&m#FreA94OU$Q-qbzff`L zV|U`hsDUb=I`l(LFbR|3D%365YVNY~gQ$}_h1$?%D}R7$|Bscw^;#z4iQO}^IEK@( zhS|c(yQ7YHIO-%8n%k`Yw8g)o7Wl83@Tog-B7dREZU>}3u@oychO zYs^P{9%ImX<}N53CMT?f+F?`k6VyUFnQ^F}hF;9B_kTMXb+~~V;5MqmKd9Fx!QbwY zXGEPyDO7z8v#Hq;HBMi1kd=E;<9==~L5;K3U(fwN?9cGu>7WLHlJ z&wyG$F|#7-DC?p6w=mmTeOHSIpe7z>evba%{})<=O{jskqdq7Ht^6`-!kgwp)Q(=F z22A|IJ)v}{xHxK|RZ;EQqCRN-P~*%(JwwaU|MUM48Ff5u4Q`^I@`tE_68_`vEE+XH z4l~v)k7{4X;_j%E_{`!F<`hgx`CN-P{=@y(#Cs@^Cr~TBW)0t93gQ$m-G?v-Y5{do z6ST6pgT;MNC-|8;%A8`(!Zg$`L5;KXCHG$g9ibo@oM3@FJ8h|Zh11) z08yx;%wcgA%uC!5v*1XpUx6C$FlxS&=8s;hxQc11xNQ}0Q701guiGJ|nI1K9R*MUw z1}<*p<;^-~OS3EL=m(%SI0eWq z#G%GnWc4di6K=8cW9Dhp#21lqyw1;LbcBDn1y17E?m%f!6J$b7SlIj+)xH+$gnF1m zQ4>x!BuY ziduL_OoN|cSDbF~E7S=je~ZlTq$i_+vZEfde5iqnp(d(``qVZ>4LHo|XPL{*P3B(I zct2Qt7WF&f4OIVIR{t8kY8d#=-Ek=D1fo#i_-v?!G_?A*r~&$zlTbTZj(Tm^DDs$Ukfw3RnEqDHD18e?wXdXA*Zq-Lltyy*&9LZb25*Q5Qv>z{jWw zYgv6Ov$vIxws5Y+i1jk~3tdZEgmj^Hd z@pT-J|6(;9#lNA_(H}>>t_i|CPAx2q&CrXb@h8-TkxAW+ro%|${OFA!Q<02pj9PgY z)DiW!cmisGFH!e+0qTgiSbQ9H)W2Xx3<`Ijff&?1uZ23%ojp+HaN zTWb)W%;WzJC@rdEK~%d^s9RDV^_15)d!ZiEDX4{Q#8h|;)&C}HWB1MHW7FSPhLs{dt+AEWxcK`lHi(p^AmR6B2OG9Qtt zhv1G zsEyiaYt(pMFi`J*FEV-<;!q98qZTq7^&882WM2QpbLA{1r!VH^0Dfm6E>8X{{FQh! z?$@etG*i+8O7T$FRsEbsq;=H(AAE#hV+#514-93(tSGX3f7=p_g(nG#3!2c z{<==P5u`4pbCefXJ(ZnEpU`(8@pI}*lD~zUh;Qrue|YVnkk7vV`ksI8C(jGv|2Nf} z=vbTjX(T>?&RAQ}9@hq z#&fW_^{7n#6H<)T>7IT={sCzr=_lf0q{9q&n)nFT!UOn}r0a++_6GUPl$9l(p|bb; zA47)$YH1e_zjK(B%?4OUUKd|j|Mh_K|H$RT5ezie+6|??9Pu}#siaz@tkjJo9U%Wb z^_j>or|%~6@8|b_MBJhIwTO(K&}B3nLnmEL@FDJ`EH(L~q_m`c#HScY-}6Vv7u;z} zSv*dl{%7(R$?M99dno(h@;XbcQiXaib?JXdU?{ewE)DtD{!;gEJV%o6%OLkDACJdr z^BZX?vEFZ8{K?%JOj%XzNSi9ueW41jOq6G)>=@S4&&O62cA~)+3O*sQa~6`i5D%0yxVWS0kUr>f@|T-`#c}%vat^qrWLwi~~sTuXf}Q6RaieC7+&j z!8(3VURNDb1M+)FXQ(Sm`hhf+w3l7Cz@DV{*DK4vFwdg*0kzA>{AjIS;dD|z2IxT2 zb(aZJk-8C|w9W^K%TPDW;x)t-{H_1*KkCr_9Q8X$!>E6MRkm?keDMA{`6>L7M!PVd z4SIvP9K9wIFR|XYY@kfEOHEk_enMG(+Wbx&M2aJxW@9|Iehy{nDBDeZ)9QH5Yt&Eb z9Y;8t>QJimi|a%30~t01lc27D$oHn*#}=zh|GOdw@Y4^<8mWTo0(Gmg41M~M*EJJA z!}-=;KaGzE@C}_Ulmb`v~PNF=JSpWMXJL!~@bdH_3G@kUBvW=AeOa5#6r1Q6+4dwh6Zx`=CT`ETt#*z||e}AHnjtl>c7^`x0Lc1P+m>mBGo zK|z8^G+avl4QVa$3ha+F>2%fxX=n0FmH$_JnmGJLh%=XX5gxYsO7wewy|m2lAC$eX z^S^^h=~RtW!x|*Qla*ILgeA4&R%I45nce=uMa`I*$6pieiFu6iy`bzD#V zW^11dzoP9Iq$&Iv=6_dF3Up<1@&9ur|13-Tmbz)AJhbmZy!wNF$tdqgJkH;PuLk)u zOi~<=SXmw76XXk0w#DWdK;GNZf?n3J9)%moC#L*!(m&+a<2we(g{4qeL!7TcDSLk< zBmRnjKd(3=t=+fyjQBj|CCO*7_F>r7fBx@#<5U_PA_Y=;oj5Bg$~v^B{51p3MO_of zZzM&Nt`aYyT~dDyzhh9AfOd7Uy0y0|KMQQk4*D7RC5?2&)9@lGmm50!D6dBTs}BaK zLdQ>u>yy0X|D(+|YcJ~Az?H462leMD3nTuK{4lF8ZTf$Q`;3YX3^;_s?@0MbZAjhe zw1w1@d<1DX?R2Fhud57wz9UT}=^8}&DbidwbYjT&A-~i5^`PA?t6Mf!t}*DBAAB1f>0>Hncy4 zLn$AHy6#byF@XKAB(sf*HKYPm6lTKIr1j({;FqNGv?)xzuH!DwHc*48$rr@{Tfk`gza;6(Px2-sC`ILLf+9A+Lj0VRmb9Jv?bw+5IkY>2 zQKSW=Ur5)eOGKL*Ho>=)>8eEh3w1Y1HEhCpy2r!4;i?fQ~0PuuC_3sAQl^_5*j`kd5-_#o+&e?-nc-k+g@ zj-Ogyq$6%kLtVen@x!Y)<@KoVL;8%=lgc92wjXVOA_ZBW9F(6SMXN1dqHUYE>-f%Q46|5aDa$`1znzqmbdL1?N@khqOs4AxQ~3?_4Y2qMabJ=*)(W=Z zpQK(aq6L0ri+M%9E)8pvRxv<0>dHe(NJ>e4f7%9NR_ebef0}$KNmnBrN7+i!L0kM1 z452PFe~0NDBIrh=@z$X^aaGbK;#PDzMyf>P1;mN41+lI{v^hYEa6|w9kljP#x1^Jl z#nNsh`N`Hk4}MFmE2rA|=i~h6QBaUHo5B+mo+nLFD=r`T9@vz+SWeH&S-b z+I&ozt^j;S-BrA8WzB6dLx?{n-wZR-wh*blI(=~Ur_NiEf)S*0*0DYl{rf?qTsH6< zd`_EP*v;zO;$`BmDX(n(-*;WjIO$2pNvl7oYfSrnlbAA?W(snbEvOG**wxU(mT>Js@GB94BubBT1LNVkD|?d(j?0E;>VhQESaiw z%1wj9R4m8K*od+w)csA|3jZSM3dKvLNH=s^;(6l5w7)`IT~&x%l5|}lZKeDVQX1M6 z!`Gw{q=vPvQ4SgnreH7)4v|Vz{+P6uRFQZVX*fyO1~WhHBdIUx7CTQI-(L+s3h>=% z-pUhS^Kg>*rcKht-#i!=|5cOJ@edD$#XoD3F}~oT==kPMbHoogl*ez-cbR;(TUH2& z&)F=4zfoMXJn=OS|NotW;vXMM6yLmgW!3q{wO$?O`>01Qk8fblR6)MEegE^sf7P#} zuX+E4p1=;BditIWC>!XjKe%XMK(E2R{li8C1lOumzFu@YUpMbBL7v{es^cCd@Ku>S zEuk;b%wmDQ>a&&v`aEv{3C{@r?YaT;{`Up~5j@93e!quUMW-?dY({@psX z>))++FW>C>=K_607Ox77FSLAW{M+SGzQ@alC-;rm#|7q;`+4`}p!fnU z68UZ)&g(xF-?s;oczogCWe)J2Ih4}lD{=IbB)+}pV?Dlf7oviE6|Q{ai4VEf$hZ31 zYEST;X|wNcpXjT3BO=f@=9lz=!FQ+3ytikO@AR#_0m0G!J^Aw9y%`jIe~0h>zV*J4 zKi(wprFqgOk#EVXut1;hbx5G^\n" "Language-Team: JumpServer team\n" @@ -93,13 +93,13 @@ msgstr "动作" #: acls/models/login_acl.py:28 acls/models/login_asset_acl.py:20 #: acls/serializers/login_acl.py:33 assets/models/label.py:15 #: audits/models.py:36 audits/models.py:56 audits/models.py:69 -#: audits/serializers.py:84 authentication/models.py:44 +#: audits/serializers.py:94 authentication/models.py:44 #: authentication/models.py:97 orgs/models.py:18 orgs/models.py:418 #: perms/models/base.py:50 templates/index.html:78 #: terminal/backends/command/models.py:18 #: terminal/backends/command/serializers.py:12 terminal/models/session.py:38 #: tickets/models/comment.py:17 users/models/user.py:184 -#: users/models/user.py:733 users/models/user.py:759 +#: users/models/user.py:743 users/models/user.py:769 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -120,9 +120,9 @@ msgstr "系统用户" #: acls/models/login_asset_acl.py:22 #: applications/serializers/attrs/application_category/remote_app.py:33 #: assets/models/asset.py:355 assets/models/authbook.py:26 -#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:30 -#: assets/serializers/asset_user.py:47 assets/serializers/asset_user.py:84 -#: assets/serializers/system_user.py:192 audits/models.py:38 +#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:34 +#: assets/serializers/asset_user.py:48 assets/serializers/asset_user.py:89 +#: assets/serializers/system_user.py:195 audits/models.py:38 #: perms/models/asset_permission.py:99 templates/index.html:82 #: terminal/backends/command/models.py:19 #: terminal/backends/command/serializers.py:13 terminal/models/session.py:40 @@ -158,13 +158,13 @@ msgstr "" #: acls/serializers/login_acl.py:30 acls/serializers/login_asset_acl.py:31 #: applications/serializers/attrs/application_type/mysql_workbench.py:18 #: assets/models/asset.py:183 assets/models/domain.py:52 -#: assets/serializers/asset_user.py:46 settings/serializers/settings.py:112 +#: assets/serializers/asset_user.py:47 settings/serializers/settings.py:112 #: users/templates/users/_granted_assets.html:26 #: users/templates/users/user_asset_permission.html:156 msgid "IP" msgstr "IP" -#: acls/serializers/login_acl.py:50 +#: acls/serializers/login_acl.py:55 msgid "The user `{}` is not in the current organization: `{}`" msgstr "用户 `{}` 不在当前组织: `{}`" @@ -198,7 +198,7 @@ msgstr "" "10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)" #: acls/serializers/login_asset_acl.py:35 assets/models/asset.py:184 -#: assets/serializers/asset_user.py:45 assets/serializers/gathered_user.py:20 +#: assets/serializers/asset_user.py:46 assets/serializers/gathered_user.py:23 #: settings/serializers/settings.py:111 #: users/templates/users/_granted_assets.html:25 #: users/templates/users/user_asset_permission.html:157 @@ -213,7 +213,7 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. 可选的协议 #: acls/serializers/login_asset_acl.py:55 assets/models/asset.py:187 #: assets/models/domain.py:54 assets/models/user.py:123 -#: terminal/serializers/session.py:29 terminal/serializers/storage.py:69 +#: terminal/serializers/session.py:32 terminal/serializers/storage.py:69 msgid "Protocol" msgstr "协议" @@ -221,12 +221,12 @@ msgstr "协议" msgid "Unsupported protocols: {}" msgstr "不支持的协议: {}" -#: acls/serializers/login_asset_acl.py:94 -#: tickets/serializers/ticket/ticket.py:109 +#: acls/serializers/login_asset_acl.py:98 +#: tickets/serializers/ticket/ticket.py:111 msgid "The organization `{}` does not exist" msgstr "组织 `{}` 不存在" -#: acls/serializers/login_asset_acl.py:99 +#: acls/serializers/login_asset_acl.py:103 msgid "None of the reviewers belong to Organization `{}`" msgstr "所有复核人都不属于组织 `{}`" @@ -315,9 +315,9 @@ msgstr "目标URL" #: applications/serializers/attrs/application_type/custom.py:25 #: applications/serializers/attrs/application_type/mysql_workbench.py:34 #: applications/serializers/attrs/application_type/vmware_client.py:30 -#: assets/models/base.py:252 assets/serializers/asset_user.py:71 +#: assets/models/base.py:252 assets/serializers/asset_user.py:76 #: audits/signals_handler.py:58 authentication/forms.py:22 -#: authentication/templates/authentication/login.html:155 +#: authentication/templates/authentication/login.html:164 #: settings/serializers/settings.py:93 users/forms/profile.py:21 #: users/templates/users/user_otp_check_password.html:13 #: users/templates/users/user_password_update.html:43 @@ -354,10 +354,8 @@ msgid "You can't delete the root node ({})" msgstr "不能删除根节点 ({})" #: assets/api/node.py:75 -#, fuzzy -#| msgid "Deletion failed and the node contains children or assets" msgid "Deletion failed and the node contains assets" -msgstr "删除失败,节点包含子节点或资产" +msgstr "删除失败,节点包含资产" #: assets/backends/db.py:244 msgid "Could not remove asset admin user" @@ -499,7 +497,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:25 #: orgs/models.py:420 perms/models/base.py:56 users/models/group.py:18 -#: users/models/user.py:760 xpack/plugins/cloud/models.py:107 +#: users/models/user.py:770 xpack/plugins/cloud/models.py:107 msgid "Date created" msgstr "创建日期" @@ -571,7 +569,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:745 +#: users/models/user.py:755 msgid "System" msgstr "系统" @@ -676,7 +674,7 @@ msgstr "全称" msgid "Parent key" msgstr "ssh私钥" -#: assets/models/node.py:559 assets/serializers/system_user.py:191 +#: assets/models/node.py:559 assets/serializers/system_user.py:194 #: users/templates/users/user_asset_permission.html:41 #: users/templates/users/user_asset_permission.html:73 #: users/templates/users/user_asset_permission.html:158 @@ -789,7 +787,7 @@ msgstr "网域名称" msgid "Admin user name" msgstr "管理用户名称" -#: assets/serializers/asset.py:71 perms/serializers/asset/permission.py:47 +#: assets/serializers/asset.py:71 perms/serializers/asset/permission.py:49 msgid "Nodes name" msgstr "节点名称" @@ -805,22 +803,22 @@ msgstr "组织名称" msgid "Connectivity" msgstr "连接" -#: assets/serializers/asset_user.py:44 +#: assets/serializers/asset_user.py:45 #: authentication/templates/authentication/_access_key_modal.html:30 #: users/serializers/group.py:37 msgid "ID" msgstr "ID" -#: assets/serializers/asset_user.py:48 +#: assets/serializers/asset_user.py:49 msgid "Backend" msgstr "后端" -#: assets/serializers/asset_user.py:75 users/forms/profile.py:160 +#: assets/serializers/asset_user.py:80 users/forms/profile.py:160 #: users/models/user.py:585 users/templates/users/user_password_update.html:48 msgid "Public key" msgstr "SSH公钥" -#: assets/serializers/asset_user.py:79 users/models/user.py:582 +#: assets/serializers/asset_user.py:84 users/models/user.py:582 msgid "Private key" msgstr "ssh私钥" @@ -840,9 +838,9 @@ msgstr "应用数量" msgid "Gateways count" msgstr "网关数量" -#: assets/serializers/label.py:13 assets/serializers/system_user.py:45 -#: assets/serializers/system_user.py:166 -#: perms/serializers/asset/permission.py:72 +#: assets/serializers/label.py:13 assets/serializers/system_user.py:47 +#: assets/serializers/system_user.py:169 +#: perms/serializers/asset/permission.py:74 msgid "Assets amount" msgstr "资产数量" @@ -863,33 +861,33 @@ msgstr "不能包含: /" msgid "The same level node name cannot be the same" msgstr "同级别节点名字不能重复" -#: assets/serializers/system_user.py:44 assets/serializers/system_user.py:165 -#: perms/serializers/asset/permission.py:73 +#: assets/serializers/system_user.py:46 assets/serializers/system_user.py:168 +#: perms/serializers/asset/permission.py:75 msgid "Nodes amount" msgstr "节点数量" -#: assets/serializers/system_user.py:46 assets/serializers/system_user.py:167 -#: assets/serializers/system_user.py:193 +#: assets/serializers/system_user.py:48 assets/serializers/system_user.py:170 +#: assets/serializers/system_user.py:196 msgid "Login mode display" msgstr "登录模式(显示名称)" -#: assets/serializers/system_user.py:48 assets/serializers/system_user.py:169 +#: assets/serializers/system_user.py:50 assets/serializers/system_user.py:172 msgid "Ad domain" msgstr "Ad 网域" -#: assets/serializers/system_user.py:87 +#: assets/serializers/system_user.py:89 msgid "Username same with user with protocol {} only allow 1" msgstr "用户名和用户相同的一种协议只允许存在一个" -#: assets/serializers/system_user.py:100 +#: assets/serializers/system_user.py:102 msgid "* Automatic login mode must fill in the username." msgstr "自动登录模式,必须填写用户名" -#: assets/serializers/system_user.py:108 +#: assets/serializers/system_user.py:110 msgid "Path should starts with /" msgstr "路径应该以 / 开头" -#: assets/serializers/system_user.py:119 +#: assets/serializers/system_user.py:121 msgid "Password or private key required" msgstr "密码或密钥密码需要一个" @@ -1122,7 +1120,7 @@ msgstr "登录IP" msgid "Login city" msgstr "登录城市" -#: audits/models.py:104 audits/serializers.py:38 +#: audits/models.py:104 audits/serializers.py:45 msgid "User agent" msgstr "用户代理" @@ -1156,45 +1154,45 @@ msgstr "认证方式" msgid "Operate for display" msgstr "操作(显示名称)" -#: audits/serializers.py:26 +#: audits/serializers.py:30 msgid "Type for display" msgstr "类型(显示名称)" -#: audits/serializers.py:27 +#: audits/serializers.py:31 msgid "Status for display" msgstr "状态(显示名称)" -#: audits/serializers.py:28 +#: audits/serializers.py:32 msgid "MFA for display" msgstr "多因子认证状态(显示名称)" -#: audits/serializers.py:66 audits/serializers.py:81 ops/models/adhoc.py:247 -#: terminal/serializers/session.py:34 +#: audits/serializers.py:76 audits/serializers.py:91 ops/models/adhoc.py:247 +#: terminal/serializers/session.py:37 msgid "Is success" msgstr "是否成功" -#: audits/serializers.py:68 +#: audits/serializers.py:78 msgid "Hosts for display" msgstr "主机 (显示名称)" -#: audits/serializers.py:80 ops/models/command.py:26 +#: audits/serializers.py:90 ops/models/command.py:26 #: xpack/plugins/cloud/models.py:155 msgid "Result" msgstr "结果" -#: audits/serializers.py:82 terminal/serializers/storage.py:178 +#: audits/serializers.py:92 terminal/serializers/storage.py:178 msgid "Hosts" msgstr "主机" -#: audits/serializers.py:83 +#: audits/serializers.py:93 msgid "Run as" msgstr "运行用户" -#: audits/serializers.py:85 +#: audits/serializers.py:95 msgid "Run as for display" msgstr "运行用户(显示名称)" -#: audits/serializers.py:86 +#: audits/serializers.py:96 msgid "User for display" msgstr "用户(显示名称)" @@ -1206,6 +1204,12 @@ msgstr "SSH 密钥" msgid "SSO" msgstr "" +#: audits/signals_handler.py:60 +#: authentication/templates/authentication/login.html:210 +#: notifications/models.py:16 +msgid "WeCom" +msgstr "企业微信" + #: authentication/api/mfa.py:60 msgid "Code is invalid" msgstr "Code无效" @@ -1261,55 +1265,59 @@ msgstr "" msgid "Invalid token or cache refreshed." msgstr "" -#: authentication/errors.py:24 +#: authentication/errors.py:25 msgid "Username/password check failed" msgstr "用户名/密码 校验失败" -#: authentication/errors.py:25 +#: authentication/errors.py:26 msgid "Password decrypt failed" msgstr "密码解密失败" -#: authentication/errors.py:26 +#: authentication/errors.py:27 msgid "MFA failed" msgstr "多因子认证失败" -#: authentication/errors.py:27 +#: authentication/errors.py:28 msgid "MFA unset" msgstr "多因子认证没有设定" -#: authentication/errors.py:28 +#: authentication/errors.py:29 msgid "Username does not exist" msgstr "用户名不存在" -#: authentication/errors.py:29 +#: authentication/errors.py:30 msgid "Password expired" msgstr "密码已过期" -#: authentication/errors.py:30 +#: authentication/errors.py:31 msgid "Disabled or expired" msgstr "禁用或失效" -#: authentication/errors.py:31 +#: authentication/errors.py:32 msgid "This account is inactive." msgstr "此账户已禁用" -#: authentication/errors.py:32 +#: authentication/errors.py:33 msgid "This account is expired" msgstr "此账户已过期" -#: authentication/errors.py:33 +#: authentication/errors.py:34 msgid "Auth backend not match" msgstr "没有匹配到认证后端" -#: authentication/errors.py:34 +#: authentication/errors.py:35 msgid "ACL is not allowed" msgstr "ACL 不被允许" -#: authentication/errors.py:44 +#: authentication/errors.py:36 +msgid "Wecom login only for local user" +msgstr "" + +#: authentication/errors.py:46 msgid "No session found, check your cookie" msgstr "会话已变更,刷新页面" -#: authentication/errors.py:46 +#: authentication/errors.py:48 #, python-brace-format msgid "" "The username or password you entered is incorrect, please enter it again. " @@ -1319,13 +1327,13 @@ msgstr "" "您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将" "被临时 锁定 {block_time} 分钟)" -#: authentication/errors.py:52 authentication/errors.py:56 +#: authentication/errors.py:54 authentication/errors.py:58 msgid "" "The account has been locked (please contact admin to unlock it or try again " "after {} minutes)" msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)" -#: authentication/errors.py:60 +#: authentication/errors.py:62 #, python-brace-format msgid "" "MFA code invalid, or ntp sync server time, You can also try {times_try} " @@ -1334,46 +1342,50 @@ msgstr "" "MFA验证码不正确,或者服务器端时间不对。 您还可以尝试 {times_try} 次(账号将被" "临时 锁定 {block_time} 分钟)" -#: authentication/errors.py:65 +#: authentication/errors.py:67 msgid "MFA required" msgstr "需要多因子认证" -#: authentication/errors.py:66 +#: authentication/errors.py:68 msgid "MFA not set, please set it first" msgstr "多因子认证没有设置,请先完成设置" -#: authentication/errors.py:67 +#: authentication/errors.py:69 msgid "Login confirm required" msgstr "需要登录复核" -#: authentication/errors.py:68 +#: authentication/errors.py:70 msgid "Wait login confirm ticket for accept" msgstr "等待登录复核处理" -#: authentication/errors.py:69 +#: authentication/errors.py:71 msgid "Login confirm ticket was {}" msgstr "登录复核 {}" -#: authentication/errors.py:233 +#: authentication/errors.py:235 msgid "IP is not allowed" msgstr "来源 IP 不被允许登录" -#: authentication/errors.py:266 +#: authentication/errors.py:268 msgid "SSO auth closed" msgstr "SSO 认证关闭了" -#: authentication/errors.py:271 authentication/views/login.py:267 +#: authentication/errors.py:273 authentication/views/login.py:227 msgid "Your password is too simple, please change it for security" msgstr "你的密码过于简单,为了安全,请修改" -#: authentication/errors.py:280 authentication/views/login.py:282 +#: authentication/errors.py:282 authentication/views/login.py:242 msgid "You should to change your password before login" msgstr "登录完成前,请先修改密码" -#: authentication/errors.py:289 authentication/views/login.py:298 +#: authentication/errors.py:291 authentication/views/login.py:258 msgid "Your password has expired, please reset before logging in" msgstr "您的密码已过期,先修改再登录" +#: authentication/errors.py:320 +msgid "Your password is invalid" +msgstr "您的密码无效" + #: authentication/forms.py:26 msgid "{} days auto login" msgstr "{} 天内自动登录" @@ -1455,9 +1467,8 @@ msgid "Need MFA for view auth" msgstr "需要多因子认证来查看账号信息" #: authentication/templates/authentication/_mfa_confirm_modal.html:20 -#: authentication/views/login.py:286 authentication/views/login.py:302 -#: authentication/views/login.py:318 templates/_modal.html:23 -#: users/templates/users/user_password_verify.html:20 +#: authentication/views/login.py:246 authentication/views/login.py:262 +#: templates/_modal.html:23 users/templates/users/user_password_verify.html:20 msgid "Confirm" msgstr "确认" @@ -1465,33 +1476,38 @@ msgstr "确认" msgid "Code error" msgstr "代码错误" -#: authentication/templates/authentication/login.html:148 +#: authentication/templates/authentication/login.html:157 msgid "Welcome back, please enter username and password to login" msgstr "欢迎回来,请输入用户名和密码登录" -#: authentication/templates/authentication/login.html:174 +#: authentication/templates/authentication/login.html:183 #: users/templates/users/forgot_password.html:15 #: users/templates/users/forgot_password.html:16 msgid "Forgot password" msgstr "忘记密码" -#: authentication/templates/authentication/login.html:181 +#: authentication/templates/authentication/login.html:190 #: templates/_header_bar.html:83 msgid "Login" msgstr "登录" -#: authentication/templates/authentication/login.html:188 +#: authentication/templates/authentication/login.html:197 msgid "More login options" msgstr "更多登录方式" -#: authentication/templates/authentication/login.html:191 +#: authentication/templates/authentication/login.html:200 msgid "OpenID" msgstr "OpenID" -#: authentication/templates/authentication/login.html:196 +#: authentication/templates/authentication/login.html:205 msgid "CAS" msgstr "" +#: authentication/templates/authentication/login.html:215 +#: notifications/models.py:18 +msgid "DingTalk" +msgstr "钉钉" + #: authentication/templates/authentication/login_otp.html:17 msgid "One-time password" msgstr "一次性密码" @@ -1522,6 +1538,7 @@ msgid "Copy link" msgstr "复制链接" #: authentication/templates/authentication/login_wait_confirm.html:51 +#: templates/flash_message_standalone.html:38 msgid "Return" msgstr "返回" @@ -1529,19 +1546,52 @@ msgstr "返回" msgid "Copy success" msgstr "复制成功" -#: authentication/views/login.py:63 authentication/views/login.py:313 -msgid "Redirecting" -msgstr "跳转中" +#: authentication/views/dingtalk.py:40 authentication/views/wecom.py:40 +msgid "You've been hacked" +msgstr "" -#: authentication/views/login.py:64 -msgid "Redirecting to {} authentication" -msgstr "正在跳转到 {} 认证" +#: authentication/views/dingtalk.py:76 +msgid "DingTalk is already bound" +msgstr "" -#: authentication/views/login.py:88 +#: authentication/views/dingtalk.py:89 authentication/views/wecom.py:88 +msgid "Please verify your password first" +msgstr "请检查密码" + +#: authentication/views/dingtalk.py:113 authentication/views/wecom.py:112 +msgid "Invalid user_id" +msgstr "无效的 user_id" + +#: authentication/views/dingtalk.py:129 +msgid "DingTalk query user failed" +msgstr "" + +#: authentication/views/dingtalk.py:136 authentication/views/dingtalk.py:219 +#: authentication/views/dingtalk.py:220 +msgid "Binding DingTalk successfully" +msgstr "绑定 钉钉 成功" + +#: authentication/views/dingtalk.py:188 +msgid "Failed to get user from DingTalk" +msgstr "" + +#: authentication/views/dingtalk.py:194 +msgid "DingTalk is not bound" +msgstr "" + +#: authentication/views/dingtalk.py:195 authentication/views/wecom.py:193 +msgid "Please login with a password and then bind the WoCom" +msgstr "" + +#: authentication/views/dingtalk.py:237 authentication/views/dingtalk.py:238 +msgid "Binding DingTalk failed" +msgstr "" + +#: authentication/views/login.py:55 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:213 +#: authentication/views/login.py:173 msgid "" "Wait for {} confirm, You also can copy link to her/him
    \n" " Don't close this page" @@ -1549,27 +1599,48 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
    \n" " 不要关闭本页面" -#: authentication/views/login.py:218 +#: authentication/views/login.py:178 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:250 +#: authentication/views/login.py:210 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:251 +#: authentication/views/login.py:211 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/login.py:266 authentication/views/login.py:281 -#: authentication/views/login.py:297 +#: authentication/views/login.py:226 authentication/views/login.py:241 +#: authentication/views/login.py:257 msgid "Please change your password" msgstr "请修改密码" -#: authentication/views/login.py:314 -msgid "Redirect to third party auth" +#: authentication/views/wecom.py:75 +msgid "WeCom is already bound" msgstr "" +#: authentication/views/wecom.py:127 +msgid "WeCom query user failed" +msgstr "" + +#: authentication/views/wecom.py:134 authentication/views/wecom.py:217 +#: authentication/views/wecom.py:218 +msgid "Binding WeCom successfully" +msgstr "绑定 企业微信 成功" + +#: authentication/views/wecom.py:186 +msgid "Failed to get user from WeCom" +msgstr "" + +#: authentication/views/wecom.py:192 +msgid "WeCom is not bound" +msgstr "没有绑定企业微信" + +#: authentication/views/wecom.py:235 authentication/views/wecom.py:236 +msgid "Binding WeCom failed" +msgstr "绑定企业微信失败" + #: common/const/__init__.py:6 #, python-format msgid "%(name)s was created successfully" @@ -1649,7 +1720,15 @@ msgstr "" msgid "Encrypt field using Secret Key" msgstr "" -#: common/mixins/api.py:52 +#: common/message/backends/exceptions.py:23 +msgid "Network error, please contact system administrator" +msgstr "网络错误,请联系系统管理员" + +#: common/message/backends/wecom/__init__.py:19 +msgid "WeCom error, please contact system administrator" +msgstr "企业微信错误,请联系系统管理员" + +#: common/mixins/api.py:57 msgid "Request file format may be wrong" msgstr "上传的文件格式错误 或 其它类型资源的文件" @@ -1677,7 +1756,7 @@ msgstr "字段必须唯一" msgid "Should not contains special characters" msgstr "不能包含特殊字符" -#: jumpserver/context_processor.py:17 +#: jumpserver/context_processor.py:19 msgid "JumpServer Open Source Bastion Host" msgstr "JumpServer 开源堡垒机" @@ -1685,7 +1764,7 @@ msgstr "JumpServer 开源堡垒机" msgid "

    Flow service unavailable, check it

    " msgstr "" -#: jumpserver/views/other.py:25 +#: jumpserver/views/other.py:27 msgid "" "
    Luna is a separately deployed program, you need to deploy Luna, koko, " "configure nginx for url distribution,
    If you see this page, " @@ -1694,11 +1773,11 @@ msgstr "" "
    Luna是单独部署的一个程序,你需要部署luna,koko,
    如果你看到了" "这个页面,证明你访问的不是nginx监听的端口,祝你好运
    " -#: jumpserver/views/other.py:69 +#: jumpserver/views/other.py:71 msgid "Websocket server run on port: {}, you should proxy it on nginx" msgstr "Websocket 服务运行在端口: {}, 请检查nginx是否代理是否设置" -#: jumpserver/views/other.py:83 +#: jumpserver/views/other.py:85 msgid "" "
    Koko is a separately deployed program, you need to deploy Koko, " "configure nginx for url distribution,
    If you see this page, " @@ -1708,6 +1787,20 @@ msgstr "" "div>
    如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运" +#: notifications/models.py:17 users/forms/profile.py:101 +#: users/models/user.py:557 +msgid "Email" +msgstr "邮件" + +#: notifications/models.py:67 templates/_nav.html:110 terminal/apps.py:9 +#: terminal/serializers/session.py:40 +msgid "Terminal" +msgstr "终端" + +#: notifications/models.py:68 ops/apps.py:9 +msgid "Operations" +msgstr "运维" + #: ops/api/celery.py:61 ops/api/celery.py:76 msgid "Waiting task start" msgstr "等待任务开始" @@ -1811,7 +1904,7 @@ msgid "Time" msgstr "时间" #: ops/models/adhoc.py:246 ops/models/command.py:28 -#: terminal/serializers/session.py:38 +#: terminal/serializers/session.py:41 msgid "Is finished" msgstr "是否完成" @@ -1843,10 +1936,18 @@ msgstr "任务开始" msgid "Command `{}` is forbidden ........" msgstr "命令 `{}` 不允许被执行 ......." -#: ops/models/command.py:113 +#: ops/models/command.py:115 msgid "Task end" msgstr "任务结束" +#: ops/notifications.py:10 +msgid "Server performance" +msgstr "服务器性能" + +#: ops/notifications.py:17 +msgid "Disk used more than 80%: {} => {}" +msgstr "磁盘使用率超过 80%: {} => {}" + #: ops/tasks.py:71 msgid "Clean task history period" msgstr "定期清除任务历史" @@ -1863,10 +1964,6 @@ msgstr "任务列表" msgid "Update task content: {}" msgstr "更新任务内容: {}" -#: ops/utils.py:74 -msgid "Disk used more than 80%: {} => {}" -msgstr "磁盘使用率超过 80%: {} => {}" - #: orgs/api.py:76 #, python-brace-format msgid "Have `{model._meta.verbose_name}` exists, Please delete" @@ -1877,8 +1974,8 @@ msgid "The current organization cannot be deleted" msgstr "当前组织不能被删除" #: orgs/mixins/models.py:45 orgs/mixins/serializers.py:25 orgs/models.py:36 -#: orgs/models.py:417 orgs/serializers.py:101 -#: tickets/serializers/ticket/ticket.py:81 +#: orgs/models.py:417 orgs/serializers.py:108 +#: tickets/serializers/ticket/ticket.py:83 msgid "Organization" msgstr "组织" @@ -1948,7 +2045,7 @@ msgid "Clipboard copy paste" msgstr "剪贴板复制粘贴" #: perms/models/asset_permission.py:102 -#: perms/serializers/asset/permission.py:69 +#: perms/serializers/asset/permission.py:71 msgid "Actions" msgstr "动作" @@ -1989,49 +2086,41 @@ msgid "" "permission type. ({})" msgstr "应用列表中包含与授权类型不同的应用。({})" -#: perms/serializers/asset/permission.py:43 -#: perms/serializers/asset/permission.py:67 users/serializers/user.py:34 -#: users/serializers/user.py:70 +#: perms/serializers/asset/permission.py:45 +#: perms/serializers/asset/permission.py:69 users/serializers/user.py:34 +#: users/serializers/user.py:82 msgid "Is expired" msgstr "是否过期" -#: perms/serializers/asset/permission.py:44 -#, fuzzy -#| msgid "Username" +#: perms/serializers/asset/permission.py:46 msgid "Users name" msgstr "用户名" -#: perms/serializers/asset/permission.py:45 -#, fuzzy -#| msgid "User groups amount" +#: perms/serializers/asset/permission.py:47 msgid "User groups name" msgstr "用户组数量" -#: perms/serializers/asset/permission.py:46 -#, fuzzy -#| msgid "Asset num" -msgid "Assets name" -msgstr "资产数量" - #: perms/serializers/asset/permission.py:48 -#, fuzzy -#| msgid "System users amount" -msgid "System users name" -msgstr "系统用户数量" +msgid "Assets name" +msgstr "资产名字" -#: perms/serializers/asset/permission.py:68 users/serializers/user.py:69 +#: perms/serializers/asset/permission.py:50 +msgid "System users name" +msgstr "系统用户名字" + +#: perms/serializers/asset/permission.py:70 users/serializers/user.py:81 msgid "Is valid" msgstr "账户是否有效" -#: perms/serializers/asset/permission.py:70 users/serializers/group.py:36 +#: perms/serializers/asset/permission.py:72 users/serializers/group.py:36 msgid "Users amount" msgstr "用户数量" -#: perms/serializers/asset/permission.py:71 +#: perms/serializers/asset/permission.py:73 msgid "User groups amount" msgstr "用户组数量" -#: perms/serializers/asset/permission.py:74 +#: perms/serializers/asset/permission.py:76 msgid "System users amount" msgstr "系统用户数量" @@ -2044,6 +2133,10 @@ msgstr "邮件已经发送{}, 请检查" msgid "Welcome to the JumpServer open source Bastion Host" msgstr "欢迎使用JumpServer开源堡垒机" +#: settings/api/dingtalk.py:36 settings/api/wecom.py:36 +msgid "OK" +msgstr "" + #: settings/api/ldap.py:189 msgid "Get ldap users is None" msgstr "获取 LDAP 用户为 None" @@ -2379,6 +2472,38 @@ msgstr "邮件收件人" msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" +#: settings/serializers/settings.py:193 +msgid "Corporation ID" +msgstr "企业 ID(CorpId)" + +#: settings/serializers/settings.py:194 +msgid "Agent ID" +msgstr "应用 ID(AgentId)" + +#: settings/serializers/settings.py:195 +msgid "Corporation 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 "启用钉钉认证" + #: settings/utils/ldap.py:411 msgid "Host or port is disconnected: {}" msgstr "主机或端口不可连接: {}" @@ -2685,10 +2810,6 @@ msgstr "Web终端" msgid "File manager" msgstr "文件管理" -#: templates/_nav.html:110 terminal/serializers/session.py:37 -msgid "Terminal" -msgstr "终端" - #: templates/_nav.html:121 msgid "Job Center" msgstr "作业中心" @@ -2774,14 +2895,6 @@ msgstr "确认删除" msgid "Are you sure delete" msgstr "您确定删除吗?" -#: templates/flash_message_standalone.html:28 -msgid "Cancel" -msgstr "取消" - -#: templates/flash_message_standalone.html:37 -msgid "Go" -msgstr "跳转" - #: templates/index.html:11 msgid "Total users" msgstr "用户总数" @@ -3111,27 +3224,110 @@ msgstr "命令存储" msgid "Replay storage" msgstr "录像存储" -#: terminal/serializers/session.py:30 +#: terminal/notifications.py:29 +msgid "Terminal command alert" +msgstr "终端命令告警" + +#: terminal/notifications.py:38 +#, python-format +msgid "" +"\n" +" Command: %(command)s\n" +"
    \n" +" Asset: %(host_name)s (%(host_ip)s)\n" +"
    \n" +" User: %(user)s\n" +"
    \n" +" Level: %(risk_level)s\n" +"
    \n" +" Session:
    session " +"detail\n" +"
    \n" +" " +msgstr "" +"\n" +" 命令: %(command)s\n" +"
    \n" +" 资产: %(host_name)s (%(host_ip)s)\n" +"
    \n" +" 用户: %(user)s\n" +"
    \n" +" 等级: %(risk_level)s\n" +"
    \n" +" 会话: 会话详情\n" +"
    \n" +" " + +#: terminal/notifications.py:73 +#, python-format +msgid "" +"Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $" +"%(command)s" +msgstr "危险命令告警: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s" + +#: terminal/notifications.py:90 +msgid "Batch command alert" +msgstr "批量命令告警" + +#: terminal/notifications.py:101 +#, python-format +msgid "" +"\n" +"
    \n" +" Assets: %(assets)s\n" +"
    \n" +" User: %(user)s\n" +"
    \n" +" Level: %(risk_level)s\n" +"
    \n" +"\n" +" ----------------- Commands ---------------- " +"
    \n" +" %(command)s
    \n" +" ----------------- Commands ---------------- " +"
    \n" +" " +msgstr "" +"\n" +"
    \n" +" 资产: %(assets)s\n" +"
    \n" +" 用户: %(user)s\n" +"
    \n" +" 等级: %(risk_level)s\n" +"
    \n" +"\n" +" ----------------- 命令 ----------------
    \n" +" %(command)s
    \n" +" ----------------- 命令 ----------------
    \n" +" " + +#: terminal/notifications.py:127 +#, python-format +msgid "Insecure Web Command Execution Alert: [%(name)s]" +msgstr "Web页面-> 命令执行 告警: [%(name)s]" + +#: terminal/serializers/session.py:33 msgid "User ID" msgstr "用户 ID" -#: terminal/serializers/session.py:31 +#: terminal/serializers/session.py:34 msgid "Asset ID" msgstr "资产 ID" -#: terminal/serializers/session.py:32 +#: terminal/serializers/session.py:35 msgid "System user ID" msgstr "系统用户 ID" -#: terminal/serializers/session.py:33 +#: terminal/serializers/session.py:36 msgid "Login from for display" msgstr "登录来源(显示名称)" -#: terminal/serializers/session.py:35 +#: terminal/serializers/session.py:38 msgid "Can replay" msgstr "是否可重放" -#: terminal/serializers/session.py:36 +#: terminal/serializers/session.py:39 msgid "Can join" msgstr "是否可加入" @@ -3200,82 +3396,10 @@ msgstr "文档类型" msgid "Ignore Certificate Verification" msgstr "忽略证书认证" -#: terminal/serializers/terminal.py:66 terminal/serializers/terminal.py:74 +#: terminal/serializers/terminal.py:73 terminal/serializers/terminal.py:81 msgid "Not found" msgstr "没有发现" -#: terminal/utils.py:78 -#, python-format -msgid "" -"Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $" -"%(command)s" -msgstr "危险命令告警: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s" - -#: terminal/utils.py:86 -#, python-format -msgid "" -"\n" -" Command: %(command)s\n" -"
    \n" -" Asset: %(host_name)s (%(host_ip)s)\n" -"
    \n" -" User: %(user)s\n" -"
    \n" -" Level: %(risk_level)s\n" -"
    \n" -" Session: session detail\n" -"
    \n" -" " -msgstr "" -"\n" -" 命令: %(command)s\n" -"
    \n" -" 资产: %(host_name)s (%(host_ip)s)\n" -"
    \n" -" 用户: %(user)s\n" -"
    \n" -" 等级: %(risk_level)s\n" -"
    \n" -" 会话: 会话详情\n" -"
    \n" -" " - -#: terminal/utils.py:113 -#, python-format -msgid "Insecure Web Command Execution Alert: [%(name)s]" -msgstr "Web页面-> 命令执行 告警: [%(name)s]" - -#: terminal/utils.py:121 -#, python-format -msgid "" -"\n" -"
    \n" -" Assets: %(assets)s\n" -"
    \n" -" User: %(user)s\n" -"
    \n" -" Level: %(risk_level)s\n" -"
    \n" -"\n" -" ----------------- Commands ----------------
    \n" -" %(command)s
    \n" -" ----------------- Commands ----------------
    \n" -" " -msgstr "" -"\n" -"
    \n" -" 资产: %(assets)s\n" -"
    \n" -" 用户: %(user)s\n" -"
    \n" -" 等级: %(risk_level)s\n" -"
    \n" -"\n" -" ----------------- 命令 ----------------
    \n" -" %(command)s
    \n" -" ----------------- 命令 ----------------
    \n" -" " - #: tickets/const.py:8 msgid "General" msgstr "一般" @@ -3636,13 +3760,13 @@ msgstr "动作 (显示名称)" msgid "Status display" msgstr "状态(显示名称)" -#: tickets/serializers/ticket/ticket.py:99 +#: tickets/serializers/ticket/ticket.py:101 msgid "" "The `type` in the submission data (`{}`) is different from the type in the " "request url (`{}`)" msgstr "提交数据中的类型 (`{}`) 与请求URL地址中的类型 (`{}`) 不一致" -#: tickets/serializers/ticket/ticket.py:120 +#: tickets/serializers/ticket/ticket.py:122 msgid "None of the assignees belong to Organization `{}` admins" msgstr "所有受理人都不属于组织 `{}` 下的管理员" @@ -3712,10 +3836,6 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:101 users/models/user.py:557 -msgid "Email" -msgstr "邮件" - #: users/forms/profile.py:108 msgid "Old password" msgstr "原来密码" @@ -3781,11 +3901,11 @@ msgstr "用户来源" msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:741 +#: users/models/user.py:751 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:744 +#: users/models/user.py:754 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -3793,7 +3913,7 @@ msgstr "Administrator是初始的超级管理员" msgid "The old password is incorrect" msgstr "旧密码错误" -#: users/serializers/profile.py:36 users/serializers/user.py:113 +#: users/serializers/profile.py:36 users/serializers/user.py:125 msgid "Password does not match security rules" msgstr "密码不满足安全规则" @@ -3805,7 +3925,7 @@ msgstr "新密码不能是最近 {} 次的密码" msgid "The newly set password is inconsistent" msgstr "两次密码不一致" -#: users/serializers/profile.py:121 users/serializers/user.py:68 +#: users/serializers/profile.py:121 users/serializers/user.py:80 msgid "Is first login" msgstr "首次登录" @@ -3846,35 +3966,35 @@ msgstr "是否可更新" msgid "Can delete" msgstr "是否可删除" -#: users/serializers/user.py:39 users/serializers/user.py:75 +#: users/serializers/user.py:39 users/serializers/user.py:87 msgid "Organization role name" msgstr "组织角色名称" -#: users/serializers/user.py:71 +#: users/serializers/user.py:83 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:73 +#: users/serializers/user.py:85 msgid "Groups name" msgstr "用户组名" -#: users/serializers/user.py:74 +#: users/serializers/user.py:86 msgid "Source name" msgstr "用户来源名" -#: users/serializers/user.py:76 +#: users/serializers/user.py:88 msgid "Super role name" msgstr "超级角色名称" -#: users/serializers/user.py:77 +#: users/serializers/user.py:89 msgid "Total role name" msgstr "汇总角色名称" -#: users/serializers/user.py:101 +#: users/serializers/user.py:113 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/user.py:198 +#: users/serializers/user.py:210 msgid "name not unique" msgstr "名称重复" @@ -4925,12 +5045,85 @@ msgstr "旗舰版" msgid "Community edition" msgstr "社区版" -#~ msgid "The administrator require you to change your password this time" -#~ msgstr "管理员要求您本次修改密码" +#, python-format +#~ msgid "" +#~ "\n" +#~ " Command: %(command)s\n" +#~ "
    \n" +#~ " Asset: %(host_name)s (%(host_ip)s)\n" +#~ "
    \n" +#~ " User: %(user)s\n" +#~ "
    \n" +#~ " Level: %(risk_level)s\n" +#~ "
    \n" +#~ " Session: session detail\n" +#~ "
    \n" +#~ " " +#~ msgstr "" +#~ "\n" +#~ " 命令: %(command)s\n" +#~ "
    \n" +#~ " 资产: %(host_name)s (%(host_ip)s)\n" +#~ "
    \n" +#~ " 用户: %(user)s\n" +#~ "
    \n" +#~ " 等级: %(risk_level)s\n" +#~ "
    \n" +#~ " 会话: 会话详情\n" +#~ "
    \n" +#~ " " + +#, python-format +#~ msgid "" +#~ "\n" +#~ "
    \n" +#~ " Assets: %(assets)s\n" +#~ "
    \n" +#~ " User: %(user)s\n" +#~ "
    \n" +#~ " Level: %(risk_level)s\n" +#~ "
    \n" +#~ "\n" +#~ " ----------------- Commands ----------------
    \n" +#~ " %(command)s
    \n" +#~ " ----------------- Commands ----------------
    \n" +#~ " " +#~ msgstr "" +#~ "\n" +#~ "
    \n" +#~ " 资产: %(assets)s\n" +#~ "
    \n" +#~ " 用户: %(user)s\n" +#~ "
    \n" +#~ " 等级: %(risk_level)s\n" +#~ "
    \n" +#~ "\n" +#~ " ----------------- 命令 ----------------
    \n" +#~ " %(command)s
    \n" +#~ " ----------------- 命令 ----------------
    \n" +#~ " " + +#~ msgid "Ops" +#~ msgstr "选项" + +#~ msgid "Command Alert" +#~ msgstr "命令告警" + +#~ msgid "Agent Secret" +#~ msgstr "凭证密钥(secret)" + +#~ msgid "APP key" +#~ msgstr "APPKEY" + +#~ msgid "APP secret" +#~ msgstr "LDAP 地址" #~ msgid "Auth" #~ msgstr "认证" +#~ msgid "The administrator require you to change your password this time" +#~ msgstr "管理员要求您本次修改密码" + #~ msgid "Security and Role" #~ msgstr "角色安全" @@ -4985,9 +5178,6 @@ msgstr "社区版" #~ msgid "Join" #~ msgstr "加入" -#~ msgid "Update successfully!" -#~ msgstr "更新成功" - #~ msgid "Goto profile page enable MFA" #~ msgstr "请去个人信息页面启用自己的多因子认证" @@ -5000,6 +5190,9 @@ msgstr "社区版" #~ msgid "This will reset the user password and send a reset mail" #~ msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" +#~ msgid "Cancel" +#~ msgstr "取消" + #~ msgid "" #~ "The reset-ssh-public-key E-mail has been sent successfully. Please inform " #~ "the user to update his new ssh public key." diff --git a/apps/ops/apps.py b/apps/ops/apps.py index 01dfd05fa..8bdc04ce8 100644 --- a/apps/ops/apps.py +++ b/apps/ops/apps.py @@ -1,10 +1,12 @@ from __future__ import unicode_literals +from django.utils.translation import gettext_lazy as _ from django.apps import AppConfig class OpsConfig(AppConfig): name = 'ops' + verbose_name = _('Operations') def ready(self): from orgs.models import Organization diff --git a/apps/orgs/signals_handler/common.py b/apps/orgs/signals_handler/common.py index f59c4cb47..6beecd5cb 100644 --- a/apps/orgs/signals_handler/common.py +++ b/apps/orgs/signals_handler/common.py @@ -8,7 +8,6 @@ from django.dispatch import receiver from django.utils.functional import LazyObject from django.db.models.signals import m2m_changed from django.db.models.signals import post_save, post_delete, pre_delete -from django.utils.translation import ugettext as _ from orgs.utils import tmp_to_org from orgs.models import Organization, OrganizationMember @@ -19,7 +18,6 @@ from common.const.signals import PRE_REMOVE, POST_REMOVE from common.signals import django_ready from common.utils import get_logger from common.utils.connection import RedisPubSub -from common.exceptions import JMSException logger = get_logger(__file__) diff --git a/apps/perms/api/asset/user_permission/mixin.py b/apps/perms/api/asset/user_permission/mixin.py index 3b8733b3b..c0f25b8a4 100644 --- a/apps/perms/api/asset/user_permission/mixin.py +++ b/apps/perms/api/asset/user_permission/mixin.py @@ -3,8 +3,9 @@ from rest_framework.request import Request from common.permissions import IsOrgAdminOrAppUser, IsValidUser -from common.utils import lazyproperty from common.http import is_true +from common.mixins.api import RoleAdminMixin as _RoleAdminMixin +from common.mixins.api import RoleUserMixin as _RoleUserMixin from orgs.utils import tmp_to_root_org from users.models import User from perms.utils.asset.user_permission import UserGrantedTreeRefreshController @@ -20,24 +21,13 @@ class PermBaseMixin: return super().get(request, *args, **kwargs) -class RoleAdminMixin(PermBaseMixin): +class RoleAdminMixin(PermBaseMixin, _RoleAdminMixin): permission_classes = (IsOrgAdminOrAppUser,) - kwargs: dict - - @lazyproperty - def user(self): - user_id = self.kwargs.get('pk') - return User.objects.get(id=user_id) -class RoleUserMixin(PermBaseMixin): +class RoleUserMixin(PermBaseMixin, _RoleUserMixin): permission_classes = (IsValidUser,) - request: Request def get(self, request, *args, **kwargs): with tmp_to_root_org(): return super().get(request, *args, **kwargs) - - @lazyproperty - def user(self): - return self.request.user diff --git a/apps/settings/api/__init__.py b/apps/settings/api/__init__.py index 151617be5..39e009ed5 100644 --- a/apps/settings/api/__init__.py +++ b/apps/settings/api/__init__.py @@ -1,2 +1,4 @@ from .common import * from .ldap import * +from .wecom import * +from .dingtalk import * diff --git a/apps/settings/api/common.py b/apps/settings/api/common.py index 1fa578c50..bb3107dfd 100644 --- a/apps/settings/api/common.py +++ b/apps/settings/api/common.py @@ -125,7 +125,9 @@ class PublicSettingApi(generics.RetrieveAPIView): 'SECURITY_PASSWORD_LOWER_CASE': settings.SECURITY_PASSWORD_LOWER_CASE, 'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER, 'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR, - } + }, + "AUTH_WECOM": settings.AUTH_WECOM, + "AUTH_DINGTALK": settings.AUTH_DINGTALK, } } return instance @@ -141,6 +143,8 @@ class SettingsApi(generics.RetrieveUpdateAPIView): 'ldap': serializers.LDAPSettingSerializer, 'email': serializers.EmailSettingSerializer, 'email_content': serializers.EmailContentSettingSerializer, + 'wecom': serializers.WeComSettingSerializer, + 'dingtalk': serializers.DingTalkSettingSerializer, } def get_serializer_class(self): @@ -163,6 +167,8 @@ class SettingsApi(generics.RetrieveUpdateAPIView): category = self.request.query_params.get('category', '') for name, value in serializer.validated_data.items(): encrypted = name in encrypted_items + if encrypted and value in ['', None]: + continue data.append({ 'name': name, 'value': value, 'encrypted': encrypted, 'category': category diff --git a/apps/settings/api/dingtalk.py b/apps/settings/api/dingtalk.py new file mode 100644 index 000000000..e560f8626 --- /dev/null +++ b/apps/settings/api/dingtalk.py @@ -0,0 +1,38 @@ +import requests + +from rest_framework.views import Response +from rest_framework.generics import GenericAPIView +from django.utils.translation import gettext_lazy as _ + +from common.permissions import IsSuperUser +from common.message.backends.dingtalk import URL + +from .. import serializers + + +class DingTalkTestingAPI(GenericAPIView): + permission_classes = (IsSuperUser,) + serializer_class = serializers.DingTalkSettingSerializer + + def post(self, request): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + dingtalk_appkey = serializer.validated_data['DINGTALK_APPKEY'] + dingtalk_agentid = serializer.validated_data['DINGTALK_AGENTID'] + dingtalk_appsecret = serializer.validated_data['DINGTALK_APPSECRET'] + + 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)}) diff --git a/apps/settings/api/wecom.py b/apps/settings/api/wecom.py new file mode 100644 index 000000000..0fda33c61 --- /dev/null +++ b/apps/settings/api/wecom.py @@ -0,0 +1,38 @@ +import requests + +from rest_framework.views import Response +from rest_framework.generics import GenericAPIView +from django.utils.translation import gettext_lazy as _ + +from common.permissions import IsSuperUser +from common.message.backends.wecom import URL + +from .. import serializers + + +class WeComTestingAPI(GenericAPIView): + permission_classes = (IsSuperUser,) + serializer_class = serializers.WeComSettingSerializer + + def post(self, request): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + wecom_corpid = serializer.validated_data['WECOM_CORPID'] + wecom_agentid = serializer.validated_data['WECOM_AGENTID'] + wecom_corpsecret = serializer.validated_data['WECOM_CORPSECRET'] + + 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)}) diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index b64b95cb6..5d33a1d83 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -6,7 +6,7 @@ from rest_framework import serializers __all__ = [ 'BasicSettingSerializer', 'EmailSettingSerializer', 'EmailContentSettingSerializer', 'LDAPSettingSerializer', 'TerminalSettingSerializer', 'SecuritySettingSerializer', - 'SettingsSerializer' + 'SettingsSerializer', 'WeComSettingSerializer', 'DingTalkSettingSerializer', ] @@ -189,13 +189,29 @@ class SecuritySettingSerializer(serializers.Serializer): ) +class WeComSettingSerializer(serializers.Serializer): + WECOM_CORPID = serializers.CharField(max_length=256, required=True, label=_('Corporation ID')) + WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label=_("Agent ID")) + WECOM_CORPSECRET = serializers.CharField(max_length=256, required=False, label=_("Corporation 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) + AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('Enable DingTalk Auth')) + + class SettingsSerializer( BasicSettingSerializer, EmailSettingSerializer, EmailContentSettingSerializer, LDAPSettingSerializer, TerminalSettingSerializer, - SecuritySettingSerializer + SecuritySettingSerializer, + WeComSettingSerializer, + DingTalkSettingSerializer, ): # encrypt_fields 现在使用 write_only 来判断了 diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py index 0db9c7c54..86dfc6847 100644 --- a/apps/settings/urls/api_urls.py +++ b/apps/settings/urls/api_urls.py @@ -13,6 +13,8 @@ urlpatterns = [ path('ldap/users/', api.LDAPUserListApi.as_view(), name='ldap-user-list'), path('ldap/users/import/', api.LDAPUserImportAPI.as_view(), name='ldap-user-import'), path('ldap/cache/refresh/', api.LDAPCacheRefreshAPI.as_view(), name='ldap-cache-refresh'), + path('wecom/testing/', api.WeComTestingAPI.as_view(), name='wecom-testing'), + path('dingtalk/testing/', api.DingTalkTestingAPI.as_view(), name='dingtalk-testing'), path('setting/', api.SettingsApi.as_view(), name='settings-setting'), path('public/', api.PublicSettingApi.as_view(), name='public-setting'), diff --git a/apps/static/img/login_dingtalk_log.png b/apps/static/img/login_dingtalk_log.png new file mode 100644 index 0000000000000000000000000000000000000000..998f730ad056a13c1e9b06036be3851055524550 GIT binary patch literal 5062 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�)QNC&!(EYe9DFXWyC0UyYhZsGUF5`; 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 Date: Mon, 10 May 2021 10:54:48 +0800 Subject: [PATCH 37/39] =?UTF-8?q?=E9=A6=96=E9=A1=B5=E7=9A=84=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E6=95=B0=E6=8D=AE=EF=BC=8C=E5=8F=AF=E4=BB=A5=E4=BB=8E?= =?UTF-8?q?=20org=20resource=20cache=20=E4=B8=AD=E8=8E=B7=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 首页的统计数据,可以从 org resource cache 中获取 --- apps/jumpserver/api.py | 32 ++++++++--------------------- apps/orgs/caches.py | 13 ++++++++++-- apps/orgs/signals_handler/cache.py | 33 +++++++++++++++--------------- 3 files changed, 36 insertions(+), 42 deletions(-) diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index 0f0193c49..4e734bda6 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -18,6 +18,7 @@ from terminal.utils import ComponentsPrometheusMetricsUtil from orgs.utils import current_org from common.permissions import IsOrgAdmin, IsOrgAuditor from common.utils import lazyproperty +from orgs.caches import OrgResourceStatisticsCache __all__ = ['IndexApi'] @@ -210,26 +211,7 @@ class DatesLoginMetricMixin: return sessions -class TotalCountMixin: - @staticmethod - def get_total_count_users(): - return current_org.get_members().count() - - @staticmethod - def get_total_count_assets(): - return Asset.objects.all().count() - - @staticmethod - def get_total_count_online_users(): - count = len(set(Session.objects.filter(is_finished=False).values_list('user_id', flat=True))) - return count - - @staticmethod - def get_total_count_online_sessions(): - return Session.objects.filter(is_finished=False).count() - - -class IndexApi(TotalCountMixin, DatesLoginMetricMixin, APIView): +class IndexApi(DatesLoginMetricMixin, APIView): permission_classes = (IsOrgAdmin | IsOrgAuditor,) http_method_names = ['get'] @@ -238,26 +220,28 @@ class IndexApi(TotalCountMixin, DatesLoginMetricMixin, APIView): query_params = self.request.query_params + caches = OrgResourceStatisticsCache(self.request.user.user_orgs[0]) + _all = query_params.get('all') if _all or query_params.get('total_count') or query_params.get('total_count_users'): data.update({ - 'total_count_users': self.get_total_count_users(), + 'total_count_users': caches.users_amount, }) if _all or query_params.get('total_count') or query_params.get('total_count_assets'): data.update({ - 'total_count_assets': self.get_total_count_assets(), + 'total_count_assets': caches.assets_amount, }) if _all or query_params.get('total_count') or query_params.get('total_count_online_users'): data.update({ - 'total_count_online_users': self.get_total_count_online_users(), + 'total_count_online_users': caches.total_count_online_users, }) if _all or query_params.get('total_count') or query_params.get('total_count_online_sessions'): data.update({ - 'total_count_online_sessions': self.get_total_count_online_sessions(), + 'total_count_online_sessions': caches.total_count_online_sessions, }) if _all or query_params.get('dates_metrics'): diff --git a/apps/orgs/caches.py b/apps/orgs/caches.py index 9c29659e4..c9d60bd18 100644 --- a/apps/orgs/caches.py +++ b/apps/orgs/caches.py @@ -6,12 +6,12 @@ from orgs.utils import current_org, tmp_to_org from common.cache import Cache, IntegerField from common.utils import get_logger from users.models import UserGroup, User -from assets.models import Node, AdminUser, SystemUser, Domain, Gateway +from assets.models import Node, AdminUser, SystemUser, Domain, Gateway, Asset +from terminal.models import Session from applications.models import Application from perms.models import AssetPermission, ApplicationPermission from .models import OrganizationMember - logger = get_logger(__file__) @@ -64,6 +64,9 @@ class OrgResourceStatisticsCache(OrgRelatedCache): asset_perms_amount = IntegerField(queryset=AssetPermission.objects) app_perms_amount = IntegerField(queryset=ApplicationPermission.objects) + total_count_online_users = IntegerField() + total_count_online_sessions = IntegerField() + def __init__(self, org): super().__init__() self.org = org @@ -86,3 +89,9 @@ class OrgResourceStatisticsCache(OrgRelatedCache): def compute_assets_amount(self): node = Node.org_root() return node.assets_amount + + def compute_total_count_online_users(self): + return len(set(Session.objects.filter(is_finished=False).values_list('user_id', flat=True))) + + def compute_total_count_online_sessions(self): + return Session.objects.filter(is_finished=False).count() diff --git a/apps/orgs/signals_handler/cache.py b/apps/orgs/signals_handler/cache.py index c3c23efb4..5862d5edf 100644 --- a/apps/orgs/signals_handler/cache.py +++ b/apps/orgs/signals_handler/cache.py @@ -1,5 +1,5 @@ from django.db.models.signals import m2m_changed -from django.db.models.signals import post_save, pre_delete +from django.db.models.signals import post_save, pre_delete, pre_save from django.dispatch import receiver from orgs.models import Organization, OrganizationMember @@ -7,6 +7,7 @@ from assets.models import Node from perms.models import (AssetPermission, ApplicationPermission) from users.models import UserGroup, User from applications.models import Application +from terminal.models import Session from assets.models import Asset, AdminUser, SystemUser, Domain, Gateway from common.const.signals import POST_PREFIX from orgs.caches import OrgResourceStatisticsCache @@ -47,16 +48,17 @@ def on_org_user_changed_refresh_cache(sender, action, instance, reverse, pk_set, class OrgResourceStatisticsRefreshUtil: model_cache_field_mapper = { - ApplicationPermission: 'app_perms_amount', - AssetPermission: 'asset_perms_amount', - Application: 'applications_amount', - Gateway: 'gateways_amount', - Domain: 'domains_amount', - SystemUser: 'system_users_amount', - AdminUser: 'admin_users_amount', - Node: 'nodes_amount', - Asset: 'assets_amount', - UserGroup: 'groups_amount', + ApplicationPermission: ['app_perms_amount'], + AssetPermission: ['asset_perms_amount'], + Application: ['applications_amount'], + Gateway: ['gateways_amount'], + Domain: ['domains_amount'], + SystemUser: ['system_users_amount'], + AdminUser: ['admin_users_amount'], + Node: ['nodes_amount'], + Asset: ['assets_amount'], + UserGroup: ['groups_amount'], + Session: ['total_count_online_users', 'total_count_online_sessions'] } @classmethod @@ -64,13 +66,12 @@ class OrgResourceStatisticsRefreshUtil: cache_field_name = cls.model_cache_field_mapper.get(type(instance)) if cache_field_name: org_cache = OrgResourceStatisticsCache(instance.org) - org_cache.expire(cache_field_name) + org_cache.expire(*cache_field_name) -@receiver(post_save) -def on_post_save_refresh_org_resource_statistics_cache(sender, instance, created, **kwargs): - if created: - OrgResourceStatisticsRefreshUtil.refresh_if_need(instance) +@receiver(pre_save) +def on_post_save_refresh_org_resource_statistics_cache(sender, instance, **kwargs): + OrgResourceStatisticsRefreshUtil.refresh_if_need(instance) @receiver(pre_delete) From b37c8b09bf5787f7e0b68be8d3c9d9be99c881fc Mon Sep 17 00:00:00 2001 From: xinwen Date: Thu, 13 May 2021 16:01:59 +0800 Subject: [PATCH 38/39] =?UTF-8?q?refactor:=20=E6=B7=BB=E5=8A=A0=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E7=BF=BB=E8=AF=91&=E4=BF=AE=E6=AD=A3=E5=AD=97?= =?UTF-8?q?=E6=AE=B5WECOM=5FSECRET?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/views/wecom.py | 4 +- apps/jumpserver/conf.py | 2 +- apps/jumpserver/settings/auth.py | 2 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 74427 -> 74036 bytes apps/locale/zh/LC_MESSAGES/django.po | 424 +++++++++++++------------ apps/settings/api/wecom.py | 2 +- apps/settings/serializers/settings.py | 6 +- apps/static/img/login_dingtalk_log.png | Bin 5062 -> 1518 bytes 8 files changed, 223 insertions(+), 217 deletions(-) diff --git a/apps/authentication/views/wecom.py b/apps/authentication/views/wecom.py index 5dc683f87..6d9592c8a 100644 --- a/apps/authentication/views/wecom.py +++ b/apps/authentication/views/wecom.py @@ -119,7 +119,7 @@ class WeComQRBindCallbackView(WeComQRMixin, View): wecom = WeCom( corpid=settings.WECOM_CORPID, - corpsecret=settings.WECOM_CORPSECRET, + corpsecret=settings.WECOM_SECRET, agentid=settings.WECOM_AGENTID ) wecom_userid, __ = wecom.get_user_id_by_code(code) @@ -177,7 +177,7 @@ class WeComQRLoginCallbackView(AuthMixin, WeComQRMixin, View): wecom = WeCom( corpid=settings.WECOM_CORPID, - corpsecret=settings.WECOM_CORPSECRET, + corpsecret=settings.WECOM_SECRET, agentid=settings.WECOM_AGENTID ) wecom_userid, __ = wecom.get_user_id_by_code(code) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 57c754942..7bb8dd095 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -219,7 +219,7 @@ class Config(dict): 'AUTH_WECOM': False, 'WECOM_CORPID': '', 'WECOM_AGENTID': '', - 'WECOM_CORPSECRET': '', + 'WECOM_SECRET': '', 'AUTH_DINGTALK': False, 'DINGTALK_AGENTID': '', diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 4fb77cb47..28f65a643 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -105,7 +105,7 @@ AUTH_SSO_AUTHKEY_TTL = CONFIG.AUTH_SSO_AUTHKEY_TTL AUTH_WECOM = CONFIG.AUTH_WECOM WECOM_CORPID = CONFIG.WECOM_CORPID WECOM_AGENTID = CONFIG.WECOM_AGENTID -WECOM_CORPSECRET = CONFIG.WECOM_CORPSECRET +WECOM_SECRET = CONFIG.WECOM_SECRET # DingDing auth AUTH_DINGTALK = CONFIG.AUTH_DINGTALK diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index c7bd94308a579bcb334dd636c31e63416167f69f..71f503ed789d16541a18dac7a8b752eb08da0f96 100644 GIT binary patch delta 21034 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 ehF?2Li?Hj=yRU@AKo*Gk)9{iCerF#e4E|@ zBwgXwdXBl8_#yJGY4-;C^|a|pz7OdWUk`p8p}rdVUDPiljV8a9`V_oM`5g6$lqZok zyL}qE@Mj3Ym!wzM3^M0PJLq_Uj`#7^RopVY@H^uCt;|_Yn_%zG1`(cEBFk(fKk`qk zbq8WoC{HK7LH=XXC#2J))Wk+I!V1z9%DE}mdo>;YKFAZ-9p>9K&=}ekf`9Hz}0zXTF96xjf__62I)t)F?8f3^`q| z<8IOwZ@EU1e#O0SHHuC*j+PsVUGolY6zysEk1;OM<0EzBDn%O295<<-Noqm5?5p87 z6XN_ku~UVl>nU}^urcMWxRCN1tW3Kvtj&+)buFR%Gxc_PBO9ml{Uz@U3nWojm9~#5 zHzSpxJkQ3MhG(f;OZ*z~U%agwM|d)lt4kymH4Vt8(CR(RKulME(s9cBeI<56J3rE^ zD;ISYDHkTt2oKq8dlaMYG7cy9fP7PuuCL73Qt^1LunwyDf{Ib3St{5ykn#`)sY|mpm z45{lZaa|u^Gtw5~OR3Lk@hX(}kwz2SM{GKFC9NZ!BK>OZ#*v>z(zS;6rTv{YzW=vC zdm22Yq7W&Zd|J1S(-+5*4$$~>{FVBPcpaliy0TN(&?dO!hMcA5N$=4&!lJy?cBb_U zqWx)o-C8@umU;b}hQ;P0nueq+lxSaCl&1VWZXo^PE!{LBe*(EZq{Sp%lW92K)ZIx^6zAUNz&oNXrJKTEuG7A{l=m-5_1JdFzpnqiq}R}Hsx;9O8x;c zTwc$PtQnPgb$Q1(&-3rV|7y{=RrLSU;-}_$|J~x~j_`j6F7C+T?b#yX-@ytkV*eem zR%!oxalM*{dt>&b_5SvD{C|Qh@IQrYtxEo<@UI${{eRT3-2bCyZmS0Ws&UWER*_Pm z**ktI^IA0XPf7L8!b~Y^yT0L{64gC8Fm=1`9h3T{L?yNMPZ{2?R;n)NYlOw*1Xk~;QFIXo~|hLjg0DrEQ8p1mV;(twUhF}*t`b?TMWqg~Gq9a944)CtIv z)T3+9cHLt-^y)$Iy%^uH-ahlAQaa7w8\n" "Language-Team: JumpServer team\n" @@ -25,7 +25,7 @@ msgstr "" #: orgs/models.py:23 perms/models/base.py:49 settings/models.py:29 #: terminal/models/storage.py:23 terminal/models/storage.py:90 #: terminal/models/task.py:16 terminal/models/terminal.py:100 -#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:555 +#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:558 #: users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 @@ -61,7 +61,7 @@ msgstr "激活中" #: orgs/models.py:26 perms/models/base.py:57 settings/models.py:34 #: terminal/models/storage.py:29 terminal/models/storage.py:96 #: terminal/models/terminal.py:114 tickets/models/ticket.py:73 -#: users/models/group.py:16 users/models/user.py:588 +#: users/models/group.py:16 users/models/user.py:591 #: xpack/plugins/change_auth_plan/models.py:77 xpack/plugins/cloud/models.py:35 #: xpack/plugins/cloud/models.py:98 xpack/plugins/gathered_user/models.py:26 msgid "Comment" @@ -99,7 +99,7 @@ msgstr "动作" #: terminal/backends/command/models.py:18 #: terminal/backends/command/serializers.py:12 terminal/models/session.py:38 #: tickets/models/comment.py:17 users/models/user.py:184 -#: users/models/user.py:743 users/models/user.py:769 +#: users/models/user.py:746 users/models/user.py:772 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -180,7 +180,7 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " #: applications/serializers/attrs/application_type/vmware_client.py:26 #: assets/models/base.py:251 assets/models/gathered_user.py:15 #: audits/models.py:100 authentication/forms.py:15 authentication/forms.py:17 -#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:553 +#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:556 #: users/templates/users/_select_user_modal.html:14 #: xpack/plugins/change_auth_plan/models.py:47 #: xpack/plugins/change_auth_plan/models.py:278 @@ -483,7 +483,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:24 -#: orgs/models.py:422 perms/models/base.py:55 users/models/user.py:596 +#: orgs/models.py:422 perms/models/base.py:55 users/models/user.py:599 #: users/serializers/group.py:35 xpack/plugins/change_auth_plan/models.py:81 #: xpack/plugins/cloud/models.py:104 xpack/plugins/gathered_user/models.py:30 msgid "Created by" @@ -497,7 +497,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:25 #: orgs/models.py:420 perms/models/base.py:56 users/models/group.py:18 -#: users/models/user.py:770 xpack/plugins/cloud/models.py:107 +#: users/models/user.py:773 xpack/plugins/cloud/models.py:107 msgid "Date created" msgstr "创建日期" @@ -543,7 +543,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:574 +#: assets/models/cluster.py:22 users/models/user.py:577 msgid "Phone" msgstr "手机" @@ -569,7 +569,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:755 +#: users/models/user.py:758 msgid "System" msgstr "系统" @@ -814,11 +814,11 @@ msgid "Backend" msgstr "后端" #: assets/serializers/asset_user.py:80 users/forms/profile.py:160 -#: users/models/user.py:585 users/templates/users/user_password_update.html:48 +#: users/models/user.py:588 users/templates/users/user_password_update.html:48 msgid "Public key" msgstr "SSH公钥" -#: assets/serializers/asset_user.py:84 users/models/user.py:582 +#: assets/serializers/asset_user.py:84 users/models/user.py:585 msgid "Private key" msgstr "ssh私钥" @@ -1127,7 +1127,7 @@ msgstr "用户代理" #: audits/models.py:105 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/login_otp.html:6 -#: users/forms/profile.py:64 users/models/user.py:577 +#: users/forms/profile.py:64 users/models/user.py:580 #: users/serializers/profile.py:104 msgid "MFA" msgstr "多因子认证" @@ -1206,10 +1206,14 @@ msgstr "" #: audits/signals_handler.py:60 #: authentication/templates/authentication/login.html:210 -#: notifications/models.py:16 msgid "WeCom" msgstr "企业微信" +#: audits/signals_handler.py:61 +#: authentication/templates/authentication/login.html:215 +msgid "DingTalk" +msgstr "钉钉" + #: authentication/api/mfa.py:60 msgid "Code is invalid" msgstr "Code无效" @@ -1310,7 +1314,7 @@ msgid "ACL is not allowed" msgstr "ACL 不被允许" #: authentication/errors.py:36 -msgid "Wecom login only for local user" +msgid "Only local users are allowed" msgstr "" #: authentication/errors.py:46 @@ -1370,15 +1374,15 @@ msgstr "来源 IP 不被允许登录" msgid "SSO auth closed" msgstr "SSO 认证关闭了" -#: authentication/errors.py:273 authentication/views/login.py:227 +#: authentication/errors.py:273 authentication/mixins.py:271 msgid "Your password is too simple, please change it for security" msgstr "你的密码过于简单,为了安全,请修改" -#: authentication/errors.py:282 authentication/views/login.py:242 +#: authentication/errors.py:282 authentication/mixins.py:278 msgid "You should to change your password before login" msgstr "登录完成前,请先修改密码" -#: authentication/errors.py:291 authentication/views/login.py:258 +#: authentication/errors.py:291 authentication/mixins.py:285 msgid "Your password has expired, please reset before logging in" msgstr "您的密码已过期,先修改再登录" @@ -1395,6 +1399,10 @@ msgstr "{} 天内自动登录" msgid "MFA code" msgstr "多因子认证验证码" +#: authentication/mixins.py:261 +msgid "Please change your password" +msgstr "请修改密码" + #: authentication/models.py:40 msgid "Private Token" msgstr "SSH密钥" @@ -1417,7 +1425,7 @@ msgstr "文档" #: authentication/templates/authentication/_access_key_modal.html:31 msgid "Secret" -msgstr "密文" +msgstr "秘钥" #: authentication/templates/authentication/_access_key_modal.html:33 msgid "Date" @@ -1467,7 +1475,6 @@ msgid "Need MFA for view auth" msgstr "需要多因子认证来查看账号信息" #: authentication/templates/authentication/_mfa_confirm_modal.html:20 -#: authentication/views/login.py:246 authentication/views/login.py:262 #: templates/_modal.html:23 users/templates/users/user_password_verify.html:20 msgid "Confirm" msgstr "确认" @@ -1503,11 +1510,6 @@ msgstr "OpenID" msgid "CAS" msgstr "" -#: authentication/templates/authentication/login.html:215 -#: notifications/models.py:18 -msgid "DingTalk" -msgstr "钉钉" - #: authentication/templates/authentication/login_otp.html:17 msgid "One-time password" msgstr "一次性密码" @@ -1538,7 +1540,6 @@ msgid "Copy link" msgstr "复制链接" #: authentication/templates/authentication/login_wait_confirm.html:51 -#: templates/flash_message_standalone.html:38 msgid "Return" msgstr "返回" @@ -1587,11 +1588,19 @@ msgstr "" msgid "Binding DingTalk failed" msgstr "" -#: authentication/views/login.py:55 +#: authentication/views/login.py:59 +msgid "Redirecting" +msgstr "" + +#: authentication/views/login.py:60 +msgid "Redirecting to {} authentication" +msgstr "" + +#: authentication/views/login.py:84 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:173 +#: authentication/views/login.py:202 msgid "" "Wait for {} confirm, You also can copy link to her/him
    \n" " Don't close this page" @@ -1599,23 +1608,18 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
    \n" " 不要关闭本页面" -#: authentication/views/login.py:178 +#: authentication/views/login.py:207 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:210 +#: authentication/views/login.py:239 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:211 +#: authentication/views/login.py:240 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/login.py:226 authentication/views/login.py:241 -#: authentication/views/login.py:257 -msgid "Please change your password" -msgstr "请修改密码" - #: authentication/views/wecom.py:75 msgid "WeCom is already bound" msgstr "" @@ -1764,7 +1768,7 @@ msgstr "JumpServer 开源堡垒机" msgid "

    Flow service unavailable, check it

    " msgstr "" -#: jumpserver/views/other.py:27 +#: jumpserver/views/other.py:25 msgid "" "
    Luna is a separately deployed program, you need to deploy Luna, koko, " "configure nginx for url distribution,
    If you see this page, " @@ -1773,11 +1777,11 @@ msgstr "" "
    Luna是单独部署的一个程序,你需要部署luna,koko,
    如果你看到了" "这个页面,证明你访问的不是nginx监听的端口,祝你好运
    " -#: jumpserver/views/other.py:71 +#: jumpserver/views/other.py:69 msgid "Websocket server run on port: {}, you should proxy it on nginx" msgstr "Websocket 服务运行在端口: {}, 请检查nginx是否代理是否设置" -#: jumpserver/views/other.py:85 +#: jumpserver/views/other.py:83 msgid "" "
    Koko is a separately deployed program, you need to deploy Koko, " "configure nginx for url distribution,
    If you see this page, " @@ -1787,20 +1791,6 @@ msgstr "" "div>
    如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运" -#: notifications/models.py:17 users/forms/profile.py:101 -#: users/models/user.py:557 -msgid "Email" -msgstr "邮件" - -#: notifications/models.py:67 templates/_nav.html:110 terminal/apps.py:9 -#: terminal/serializers/session.py:40 -msgid "Terminal" -msgstr "终端" - -#: notifications/models.py:68 ops/apps.py:9 -msgid "Operations" -msgstr "运维" - #: ops/api/celery.py:61 ops/api/celery.py:76 msgid "Waiting task start" msgstr "等待任务开始" @@ -1809,6 +1799,10 @@ msgstr "等待任务开始" msgid "Not has host {} permission" msgstr "没有该主机 {} 权限" +#: ops/apps.py:9 +msgid "Operations" +msgstr "运维" + #: ops/mixin.py:29 ops/mixin.py:92 ops/mixin.py:160 msgid "Cycle perform" msgstr "周期执行" @@ -1936,18 +1930,10 @@ msgstr "任务开始" msgid "Command `{}` is forbidden ........" msgstr "命令 `{}` 不允许被执行 ......." -#: ops/models/command.py:115 +#: ops/models/command.py:113 msgid "Task end" msgstr "任务结束" -#: ops/notifications.py:10 -msgid "Server performance" -msgstr "服务器性能" - -#: ops/notifications.py:17 -msgid "Disk used more than 80%: {} => {}" -msgstr "磁盘使用率超过 80%: {} => {}" - #: ops/tasks.py:71 msgid "Clean task history period" msgstr "定期清除任务历史" @@ -1964,6 +1950,10 @@ msgstr "任务列表" msgid "Update task content: {}" msgstr "更新任务内容: {}" +#: ops/utils.py:74 +msgid "Disk used more than 80%: {} => {}" +msgstr "磁盘使用率超过 80%: {} => {}" + #: orgs/api.py:76 #, python-brace-format msgid "Have `{model._meta.verbose_name}` exists, Please delete" @@ -1991,7 +1981,7 @@ msgstr "组织审计员" msgid "GLOBAL" msgstr "全局组织" -#: orgs/models.py:419 users/models/user.py:565 +#: orgs/models.py:419 users/models/user.py:568 #: users/templates/users/_select_user_modal.html:15 msgid "Role" msgstr "角色" @@ -2063,7 +2053,7 @@ msgid "Favorite" msgstr "收藏夹" #: perms/models/base.py:51 templates/_nav.html:21 users/models/group.py:31 -#: users/models/user.py:561 users/templates/users/_select_user_modal.html:16 +#: users/models/user.py:564 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 @@ -2076,7 +2066,7 @@ msgstr "用户组" #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:77 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:43 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:81 -#: users/models/user.py:593 +#: users/models/user.py:596 msgid "Date expired" msgstr "失效日期" @@ -2473,16 +2463,16 @@ msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" #: settings/serializers/settings.py:193 -msgid "Corporation ID" +msgid "Corporation ID(corpid)" msgstr "企业 ID(CorpId)" #: settings/serializers/settings.py:194 -msgid "Agent ID" +msgid "Agent ID(agentid)" msgstr "应用 ID(AgentId)" #: settings/serializers/settings.py:195 -msgid "Corporation Secret" -msgstr "凭证密钥(Secret)" +msgid "Secret(secret)" +msgstr "秘钥(secret)" #: settings/serializers/settings.py:196 msgid "Enable WeCom Auth" @@ -2810,6 +2800,11 @@ msgstr "Web终端" msgid "File manager" msgstr "文件管理" +#: templates/_nav.html:110 terminal/apps.py:9 +#: terminal/serializers/session.py:40 +msgid "Terminal" +msgstr "终端" + #: templates/_nav.html:121 msgid "Job Center" msgstr "作业中心" @@ -2895,6 +2890,14 @@ msgstr "确认删除" msgid "Are you sure delete" msgstr "您确定删除吗?" +#: templates/flash_message_standalone.html:28 +msgid "Cancel" +msgstr "取消" + +#: templates/flash_message_standalone.html:37 +msgid "Go" +msgstr "" + #: templates/index.html:11 msgid "Total users" msgstr "用户总数" @@ -3224,89 +3227,6 @@ msgstr "命令存储" msgid "Replay storage" msgstr "录像存储" -#: terminal/notifications.py:29 -msgid "Terminal command alert" -msgstr "终端命令告警" - -#: terminal/notifications.py:38 -#, python-format -msgid "" -"\n" -" Command: %(command)s\n" -"
    \n" -" Asset: %(host_name)s (%(host_ip)s)\n" -"
    \n" -" User: %(user)s\n" -"
    \n" -" Level: %(risk_level)s\n" -"
    \n" -" Session: session " -"detail\n" -"
    \n" -" " -msgstr "" -"\n" -" 命令: %(command)s\n" -"
    \n" -" 资产: %(host_name)s (%(host_ip)s)\n" -"
    \n" -" 用户: %(user)s\n" -"
    \n" -" 等级: %(risk_level)s\n" -"
    \n" -" 会话: 会话详情\n" -"
    \n" -" " - -#: terminal/notifications.py:73 -#, python-format -msgid "" -"Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $" -"%(command)s" -msgstr "危险命令告警: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s" - -#: terminal/notifications.py:90 -msgid "Batch command alert" -msgstr "批量命令告警" - -#: terminal/notifications.py:101 -#, python-format -msgid "" -"\n" -"
    \n" -" Assets: %(assets)s\n" -"
    \n" -" User: %(user)s\n" -"
    \n" -" Level: %(risk_level)s\n" -"
    \n" -"\n" -" ----------------- Commands ---------------- " -"
    \n" -" %(command)s
    \n" -" ----------------- Commands ---------------- " -"
    \n" -" " -msgstr "" -"\n" -"
    \n" -" 资产: %(assets)s\n" -"
    \n" -" 用户: %(user)s\n" -"
    \n" -" 等级: %(risk_level)s\n" -"
    \n" -"\n" -" ----------------- 命令 ----------------
    \n" -" %(command)s
    \n" -" ----------------- 命令 ----------------
    \n" -" " - -#: terminal/notifications.py:127 -#, python-format -msgid "Insecure Web Command Execution Alert: [%(name)s]" -msgstr "Web页面-> 命令执行 告警: [%(name)s]" - #: terminal/serializers/session.py:33 msgid "User ID" msgstr "用户 ID" @@ -3400,6 +3320,78 @@ msgstr "忽略证书认证" msgid "Not found" msgstr "没有发现" +#: terminal/utils.py:78 +#, python-format +msgid "" +"Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $" +"%(command)s" +msgstr "危险命令告警: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s" + +#: terminal/utils.py:86 +#, python-format +msgid "" +"\n" +" Command: %(command)s\n" +"
    \n" +" Asset: %(host_name)s (%(host_ip)s)\n" +"
    \n" +" User: %(user)s\n" +"
    \n" +" Level: %(risk_level)s\n" +"
    \n" +" Session: session detail\n" +"
    \n" +" " +msgstr "" +"\n" +" 命令: %(command)s\n" +"
    \n" +" 资产: %(host_name)s (%(host_ip)s)\n" +"
    \n" +" 用户: %(user)s\n" +"
    \n" +" 等级: %(risk_level)s\n" +"
    \n" +" 会话: 会话详情\n" +"
    \n" +" " + +#: terminal/utils.py:113 +#, python-format +msgid "Insecure Web Command Execution Alert: [%(name)s]" +msgstr "Web页面-> 命令执行 告警: [%(name)s]" + +#: terminal/utils.py:121 +#, python-format +msgid "" +"\n" +"
    \n" +" Assets: %(assets)s\n" +"
    \n" +" User: %(user)s\n" +"
    \n" +" Level: %(risk_level)s\n" +"
    \n" +"\n" +" ----------------- Commands ----------------
    \n" +" %(command)s
    \n" +" ----------------- Commands ----------------
    \n" +" " +msgstr "" +"\n" +"
    \n" +" 资产: %(assets)s\n" +"
    \n" +" 用户: %(user)s\n" +"
    \n" +" 等级: %(risk_level)s\n" +"
    \n" +"\n" +" ----------------- 命令 ----------------
    \n" +" %(command)s
    \n" +" ----------------- 命令 ----------------
    \n" +" " + #: tickets/const.py:8 msgid "General" msgstr "一般" @@ -3560,8 +3552,6 @@ msgid "Applied from command filter rules" msgstr "申请来自命令过滤规则" #: tickets/handler/command_confirm.py:30 -#, fuzzy -#| msgid "Applied from command filter rules" msgid "Applied from command filter" msgstr "申请来自命令过滤规则" @@ -3727,8 +3717,6 @@ msgid "From cmd filter rule" msgstr "来自命令过滤规则" #: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:22 -#, fuzzy -#| msgid "From cmd filter rule" msgid "From cmd filter" msgstr "来自命令过滤规则" @@ -3836,6 +3824,10 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" +#: users/forms/profile.py:101 users/models/user.py:560 +msgid "Email" +msgstr "邮件" + #: users/forms/profile.py:108 msgid "Old password" msgstr "原来密码" @@ -3885,27 +3877,27 @@ msgstr "强制启用" msgid "Local" msgstr "数据库" -#: users/models/user.py:568 +#: users/models/user.py:571 msgid "Avatar" msgstr "头像" -#: users/models/user.py:571 +#: users/models/user.py:574 msgid "Wechat" msgstr "微信" -#: users/models/user.py:601 +#: users/models/user.py:604 msgid "Source" msgstr "用户来源" -#: users/models/user.py:605 +#: users/models/user.py:608 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:751 +#: users/models/user.py:754 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:754 +#: users/models/user.py:757 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -4293,7 +4285,7 @@ msgstr "" "
    \n" " " -#: users/utils.py:116 users/views/profile/reset.py:80 +#: users/utils.py:116 users/views/profile/reset.py:126 msgid "Reset password success" msgstr "重置密码成功" @@ -4522,7 +4514,7 @@ msgstr "多因子认证禁用成功,返回登录页面" msgid "Password update" msgstr "密码更新" -#: users/views/profile/password.py:60 users/views/profile/reset.py:127 +#: users/views/profile/password.py:60 users/views/profile/reset.py:105 msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" @@ -4534,41 +4526,41 @@ msgstr "用户名或密码无效" msgid "Public key update" msgstr "密钥更新" -#: users/views/profile/reset.py:46 +#: users/views/profile/reset.py:40 +msgid "Send reset password message" +msgstr "发送重置密码邮件" + +#: users/views/profile/reset.py:41 +msgid "Send reset password mail success, login your mail box and follow it " +msgstr "" +"发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)" + +#: users/views/profile/reset.py:52 msgid "Email address invalid, please input again" msgstr "邮箱地址错误,重新输入" -#: users/views/profile/reset.py:52 +#: users/views/profile/reset.py:58 msgid "" "The user is from {}, please go to the corresponding system to change the " "password" msgstr "用户来自 {} 请去相应系统修改密码" -#: users/views/profile/reset.py:66 -msgid "Send reset password message" -msgstr "发送重置密码邮件" - -#: users/views/profile/reset.py:67 -msgid "Send reset password mail success, login your mail box and follow it " -msgstr "" -"发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)" - -#: users/views/profile/reset.py:81 -msgid "Reset password success, return to login page" -msgstr "重置密码成功,返回到登录页面" - -#: users/views/profile/reset.py:105 users/views/profile/reset.py:115 +#: users/views/profile/reset.py:83 users/views/profile/reset.py:93 msgid "Token invalid or expired" msgstr "Token错误或失效" -#: users/views/profile/reset.py:120 +#: users/views/profile/reset.py:98 msgid "User auth from {}, go there change password" msgstr "用户认证源来自 {}, 请去相应系统修改密码" -#: users/views/profile/reset.py:133 +#: users/views/profile/reset.py:111 msgid "* The new password cannot be the last {} passwords" msgstr "* 新密码不能是最近 {} 次的密码" +#: users/views/profile/reset.py:127 +msgid "Reset password success, return to login page" +msgstr "重置密码成功,返回到登录页面" + #: xpack/plugins/change_auth_plan/meta.py:9 #: xpack/plugins/change_auth_plan/models.py:89 #: xpack/plugins/change_auth_plan/models.py:184 @@ -5045,20 +5037,35 @@ msgstr "旗舰版" msgid "Community edition" msgstr "社区版" +#, fuzzy +#~| msgid "Agent Secret" +#~ msgid "WeCom Secret" +#~ msgstr "凭证密钥(secret)" + +#~ msgid "Server performance" +#~ msgstr "服务器性能" + +#~ msgid "Corporation Secret" +#~ msgstr "凭证密钥(Secret)" + +#~ msgid "Terminal command alert" +#~ msgstr "终端命令告警" + #, python-format #~ msgid "" #~ "\n" -#~ " Command: %(command)s\n" -#~ "
    \n" -#~ " Asset: %(host_name)s (%(host_ip)s)\n" -#~ "
    \n" -#~ " User: %(user)s\n" -#~ "
    \n" -#~ " Level: %(risk_level)s\n" -#~ "
    \n" -#~ " Session: session detail\n" -#~ "
    \n" -#~ " " +#~ " Command: %(command)s\n" +#~ "
    \n" +#~ " Asset: %(host_name)s (%(host_ip)s)\n" +#~ "
    \n" +#~ " User: %(user)s\n" +#~ "
    \n" +#~ " Level: %(risk_level)s\n" +#~ "
    \n" +#~ " Session: session detail\n" +#~ "
    \n" +#~ " " #~ msgstr "" #~ "\n" #~ " 命令: %(command)s\n" @@ -5073,21 +5080,26 @@ msgstr "社区版" #~ "
    \n" #~ " " +#~ msgid "Batch command alert" +#~ msgstr "批量命令告警" + #, python-format #~ msgid "" #~ "\n" -#~ "
    \n" -#~ " Assets: %(assets)s\n" -#~ "
    \n" -#~ " User: %(user)s\n" -#~ "
    \n" -#~ " Level: %(risk_level)s\n" -#~ "
    \n" +#~ "
    \n" +#~ " Assets: %(assets)s\n" +#~ "
    \n" +#~ " User: %(user)s\n" +#~ "
    \n" +#~ " Level: %(risk_level)s\n" +#~ "
    \n" #~ "\n" -#~ " ----------------- Commands ----------------
    \n" -#~ " %(command)s
    \n" -#~ " ----------------- Commands ----------------
    \n" -#~ " " +#~ " ----------------- Commands ---------------- " +#~ "
    \n" +#~ " %(command)s
    \n" +#~ " ----------------- Commands ---------------- " +#~ "
    \n" +#~ " " #~ msgstr "" #~ "\n" #~ "
    \n" @@ -5109,9 +5121,6 @@ msgstr "社区版" #~ msgid "Command Alert" #~ msgstr "命令告警" -#~ msgid "Agent Secret" -#~ msgstr "凭证密钥(secret)" - #~ msgid "APP key" #~ msgstr "APPKEY" @@ -5190,9 +5199,6 @@ msgstr "社区版" #~ msgid "This will reset the user password and send a reset mail" #~ msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" -#~ msgid "Cancel" -#~ msgstr "取消" - #~ msgid "" #~ "The reset-ssh-public-key E-mail has been sent successfully. Please inform " #~ "the user to update his new ssh public key." diff --git a/apps/settings/api/wecom.py b/apps/settings/api/wecom.py index 0fda33c61..3087efd1f 100644 --- a/apps/settings/api/wecom.py +++ b/apps/settings/api/wecom.py @@ -20,7 +20,7 @@ class WeComTestingAPI(GenericAPIView): wecom_corpid = serializer.validated_data['WECOM_CORPID'] wecom_agentid = serializer.validated_data['WECOM_AGENTID'] - wecom_corpsecret = serializer.validated_data['WECOM_CORPSECRET'] + wecom_corpsecret = serializer.validated_data['WECOM_SECRET'] try: params = {'corpid': wecom_corpid, 'corpsecret': wecom_corpsecret} diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index 5d33a1d83..3aca30c10 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -190,9 +190,9 @@ class SecuritySettingSerializer(serializers.Serializer): class WeComSettingSerializer(serializers.Serializer): - WECOM_CORPID = serializers.CharField(max_length=256, required=True, label=_('Corporation ID')) - WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label=_("Agent ID")) - WECOM_CORPSECRET = serializers.CharField(max_length=256, required=False, label=_("Corporation Secret"), write_only=True) + 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) AUTH_WECOM = serializers.BooleanField(default=False, label=_('Enable WeCom Auth')) diff --git a/apps/static/img/login_dingtalk_log.png b/apps/static/img/login_dingtalk_log.png index 998f730ad056a13c1e9b06036be3851055524550..61ef4e4d4f016223e8780806b8fb36cc48383870 100644 GIT binary patch delta 1391 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 Date: Thu, 13 May 2021 19:08:35 +0800 Subject: [PATCH 39/39] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E7=94=9F=E6=88=90=E5=85=AC=E9=92=A5=E4=BC=98=E5=85=88?= =?UTF-8?q?=E4=BD=BF=E7=94=A8dss=E6=A0=BC=E5=BC=8F=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98(=E9=BB=98=E8=AE=A4=E4=BC=98=E5=85=88=E4=BD=BF?= =?UTF-8?q?=E7=94=A8rsa)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/common/utils/encode.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/common/utils/encode.py b/apps/common/utils/encode.py index cd130e7fe..dc68c9ba8 100644 --- a/apps/common/utils/encode.py +++ b/apps/common/utils/encode.py @@ -75,11 +75,16 @@ def ssh_key_string_to_obj(text, password=None): key = paramiko.RSAKey.from_private_key(StringIO(text), password=password) except paramiko.SSHException: pass + else: + return key try: key = paramiko.DSSKey.from_private_key(StringIO(text), password=password) except paramiko.SSHException: pass + else: + return key + return key