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 @@
{% 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