diff --git a/apps/authentication/api/mfa.py b/apps/authentication/api/mfa.py index f067d5c5c..978c52072 100644 --- a/apps/authentication/api/mfa.py +++ b/apps/authentication/api/mfa.py @@ -2,17 +2,17 @@ # import builtins import time + from django.utils.translation import ugettext as _ from django.conf import settings +from django.shortcuts import get_object_or_404 from rest_framework.permissions import AllowAny from rest_framework.generics import CreateAPIView from rest_framework.serializers import ValidationError from rest_framework.response import Response -from authentication.sms_verify_code import VerifyCodeUtil -from common.exceptions import JMSException -from common.permissions import IsValidUser, NeedMFAVerify, IsAppUser -from users.models.user import MFAType +from common.permissions import IsValidUser, NeedMFAVerify +from users.models.user import MFAType, User from ..serializers import OtpVerifySerializer from .. import serializers from .. import errors @@ -90,6 +90,13 @@ class SendSMSVerifyCodeApi(AuthMixin, CreateAPIView): permission_classes = (AllowAny,) def create(self, request, *args, **kwargs): - user = self.get_user_from_session() + username = request.data.get('username', '') + username = username.strip() + if username: + user = get_object_or_404(User, username=username) + else: + user = self.get_user_from_session() + if not user.mfa_enabled: + raise errors.NotEnableMFAError timeout = user.send_sms_code() - return Response({'code': 'ok','timeout': timeout}) + return Response({'code': 'ok', 'timeout': timeout}) diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index 5844eb777..ca190fe2b 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -78,6 +78,7 @@ mfa_type_failed_msg = _( mfa_required_msg = _("MFA required") mfa_unset_msg = _("MFA not set, please set it first") +otp_unset_msg = _("OTP not set, please set it first") login_confirm_required_msg = _("Login confirm required") login_confirm_wait_msg = _("Wait login confirm ticket for accept") login_confirm_error_msg = _("Login confirm ticket was {}") @@ -354,3 +355,15 @@ class NotHaveUpDownLoadPerm(JMSException): status_code = status.HTTP_403_FORBIDDEN code = 'not_have_up_down_load_perm' default_detail = _('No upload or download permission') + + +class NotEnableMFAError(JMSException): + default_detail = mfa_unset_msg + + +class OTPRequiredError(JMSException): + default_detail = otp_unset_msg + + def __init__(self, url, *args, **kwargs): + super().__init__(*args, **kwargs) + self.url = url diff --git a/apps/authentication/forms.py b/apps/authentication/forms.py index d4c1375e6..d8533536d 100644 --- a/apps/authentication/forms.py +++ b/apps/authentication/forms.py @@ -43,7 +43,7 @@ class UserLoginForm(forms.Form): class UserCheckOtpCodeForm(forms.Form): - code = forms.CharField(label=_('MFA Code'), max_length=6) + code = forms.CharField(label=_('MFA Code'), max_length=6, required=False) mfa_type = forms.CharField(label=_('MFA type'), max_length=6) @@ -59,7 +59,7 @@ class ChallengeMixin(forms.Form): challenge = forms.CharField( label=_('MFA code'), max_length=6, required=False, widget=forms.TextInput(attrs={ - 'placeholder': _("MFA code"), + 'placeholder': _("Dynamic code"), 'style': 'width: 50%' }) ) @@ -69,6 +69,8 @@ def get_user_login_form_cls(*, captcha=False): bases = [] if settings.SECURITY_LOGIN_CHALLENGE_ENABLED: bases.append(ChallengeMixin) + elif settings.SECURITY_MFA_IN_LOGIN_PAGE: + bases.append(UserCheckOtpCodeForm) elif settings.SECURITY_LOGIN_CAPTCHA_ENABLED and captcha: bases.append(CaptchaMixin) bases.append(UserLoginForm) diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 07beee3e7..a32ee6b81 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -7,6 +7,7 @@ import time from django.core.cache import cache from django.conf import settings +from django.urls import reverse_lazy from django.contrib import auth from django.utils.translation import ugettext as _ from django.contrib.auth import ( @@ -14,11 +15,11 @@ from django.contrib.auth import ( PermissionDenied, user_login_failed, _clean_credentials ) from django.shortcuts import reverse, redirect -from django.views.generic.edit import FormView from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil from users.models import User, MFAType from users.utils import LoginBlockUtil, MFABlockUtils +from users.exceptions import MFANotEnabled from . import errors from .utils import rsa_decrypt, gen_key_pair from .signals import post_auth_success, post_auth_failed @@ -208,18 +209,20 @@ class AuthMixin(PasswordEncryptionViewMixin): ip = self.get_request_ip() self._set_partial_credential_error(username=username, ip=ip, request=request) - password = password + challenge.strip() if decrypt_passwd: password = self.get_decrypted_password() + password = password + challenge.strip() return username, password, public_key, ip, auto_login def _check_only_allow_exists_user_auth(self, username): # 仅允许预先存在的用户认证 - if settings.ONLY_ALLOW_EXIST_USER_AUTH: - exist = User.objects.filter(username=username).exists() - if not exist: - logger.error(f"Only allow exist user auth, login failed: {username}") - self.raise_credential_error(errors.reason_user_not_exist) + if not settings.ONLY_ALLOW_EXIST_USER_AUTH: + return + + exist = User.objects.filter(username=username).exists() + if not exist: + logger.error(f"Only allow exist user auth, login failed: {username}") + self.raise_credential_error(errors.reason_user_not_exist) def _check_auth_user_is_valid(self, username, password, public_key): user = authenticate(self.request, username=username, password=password, public_key=public_key) @@ -231,6 +234,17 @@ class AuthMixin(PasswordEncryptionViewMixin): self.raise_credential_error(errors.reason_user_inactive) return user + def _check_login_mfa_login_if_need(self, user): + request = self.request + if hasattr(request, 'data'): + data = request.data + else: + data = request.POST + code = data.get('code') + mfa_type = data.get('mfa_type') + if settings.SECURITY_MFA_IN_LOGIN_PAGE and code and mfa_type: + self.check_user_mfa(code, mfa_type, user=user) + def _check_login_acl(self, user, ip): # ACL 限制用户登录 from acls.models import LoginACL @@ -255,8 +269,7 @@ class AuthMixin(PasswordEncryptionViewMixin): def check_user_auth(self, decrypt_passwd=False): self.check_is_block() - request = self.request - username, password, public_key, ip, auto_login = self.get_auth_data(decrypt_passwd=decrypt_passwd) + username, password, public_key, ip, auto_login = self.get_auth_data(decrypt_passwd) self._check_only_allow_exists_user_auth(username) user = self._check_auth_user_is_valid(username, password, public_key) @@ -266,7 +279,11 @@ class AuthMixin(PasswordEncryptionViewMixin): self._check_passwd_is_too_simple(user, password) self._check_passwd_need_update(user) + # 校验login-mfa, 如果登录页面上显示 mfa 的话 + self._check_login_mfa_login_if_need(user) + LoginBlockUtil(username, ip).clean_failed_count() + request = self.request request.session['auth_password'] = 1 request.session['user_id'] = str(user.id) request.session['auto_login'] = auto_login @@ -348,12 +365,11 @@ class AuthMixin(PasswordEncryptionViewMixin): def check_user_mfa_if_need(self, user): if self.request.session.get('auth_mfa'): return - if settings.OTP_IN_RADIUS: return - if not user.mfa_enabled: return + unset, url = user.mfa_enabled_but_not_set() if unset: raise errors.MFAUnsetError(user, self.request, url) @@ -372,19 +388,29 @@ class AuthMixin(PasswordEncryptionViewMixin): self.request.session['auth_mfa_type'] = '' def check_mfa_is_block(self, username, ip, raise_exception=True): - if MFABlockUtils(username, ip).is_block(): - logger.warn('Ip was blocked' + ': ' + username + ':' + ip) - exception = errors.BlockMFAError(username=username, request=self.request, ip=ip) - if raise_exception: - raise exception - else: - return exception + blocked = MFABlockUtils(username, ip).is_block() + if not blocked: + return + logger.warn('Ip was blocked' + ': ' + username + ':' + ip) + exception = errors.BlockMFAError(username=username, request=self.request, ip=ip) + if raise_exception: + raise exception + else: + return exception + + def check_user_mfa(self, code, mfa_type=MFAType.OTP, user=None): + user = user if user else self.get_user_from_session() + if not user.mfa_enabled: + return True + + if not (bool(user.otp_secret_key) and mfa_type == MFAType.OTP): + self.set_passwd_verify_on_session(user) + raise errors.OTPRequiredError(reverse_lazy('authentication:user-otp-enable-bind')) - def check_user_mfa(self, code, mfa_type=MFAType.OTP): - user = self.get_user_from_session() ip = self.get_request_ip() self.check_mfa_is_block(user.username, ip) ok = user.check_mfa(code, mfa_type=mfa_type) + if ok: self.mark_mfa_ok() return @@ -468,3 +494,31 @@ class AuthMixin(PasswordEncryptionViewMixin): if args: guard_url = "%s?%s" % (guard_url, args) return redirect(guard_url) + + @staticmethod + def get_user_mfa_methods(user=None): + otp_enabled = user.otp_secret_key if user else True + # 没有用户时,或者有用户并且有电话配置 + sms_enabled = any([user and user.phone, not user]) \ + and settings.SMS_ENABLED and settings.XPACK_ENABLED + + methods = [ + { + 'name': 'otp', + 'label': 'MFA', + 'enable': otp_enabled, + 'selected': False, + }, + { + 'name': 'sms', + 'label': _('SMS'), + 'enable': sms_enabled, + 'selected': False, + }, + ] + + for item in methods: + if item['enable']: + item['selected'] = True + break + return methods diff --git a/apps/authentication/models.py b/apps/authentication/models.py index 62bd74392..6e7b59e54 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -1,7 +1,7 @@ import uuid from django.utils import timezone -from django.utils.translation import ugettext_lazy as _, ugettext as __ +from django.utils.translation import ugettext_lazy as _ from rest_framework.authtoken.models import Token from django.conf import settings diff --git a/apps/authentication/sms_verify_code.py b/apps/authentication/sms_verify_code.py index 0c3e986ce..4236d9c0d 100644 --- a/apps/authentication/sms_verify_code.py +++ b/apps/authentication/sms_verify_code.py @@ -1,10 +1,8 @@ import random -from django.conf import settings from django.core.cache import cache from django.utils.translation import gettext_lazy as _ -from common.message.backends.sms.alibaba import AlibabaSMS from common.message.backends.sms import SMS from common.utils import get_logger from common.exceptions import JMSException diff --git a/apps/authentication/templates/authentication/_captcha_field.html b/apps/authentication/templates/authentication/_captcha_field.html index a190aacb7..0aa3fc260 100644 --- a/apps/authentication/templates/authentication/_captcha_field.html +++ b/apps/authentication/templates/authentication/_captcha_field.html @@ -5,9 +5,9 @@
{% if audio %} - + {% endif %} -
+
{% include "django/forms/widgets/multiwidget.html" %} diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html index 08591892b..f968e325c 100644 --- a/apps/authentication/templates/authentication/login.html +++ b/apps/authentication/templates/authentication/login.html @@ -10,23 +10,15 @@ {{ JMS_TITLE }} + {% include '_head_css_js.html' %} - - - - - - - - - - + -
+
{% if form.challenge %} {% bootstrap_field form.challenge show_label=False %} + {% elif form.mfa_type %} +
+ {% include '_mfa_otp_login.html' %} +
{% elif form.captcha %}
{% bootstrap_field form.captcha show_label=False %} @@ -233,7 +225,7 @@ var password = $('#password').val(); //明文密码 var passwordEncrypted = encryptLoginPassword(password, rsaPublicKey) $('#password-hidden').val(passwordEncrypted); //返回给密码输入input - $('#login-form').submit();//post提交 + $('#login-form').submit(); //post提交 } $(document).ready(function () { diff --git a/apps/authentication/templates/authentication/login_otp.html b/apps/authentication/templates/authentication/login_otp.html index ff4d4e28b..fbd674e5e 100644 --- a/apps/authentication/templates/authentication/login_otp.html +++ b/apps/authentication/templates/authentication/login_otp.html @@ -13,78 +13,19 @@

{{ form.code.errors.as_text }}

{% endif %}
- + {% include '_mfa_otp_login.html' %}
-
- - - - -
- - +
{% trans "Can't provide security? Please contact the administrator!" %}
{% endblock %} diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index 2dea0da7b..2fb66496b 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -13,7 +13,6 @@ 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'), diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 37ede915a..257c555bf 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -29,7 +29,6 @@ from ..const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY from .. import mixins, errors from ..forms import get_user_login_form_cls - __all__ = [ 'UserLoginView', 'UserLogoutView', 'UserLoginGuardView', 'UserLoginWaitConfirmView', @@ -117,7 +116,6 @@ class UserLoginView(mixins.AuthMixin, FormView): except errors.AuthFailedError as e: form.add_error(None, e.msg) 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 @@ -125,11 +123,20 @@ class UserLoginView(mixins.AuthMixin, FormView): self.request.session.set_test_cookie() return self.render_to_response(context) except ( - errors.PasswdTooSimple, - errors.PasswordRequireResetError, - errors.PasswdNeedUpdate + errors.PasswdTooSimple, + errors.PasswordRequireResetError, + errors.PasswdNeedUpdate ) as e: return redirect(e.url) + except ( + errors.MFAUnsetError, + errors.MFAFailedError, + errors.BlockMFAError + ) as e: + form.add_error('code', e.msg) + return super().form_invalid(form) + except errors.OTPRequiredError as e: + return redirect(e.url) self.clear_rsa_key() return self.redirect_to_guard_view() @@ -150,31 +157,31 @@ class UserLoginView(mixins.AuthMixin, FormView): 'name': 'OpenID', 'enabled': settings.AUTH_OPENID, 'url': reverse('authentication:openid:login'), - 'logo': static('img/login_oidc_logo.png') + 'logo': static('img/login_oidc_logo.png') }, { 'name': 'CAS', 'enabled': settings.AUTH_CAS, 'url': reverse('authentication:cas:cas-login'), - 'logo': static('img/login_cas_logo.png') + 'logo': static('img/login_cas_logo.png') }, { 'name': _('WeCom'), 'enabled': settings.AUTH_WECOM, 'url': reverse('authentication:wecom-qr-login'), - 'logo': static('img/login_wecom_logo.png') + 'logo': static('img/login_wecom_logo.png') }, { 'name': _('DingTalk'), 'enabled': settings.AUTH_DINGTALK, 'url': reverse('authentication:dingtalk-qr-login'), - 'logo': static('img/login_dingtalk_logo.png') + 'logo': static('img/login_dingtalk_logo.png') }, { 'name': _('FeiShu'), 'enabled': settings.AUTH_FEISHU, 'url': reverse('authentication:feishu-qr-login'), - 'logo': static('img/login_feishu_logo.png') + 'logo': static('img/login_feishu_logo.png') } ] return [method for method in auth_methods if method['enabled']] @@ -191,7 +198,8 @@ class UserLoginView(mixins.AuthMixin, FormView): context = { 'demo_mode': os.environ.get("DEMO_MODE"), 'auth_methods': self.get_support_auth_methods(), - 'forgot_password_url': self.get_forgot_password_url() + 'forgot_password_url': self.get_forgot_password_url(), + 'methods': self.get_user_mfa_methods(), } kwargs.update(context) return super().get_context_data(**kwargs) diff --git a/apps/authentication/views/mfa.py b/apps/authentication/views/mfa.py index 758c3c62f..3984c8cd4 100644 --- a/apps/authentication/views/mfa.py +++ b/apps/authentication/views/mfa.py @@ -20,11 +20,11 @@ class UserLoginOtpView(mixins.AuthMixin, FormView): redirect_field_name = 'next' def form_valid(self, form): - otp_code = form.cleaned_data.get('code') + code = form.cleaned_data.get('code') mfa_type = form.cleaned_data.get('mfa_type') try: - self.check_user_mfa(otp_code, mfa_type) + self.check_user_mfa(code, mfa_type) return redirect_to_guard_view() except (errors.MFAFailedError, errors.BlockMFAError) as e: form.add_error('code', e.msg) @@ -37,26 +37,6 @@ class UserLoginOtpView(mixins.AuthMixin, FormView): def get_context_data(self, **kwargs): user = self.get_user_from_session() - context = { - 'methods': [ - { - 'name': 'otp', - 'label': _('One-time password'), - 'enable': bool(user.otp_secret_key), - 'selected': False, - }, - { - 'name': 'sms', - 'label': _('SMS'), - 'enable': bool(user.phone) and settings.SMS_ENABLED and settings.XPACK_ENABLED, - 'selected': False, - }, - ] - } - - for item in context['methods']: - if item['enable']: - item['selected'] = True - break - context.update(kwargs) - return context + methods = self.get_user_mfa_methods(user) + kwargs.update({'methods': methods}) + return kwargs diff --git a/apps/common/message/backends/sms/tencent.py b/apps/common/message/backends/sms/tencent.py index 40fcad9dc..6ba89a2a9 100644 --- a/apps/common/message/backends/sms/tencent.py +++ b/apps/common/message/backends/sms/tencent.py @@ -1,4 +1,3 @@ -import json from collections import OrderedDict from django.conf import settings diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 75143217e..2744727a0 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -295,6 +295,7 @@ class Config(dict): 'SECURITY_PASSWORD_LOWER_CASE': False, 'SECURITY_PASSWORD_NUMBER': False, 'SECURITY_PASSWORD_SPECIAL_CHAR': False, + 'SECURITY_MFA_IN_LOGIN_PAGE': False, 'SECURITY_LOGIN_CHALLENGE_ENABLED': False, 'SECURITY_LOGIN_CAPTCHA_ENABLED': True, 'SECURITY_INSECURE_COMMAND': False, diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index eab2c05ff..b01e976f1 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -55,6 +55,7 @@ SECURITY_MFA_VERIFY_TTL = CONFIG.SECURITY_MFA_VERIFY_TTL SECURITY_VIEW_AUTH_NEED_MFA = CONFIG.SECURITY_VIEW_AUTH_NEED_MFA SECURITY_SERVICE_ACCOUNT_REGISTRATION = CONFIG.SECURITY_SERVICE_ACCOUNT_REGISTRATION SECURITY_LOGIN_CAPTCHA_ENABLED = CONFIG.SECURITY_LOGIN_CAPTCHA_ENABLED +SECURITY_MFA_IN_LOGIN_PAGE = CONFIG.SECURITY_MFA_IN_LOGIN_PAGE SECURITY_LOGIN_CHALLENGE_ENABLED = CONFIG.SECURITY_LOGIN_CHALLENGE_ENABLED SECURITY_DATA_CRYPTO_ALGO = CONFIG.SECURITY_DATA_CRYPTO_ALGO SECURITY_INSECURE_COMMAND = CONFIG.SECURITY_INSECURE_COMMAND diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 644e2c92d..45e169fc9 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-09-29 14:51+0800\n" +"POT-Creation-Date: 2021-10-18 17:36+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -25,7 +25,7 @@ msgstr "" #: orgs/models.py:24 perms/models/base.py:44 settings/models.py:29 #: settings/serializers/sms.py:6 terminal/models/storage.py:23 #: terminal/models/task.py:16 terminal/models/terminal.py:100 -#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:605 +#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:604 #: users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 @@ -60,7 +60,7 @@ msgstr "激活中" #: orgs/models.py:27 perms/models/base.py:53 settings/models.py:34 #: terminal/models/storage.py:26 terminal/models/terminal.py:114 #: tickets/models/ticket.py:71 users/models/group.py:16 -#: users/models/user.py:638 xpack/plugins/change_auth_plan/models/base.py:41 +#: users/models/user.py:637 xpack/plugins/change_auth_plan/models/base.py:41 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:113 #: xpack/plugins/gathered_user/models.py:26 msgid "Comment" @@ -97,8 +97,8 @@ msgstr "动作" #: perms/models/base.py:45 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/const.py:14 users/models/user.py:181 -#: users/models/user.py:814 users/models/user.py:840 +#: tickets/models/comment.py:17 users/const.py:14 users/models/user.py:180 +#: users/models/user.py:813 users/models/user.py:839 #: users/serializers/group.py:19 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -177,7 +177,7 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " #: applications/serializers/attrs/application_type/vmware_client.py:26 #: assets/models/base.py:176 assets/models/gathered_user.py:15 #: audits/models.py:105 authentication/forms.py:15 authentication/forms.py:17 -#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:603 +#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:602 #: users/templates/users/_select_user_modal.html:14 #: xpack/plugins/change_auth_plan/models/asset.py:35 #: xpack/plugins/change_auth_plan/models/asset.py:191 @@ -377,7 +377,7 @@ msgstr "目标URL" #: applications/serializers/attrs/application_type/vmware_client.py:30 #: assets/models/base.py:177 audits/signals_handler.py:65 #: authentication/forms.py:22 -#: authentication/templates/authentication/login.html:163 +#: authentication/templates/authentication/login.html:151 #: settings/serializers/auth/ldap.py:44 users/forms/profile.py:21 #: users/templates/users/user_otp_check_password.html:13 #: users/templates/users/user_password_update.html:43 @@ -531,7 +531,7 @@ msgstr "标签管理" #: assets/models/cluster.py:28 assets/models/cmd_filter.py:26 #: assets/models/cmd_filter.py:67 assets/models/group.py:21 #: common/db/models.py:70 common/mixins/models.py:49 orgs/models.py:25 -#: orgs/models.py:437 perms/models/base.py:51 users/models/user.py:646 +#: orgs/models.py:437 perms/models/base.py:51 users/models/user.py:645 #: users/serializers/group.py:33 #: xpack/plugins/change_auth_plan/models/base.py:45 #: xpack/plugins/cloud/models.py:119 xpack/plugins/gathered_user/models.py:30 @@ -544,7 +544,7 @@ msgstr "创建者" #: assets/models/label.py:25 common/db/models.py:72 common/mixins/models.py:50 #: ops/models/adhoc.py:38 ops/models/command.py:29 orgs/models.py:26 #: orgs/models.py:435 perms/models/base.py:52 users/models/group.py:18 -#: users/models/user.py:841 xpack/plugins/cloud/models.py:122 +#: users/models/user.py:840 xpack/plugins/cloud/models.py:122 msgid "Date created" msgstr "创建日期" @@ -599,7 +599,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:624 +#: assets/models/cluster.py:22 users/models/user.py:623 msgid "Phone" msgstr "手机" @@ -625,7 +625,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:826 +#: users/models/user.py:825 msgid "System" msgstr "系统" @@ -1161,7 +1161,7 @@ msgstr "用户代理" #: audits/models.py:110 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/login_otp.html:6 -#: users/forms/profile.py:64 users/models/user.py:627 +#: users/forms/profile.py:64 users/models/user.py:626 #: users/serializers/profile.py:102 msgid "MFA" msgstr "多因子认证" @@ -1239,13 +1239,13 @@ msgstr "" msgid "Auth Token" msgstr "认证令牌" -#: audits/signals_handler.py:68 authentication/views/login.py:160 -#: notifications/backends/__init__.py:11 users/models/user.py:660 +#: audits/signals_handler.py:68 authentication/views/login.py:169 +#: notifications/backends/__init__.py:11 users/models/user.py:659 msgid "WeCom" msgstr "企业微信" -#: audits/signals_handler.py:69 authentication/views/login.py:166 -#: notifications/backends/__init__.py:12 users/models/user.py:661 +#: audits/signals_handler.py:69 authentication/views/login.py:175 +#: notifications/backends/__init__.py:12 users/models/user.py:660 msgid "DingTalk" msgstr "钉钉" @@ -1432,7 +1432,7 @@ msgstr "{ApplicationPermission} 添加 {SystemUser}" msgid "{ApplicationPermission} REMOVE {SystemUser}" msgstr "{ApplicationPermission} 移除 {SystemUser}" -#: authentication/api/connection_token.py:227 +#: authentication/api/connection_token.py:235 msgid "Invalid token" msgstr "无效的令牌" @@ -1596,41 +1596,51 @@ msgid "MFA not set, please set it first" msgstr "多因子认证没有设置,请先完成设置" #: authentication/errors.py:81 +msgid "OTP not set, please set it first" +msgstr "OTP认证没有设置,请先完成设置" + +#: authentication/errors.py:82 msgid "Login confirm required" msgstr "需要登录复核" -#: authentication/errors.py:82 +#: authentication/errors.py:83 msgid "Wait login confirm ticket for accept" msgstr "等待登录复核处理" -#: authentication/errors.py:83 +#: authentication/errors.py:84 msgid "Login confirm ticket was {}" msgstr "登录复核 {}" -#: authentication/errors.py:260 +#: authentication/errors.py:261 msgid "IP is not allowed" msgstr "来源 IP 不被允许登录" -#: authentication/errors.py:293 +#: authentication/errors.py:294 msgid "SSO auth closed" msgstr "SSO 认证关闭了" -#: authentication/errors.py:298 authentication/mixins.py:321 +#: authentication/errors.py:299 authentication/mixins.py:338 msgid "Your password is too simple, please change it for security" msgstr "你的密码过于简单,为了安全,请修改" -#: authentication/errors.py:307 authentication/mixins.py:328 +#: authentication/errors.py:308 authentication/mixins.py:345 msgid "You should to change your password before login" msgstr "登录完成前,请先修改密码" -#: authentication/errors.py:316 authentication/mixins.py:335 +#: authentication/errors.py:317 authentication/mixins.py:352 msgid "Your password has expired, please reset before logging in" msgstr "您的密码已过期,先修改再登录" -#: authentication/errors.py:350 +#: authentication/errors.py:351 msgid "Your password is invalid" msgstr "您的密码无效" +#: authentication/errors.py:357 +#, fuzzy +#| msgid "Not has host {} permission" +msgid "No upload or download permission" +msgstr "没有该主机 {} 权限" + #: authentication/forms.py:35 msgid "{} days auto login" msgstr "{} 天内自动登录" @@ -1643,12 +1653,15 @@ msgstr "MFA 验证码" msgid "MFA type" msgstr "MFA 类型" -#: authentication/forms.py:60 authentication/forms.py:62 -#: users/forms/profile.py:27 +#: authentication/forms.py:60 users/forms/profile.py:27 msgid "MFA code" msgstr "多因子认证验证码" -#: authentication/mixins.py:311 +#: authentication/forms.py:62 +msgid "Dynamic code" +msgstr "动态码" + +#: authentication/mixins.py:328 msgid "Please change your password" msgstr "请修改密码" @@ -1656,7 +1669,7 @@ msgstr "请修改密码" msgid "Private Token" msgstr "SSH密钥" -#: authentication/models.py:49 settings/serializers/security.py:118 +#: authentication/models.py:49 settings/serializers/security.py:144 msgid "Login Confirm" msgstr "登录复核" @@ -1709,15 +1722,15 @@ msgstr "" "若怀疑此次登录行为异常,请及时修改账号密码。\n" "感谢您对{server_name}的关注!\n" -#: authentication/sms_verify_code.py:17 +#: authentication/sms_verify_code.py:15 msgid "The verification code has expired. Please resend it" msgstr "验证码已过期,请重新发送" -#: authentication/sms_verify_code.py:22 +#: authentication/sms_verify_code.py:20 msgid "The verification code is incorrect" msgstr "验证码错误" -#: authentication/sms_verify_code.py:27 +#: authentication/sms_verify_code.py:25 msgid "Please wait {} seconds before sending" msgstr "请在 {} 秒后发送" @@ -1753,14 +1766,14 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: settings/serializers/security.py:25 users/models/user.py:470 +#: settings/serializers/security.py:25 users/models/user.py:469 #: users/serializers/profile.py:99 #: users/templates/users/user_verify_mfa.html:32 msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:471 users/serializers/profile.py:100 +#: users/models/user.py:470 users/serializers/profile.py:100 msgid "Enable" msgstr "启用" @@ -1800,35 +1813,26 @@ msgstr "确认" msgid "Code error" msgstr "代码错误" -#: authentication/templates/authentication/login.html:155 +#: authentication/templates/authentication/login.html:143 msgid "Welcome back, please enter username and password to login" msgstr "欢迎回来,请输入用户名和密码登录" -#: authentication/templates/authentication/login.html:187 +#: authentication/templates/authentication/login.html:179 #: users/templates/users/forgot_password.html:15 #: users/templates/users/forgot_password.html:16 msgid "Forgot password" msgstr "忘记密码" -#: authentication/templates/authentication/login.html:194 +#: authentication/templates/authentication/login.html:186 #: templates/_header_bar.html:83 msgid "Login" msgstr "登录" -#: authentication/templates/authentication/login.html:201 +#: authentication/templates/authentication/login.html:193 msgid "More login options" msgstr "更多登录方式" -#: authentication/templates/authentication/login_otp.html:24 -msgid "Please enter the verification code" -msgstr "请输入验证码" - -#: authentication/templates/authentication/login_otp.html:25 -#: authentication/templates/authentication/login_otp.html:75 -msgid "Send verification code" -msgstr "发送验证码" - -#: authentication/templates/authentication/login_otp.html:29 +#: authentication/templates/authentication/login_otp.html:19 #: users/templates/users/user_otp_check_password.html:16 #: users/templates/users/user_otp_enable_bind.html:24 #: users/templates/users/user_otp_enable_install_app.html:29 @@ -1836,19 +1840,10 @@ msgstr "发送验证码" msgid "Next" msgstr "下一步" -#: authentication/templates/authentication/login_otp.html:31 +#: authentication/templates/authentication/login_otp.html:21 msgid "Can't provide security? Please contact the administrator!" msgstr "如果不能提供多因子认证验证码,请联系管理员!" -#: authentication/templates/authentication/login_otp.html:68 -#: authentication/templates/authentication/login_otp.html:73 -msgid "Wait: " -msgstr "等待:" - -#: authentication/templates/authentication/login_otp.html:81 -msgid "The verification code has been sent" -msgstr "验证码已发送" - #: authentication/templates/authentication/login_wait_confirm.html:41 msgid "Refresh" msgstr "刷新" @@ -1957,24 +1952,28 @@ msgstr "请使用密码登录,然后绑定飞书" msgid "Binding FeiShu failed" msgstr "绑定飞书失败" -#: authentication/views/login.py:81 +#: authentication/views/login.py:82 msgid "Redirecting" msgstr "跳转中" -#: authentication/views/login.py:82 +#: authentication/views/login.py:83 msgid "Redirecting to {} authentication" msgstr "正在跳转到 {} 认证" -#: authentication/views/login.py:108 +#: authentication/views/login.py:109 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:172 notifications/backends/__init__.py:14 -#: users/models/user.py:662 +#: authentication/views/login.py:181 notifications/backends/__init__.py:14 +#: users/models/user.py:661 msgid "FeiShu" msgstr "飞书" -#: authentication/views/login.py:259 +#: authentication/views/login.py:202 authentication/views/mfa.py:50 +msgid "SMS" +msgstr "短信" + +#: authentication/views/login.py:291 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -1982,26 +1981,18 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\n" " 不要关闭本页面" -#: authentication/views/login.py:264 +#: authentication/views/login.py:296 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:296 +#: authentication/views/login.py:328 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:297 +#: authentication/views/login.py:329 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/mfa.py:44 users/models/user.py:37 -msgid "One-time password" -msgstr "一次性密码" - -#: authentication/views/mfa.py:50 -msgid "SMS" -msgstr "短信" - #: authentication/views/wecom.py:41 msgid "WeCom Error, Please contact your system administrator" msgstr "企业微信错误,请联系系统管理员" @@ -2218,7 +2209,7 @@ msgstr "" "div>" #: notifications/backends/__init__.py:10 users/forms/profile.py:101 -#: users/models/user.py:607 +#: users/models/user.py:606 msgid "Email" msgstr "邮件" @@ -2436,7 +2427,7 @@ msgstr "组织审计员" msgid "GLOBAL" msgstr "全局组织" -#: orgs/models.py:434 users/models/user.py:615 users/serializers/user.py:37 +#: orgs/models.py:434 users/models/user.py:614 users/serializers/user.py:37 #: users/templates/users/_select_user_modal.html:15 msgid "Role" msgstr "角色" @@ -2449,7 +2440,7 @@ msgstr "管理员正在修改授权,请稍等" msgid "The authorization cannot be revoked for the time being" msgstr "该授权暂时不能撤销" -#: perms/models/application_permission.py:27 users/models/user.py:182 +#: perms/models/application_permission.py:27 users/models/user.py:181 msgid "Application" msgstr "应用程序" @@ -2501,7 +2492,7 @@ msgid "Favorite" msgstr "收藏夹" #: perms/models/base.py:47 templates/_nav.html:21 users/models/group.py:31 -#: users/models/user.py:611 users/templates/users/_select_user_modal.html:16 +#: users/models/user.py:610 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 @@ -2512,7 +2503,7 @@ msgstr "用户组" #: perms/models/base.py:50 #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:60 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:50 -#: users/models/user.py:643 +#: users/models/user.py:642 msgid "Date expired" msgstr "失效日期" @@ -2520,6 +2511,272 @@ msgstr "失效日期" msgid "From ticket" msgstr "来自工单" +#: perms/notifications.py:18 perms/notifications.py:40 +#, fuzzy +#| msgid "Asset number" +msgid "Assets may expire" +msgstr "资产编号" + +#: perms/notifications.py:21 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Hello %(name)s:\n" +#| "
\n" +#| " Your account will expire in %(date_expired)s,\n" +#| "
\n" +#| " In order not to affect your normal work, please contact the " +#| "administrator for confirmation.\n" +#| "
\n" +#| " " +msgid "" +"\n" +" Hello %(name)s:\n" +"
\n" +" Your permissions for the following assets may expire in three " +"days:\n" +"
\n" +" %(assets)s\n" +"
\n" +" Please contact the administrator\n" +" " +msgstr "" +"\n" +" 您好 %(name)s:\n" +"
\n" +" 您的账户会在 %(date_expired)s 过期,\n" +"
\n" +" 为了不影响您正常工作,请联系管理员确认。\n" +"
\n" +" " + +#: perms/notifications.py:43 +#, python-format +msgid "" +"\n" +"Hello %(name)s:\n" +"\n" +"\n" +"Your permissions for the following assets may expire in three days:\n" +"\n" +"\n" +"%(assets)s\n" +"\n" +"\n" +"Please contact the administrator\n" +" " +msgstr "" + +#: perms/notifications.py:69 perms/notifications.py:90 +#: perms/notifications.py:117 perms/notifications.py:140 +#, fuzzy +#| msgid "Asset permission" +msgid "Asset permission will expired" +msgstr "资产授权" + +#: perms/notifications.py:72 +msgid "" +"\n" +" Hello %(name)s:\n" +"
\n" +" The following asset permissions of organization %(org) will " +"expire in three days\n" +"
\n" +" %(perms)s\n" +" " +msgstr "" + +#: perms/notifications.py:93 +msgid "" +"\n" +"Hello %(name)s:\n" +"\n" +"\n" +"The following asset permissions of organization %(org) will expire in three " +"days\n" +"\n" +"\n" +"%(perms)s\n" +" " +msgstr "" + +#: perms/notifications.py:123 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Hello %(name)s:\n" +#| "
\n" +#| " Your account will expire in %(date_expired)s,\n" +#| "
\n" +#| " In order not to affect your normal work, please contact the " +#| "administrator for confirmation.\n" +#| "
\n" +#| " " +msgid "" +"\n" +" Hello %(name)s:\n" +"
\n" +" The following asset permissions will expire in three days\n" +"
\n" +" %(content)s\n" +" " +msgstr "" +"\n" +" 您好 %(name)s:\n" +"
\n" +" 您的账户会在 %(date_expired)s 过期,\n" +"
\n" +" 为了不影响您正常工作,请联系管理员确认。\n" +"
\n" +" " + +#: perms/notifications.py:146 +msgid "" +"\n" +"Hello %(name)s:\n" +"\n" +"\n" +"The following asset permissions of organization %(org) will expire in three " +"days\n" +"\n" +"\n" +"%(content)s\n" +" " +msgstr "" + +#: perms/notifications.py:169 perms/notifications.py:191 +#, fuzzy +#| msgid "Applications" +msgid "Applications may expire" +msgstr "应用管理" + +#: perms/notifications.py:172 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Hello %(name)s:\n" +#| "
\n" +#| " Your account will expire in %(date_expired)s,\n" +#| "
\n" +#| " In order not to affect your normal work, please contact the " +#| "administrator for confirmation.\n" +#| "
\n" +#| " " +msgid "" +"\n" +" Hello %(name)s:\n" +"
\n" +" Your permissions for the following applications may expire in " +"three days:\n" +"
\n" +" %(apps)s\n" +"
\n" +" Please contact the administrator\n" +" " +msgstr "" +"\n" +" 您好 %(name)s:\n" +"
\n" +" 您的账户会在 %(date_expired)s 过期,\n" +"
\n" +" 为了不影响您正常工作,请联系管理员确认。\n" +"
\n" +" " + +#: perms/notifications.py:194 +#, python-format +msgid "" +"\n" +"Hello %(name)s:\n" +"\n" +"\n" +"Your permissions for the following applications may expire in three days:\n" +"\n" +"\n" +"%(apps)s\n" +"\n" +"\n" +"Please contact the administrator\n" +" " +msgstr "" + +#: perms/notifications.py:220 perms/notifications.py:241 +#: perms/notifications.py:268 perms/notifications.py:291 +#, fuzzy +#| msgid "Application permission" +msgid "Application permission will expired" +msgstr "应用管理" + +#: perms/notifications.py:223 +msgid "" +"\n" +" Hello %(name)s:\n" +"
\n" +" The following application permissions of organization %(org) " +"will expire in three days\n" +"
\n" +" %(perms)s\n" +" " +msgstr "" + +#: perms/notifications.py:244 +msgid "" +"\n" +"Hello %(name)s:\n" +"\n" +"\n" +"The following application permissions of organization %(org) will expire in " +"three days\n" +"\n" +"\n" +"%(perms)s\n" +" " +msgstr "" + +#: perms/notifications.py:274 +#, fuzzy, python-format +#| msgid "" +#| "\n" +#| " Hello %(name)s:\n" +#| "
\n" +#| " Your account will expire in %(date_expired)s,\n" +#| "
\n" +#| " In order not to affect your normal work, please contact the " +#| "administrator for confirmation.\n" +#| "
\n" +#| " " +msgid "" +"\n" +" Hello %(name)s:\n" +"
\n" +" The following application permissions will expire in three days\n" +"
\n" +" %(content)s\n" +" " +msgstr "" +"\n" +" 您好 %(name)s:\n" +"
\n" +" 您的账户会在 %(date_expired)s 过期,\n" +"
\n" +" 为了不影响您正常工作,请联系管理员确认。\n" +"
\n" +" " + +#: perms/notifications.py:297 +msgid "" +"\n" +"Hello %(name)s:\n" +"\n" +"\n" +"The following application permissions of organization %(org) will expire in " +"three days\n" +"\n" +"\n" +"%(content)s\n" +" " +msgstr "" + #: perms/serializers/application/permission.py:18 #: perms/serializers/application/permission.py:38 #: perms/serializers/asset/permission.py:42 @@ -2885,7 +3142,7 @@ msgstr "其它系统可以使用 SSO Token 对接 JumpServer, 免去登录的过 msgid "SSO auth key TTL" msgstr "Token 有效期" -#: settings/serializers/auth/sso.py:16 settings/serializers/security.py:74 +#: settings/serializers/auth/sso.py:16 msgid "Unit: second" msgstr "单位: 秒" @@ -3196,73 +3453,103 @@ msgstr "开启后,如果用户来源为本地,CAS、OIDC 登录将会失败" msgid "MFA verify TTL" msgstr "MFA 校验有效期" -#: settings/serializers/security.py:78 +#: settings/serializers/security.py:75 +msgid "" +"Unit: second, The verification MFA takes effect only when you view the " +"account password" +msgstr "单位: 秒, 目前仅在查看账号密码校验 MFA 时生效" + +#: settings/serializers/security.py:79 +msgid "Enable Login dynamic code" +msgstr "启用登录附加码" + +#: settings/serializers/security.py:80 +msgid "" +"The password and additional code are sent to a third party authentication " +"system for verification" +msgstr "" +"密码和附加码一并发送给第三方认证系统进行校验, 如:有的第三方认证系统,需要 密" +"码+6位数字 完成认证" + +#: settings/serializers/security.py:85 +msgid "MFA in login page" +msgstr "MFA 在登录页面" + +#: settings/serializers/security.py:86 +msgid "Eu security regulations(GDPR) require MFA to be on the login page" +msgstr "欧盟数据安全法规(GDPR) 要求 MFA 在登录页面,来确保系统登录安全" + +#: settings/serializers/security.py:89 msgid "Enable Login captcha" msgstr "启用登录验证码" -#: settings/serializers/security.py:84 +#: settings/serializers/security.py:90 +msgid "Enable captcha to prevent robot authentication" +msgstr "开启验证码,防止机器人登录" + +#: settings/serializers/security.py:110 msgid "Enable terminal register" msgstr "终端注册" -#: settings/serializers/security.py:85 +#: settings/serializers/security.py:111 msgid "" "Allow terminal register, after all terminal setup, you should disable this " "for security" msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭" -#: settings/serializers/security.py:88 +#: settings/serializers/security.py:114 msgid "Replay watermark" msgstr "录像水印" -#: settings/serializers/security.py:89 +#: settings/serializers/security.py:115 msgid "Enabled, the session replay contains watermark information" msgstr "启用后,会话录像将包含水印信息" -#: settings/serializers/security.py:93 +#: settings/serializers/security.py:119 msgid "Connection max idle time" msgstr "连接最大空闲时间" -#: settings/serializers/security.py:94 +#: settings/serializers/security.py:120 msgid "If idle time more than it, disconnect connection Unit: minute" msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)" -#: settings/serializers/security.py:97 +#: settings/serializers/security.py:123 msgid "Remember manual auth" msgstr "保存手动输入密码" -#: settings/serializers/security.py:100 +#: settings/serializers/security.py:126 msgid "Enable change auth secure mode" msgstr "启用改密安全模式" -#: settings/serializers/security.py:103 +#: settings/serializers/security.py:129 msgid "Insecure command alert" msgstr "危险命令告警" -#: settings/serializers/security.py:106 +#: settings/serializers/security.py:132 msgid "Email recipient" msgstr "邮件收件人" -#: settings/serializers/security.py:107 +#: settings/serializers/security.py:133 msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" -#: settings/serializers/security.py:110 +#: settings/serializers/security.py:136 msgid "Batch command execution" msgstr "批量命令执行" -#: settings/serializers/security.py:111 +#: settings/serializers/security.py:137 msgid "Allow user run batch command or not using ansible" msgstr "是否允许用户使用 ansible 执行批量命令" -#: settings/serializers/security.py:114 +#: settings/serializers/security.py:140 msgid "Session share" msgstr "会话分享" -#: settings/serializers/security.py:115 +#: settings/serializers/security.py:141 msgid "Enabled, Allows user active session to be shared with other users" msgstr "开启后允许用户分享已连接的资产会话给它人,协同工作" -#: settings/serializers/security.py:119 +#: settings/serializers/security.py:145 msgid "Enabled, please go to the user detail add approver" msgstr "启用后, 请在用户详情中添加审批人" @@ -3576,6 +3863,30 @@ msgstr "" "\"%(user_pubkey_update)s\"> 链接
更新\n" " " +#: templates/_mfa_otp_login.html:14 +msgid "Please enter verification code" +msgstr "请输入验证码" + +#: templates/_mfa_otp_login.html:16 templates/_mfa_otp_login.html:67 +msgid "Send verification code" +msgstr "发送验证码" + +#: templates/_mfa_otp_login.html:37 +msgid "Please enter MFA code" +msgstr "请输入6位动态安全码" + +#: templates/_mfa_otp_login.html:38 +msgid "Please enter SMS code" +msgstr "请输入短信验证码" + +#: templates/_mfa_otp_login.html:60 templates/_mfa_otp_login.html:65 +msgid "Wait: " +msgstr "等待:" + +#: templates/_mfa_otp_login.html:73 +msgid "The verification code has been sent" +msgstr "验证码已发送" + #: templates/_nav.html:7 msgid "Dashboard" msgstr "仪表盘" @@ -4711,11 +5022,11 @@ msgstr "当前组织已存在该类型" msgid "Could not reset self otp, use profile reset instead" msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置" -#: users/const.py:10 users/models/user.py:179 +#: users/const.py:10 users/models/user.py:178 msgid "System administrator" msgstr "系统管理员" -#: users/const.py:11 users/models/user.py:180 +#: users/const.py:11 users/models/user.py:179 msgid "System auditor" msgstr "系统审计员" @@ -4810,52 +5121,56 @@ msgstr "不能和原来的密钥相同" msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/profile.py:160 users/models/user.py:635 +#: users/forms/profile.py:160 users/models/user.py:634 #: users/templates/users/user_password_update.html:48 msgid "Public key" msgstr "SSH公钥" -#: users/models/user.py:38 +#: users/models/user.py:36 +msgid "One-time password" +msgstr "一次性密码" + +#: users/models/user.py:37 msgid "SMS verify code" msgstr "短信验证码" -#: users/models/user.py:472 +#: users/models/user.py:471 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:584 +#: users/models/user.py:583 msgid "Local" msgstr "数据库" -#: users/models/user.py:618 +#: users/models/user.py:617 msgid "Avatar" msgstr "头像" -#: users/models/user.py:621 +#: users/models/user.py:620 msgid "Wechat" msgstr "微信" -#: users/models/user.py:632 +#: users/models/user.py:631 msgid "Private key" msgstr "ssh私钥" -#: users/models/user.py:651 +#: users/models/user.py:650 msgid "Source" msgstr "来源" -#: users/models/user.py:655 +#: users/models/user.py:654 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:658 +#: users/models/user.py:657 msgid "Need update password" msgstr "需要更新密码" -#: users/models/user.py:822 +#: users/models/user.py:821 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:825 +#: users/models/user.py:824 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -6320,6 +6635,12 @@ msgstr "旗舰版" msgid "Community edition" msgstr "社区版" +#~ msgid "Enable Login MFA" +#~ msgstr "启用登录MFA" + +#~ msgid "Enable login password add-on" +#~ msgstr "启用登录密码附加码" + #~ msgid "OpenID" #~ msgstr "OpenID" diff --git a/apps/settings/serializers/security.py b/apps/settings/serializers/security.py index 868d2c4ef..d04a252a2 100644 --- a/apps/settings/serializers/security.py +++ b/apps/settings/serializers/security.py @@ -70,14 +70,40 @@ class SecurityAuthSerializer(serializers.Serializer): help_text=_("Only log in from the user source property") ) SECURITY_MFA_VERIFY_TTL = serializers.IntegerField( - min_value=5, max_value=60*60*10, - label=_("MFA verify TTL"), help_text=_("Unit: second"), + min_value=5, max_value=60 * 60 * 10, + label=_("MFA verify TTL"), + help_text=_("Unit: second, The verification MFA takes effect only when you view the account password"), + ) + SECURITY_LOGIN_CHALLENGE_ENABLED = serializers.BooleanField( + required=False, default=False, + label=_("Enable Login dynamic code"), + help_text=_("The password and additional code are sent to a third party " + "authentication system for verification") + ) + SECURITY_MFA_IN_LOGIN_PAGE = serializers.BooleanField( + required=False, default=False, + label=_("MFA in login page"), + help_text=_("Eu security regulations(GDPR) require MFA to be on the login page") ) SECURITY_LOGIN_CAPTCHA_ENABLED = serializers.BooleanField( - required=False, default=True, - label=_("Enable Login captcha") + required=False, default=False, label=_("Enable Login captcha"), + help_text=_("Enable captcha to prevent robot authentication") ) + def validate(self, attrs): + if attrs.get('SECURITY_MFA_AUTH') != 1: + attrs['SECURITY_MFA_IN_LOGIN_PAGE'] = False + return attrs + + def to_representation(self, instance): + data = super().to_representation(instance) + if data['SECURITY_LOGIN_CHALLENGE_ENABLED']: + data['SECURITY_MFA_IN_LOGIN_PAGE'] = False + data['SECURITY_LOGIN_CAPTCHA_ENABLED'] = False + elif data['SECURITY_MFA_IN_LOGIN_PAGE']: + data['SECURITY_LOGIN_CAPTCHA_ENABLED'] = False + return data + class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSerializer): SECURITY_SERVICE_ACCOUNT_REGISTRATION = serializers.BooleanField( @@ -118,4 +144,3 @@ class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSeri required=False, label=_('Login Confirm'), help_text=_("Enabled, please go to the user detail add approver") ) - diff --git a/apps/templates/_mfa_otp_login.html b/apps/templates/_mfa_otp_login.html new file mode 100644 index 000000000..ab80ee51c --- /dev/null +++ b/apps/templates/_mfa_otp_login.html @@ -0,0 +1,81 @@ +{% load i18n %} + +
+ + +
+ + + \ No newline at end of file diff --git a/apps/users/models/user.py b/apps/users/models/user.py index cca83f2a6..f3b4f8579 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -9,10 +9,9 @@ import datetime from django.conf import settings from django.contrib.auth.models import AbstractUser -from django.contrib.auth.hashers import check_password, make_password +from django.contrib.auth.hashers import check_password from django.core.cache import cache from django.db import models -from django.db.models import TextChoices from django.utils.translation import ugettext_lazy as _ from django.utils import timezone