feat: 首页的 chanlege 和 MFA 统一 (#6989)

* feat: 首页的 chanlege 和 MFA 统一

* 登陆样式调整

* mfa bug

* q

* m

* mfa封装组件 前端可修改配置

* perf: 添加翻译

* login css bug

* perf: 修改一些风格

* perf: 修改命名

* perf: 修改 mfa code 不是必填

* mfa 前端统一组件

* stash

* perf: 统一验证码

Co-authored-by: feng626 <1304903146@qq.com>
Co-authored-by: ibuler <ibuler@qq.com>
pull/7027/head
fit2bot 3 years ago committed by GitHub
parent fa68389028
commit 63638ed1ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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})

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

@ -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)

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

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

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

@ -5,9 +5,9 @@
<div class="col-sm-6">
<div class="input-group-prepend">
{% if audio %}
<a title="{% trans "Play CAPTCHA as audio file" %}" href="{{ audio }}">
<a title="{% trans "Play CAPTCHA as audio file" %}" href="{{ audio }}"></a>
{% endif %}
</div>
</div>
{% include "django/forms/widgets/multiwidget.html" %}
</div>
</div>

@ -10,23 +10,15 @@
{{ JMS_TITLE }}
</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% include '_head_css_js.html' %}
<!-- Stylesheets -->
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
<link href="{% static 'css/font-awesome.min.css' %}" rel="stylesheet">
<link href="{% static 'css/bootstrap-style.css' %}" rel="stylesheet">
<link href="{% static 'css/login-style.css' %}" rel="stylesheet">
<link href="{% static 'css/style.css' %}" rel="stylesheet">
<link href="{% static 'css/jumpserver.css' %}" rel="stylesheet">
<!-- scripts -->
<script src="{% static 'js/jquery-3.1.1.min.js' %}"></script>
<script src="{% static 'js/plugins/sweetalert/sweetalert.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/plugins/datatables/datatables.min.js' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
<style>
.login-content {
box-shadow: 0 5px 5px -3px rgb(0 0 0 / 20%), 0 8px 10px 1px rgb(0 0 0 / 14%), 0 3px 14px 2px rgb(0 0 0 / 12%);
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.15), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.help-block {
@ -49,17 +41,22 @@
}
.login-content {
height: 472px;
width: 984px;
height: 490px;
width: 1066px;
margin-right: auto;
margin-left: auto;
margin-top: calc((100vh - 470px) / 3);
}
body {
background-color: #f2f2f2;
height: calc(100vh - (100vh - 470px) / 3);
}
.captcha {
float: right;
}
.right-image-box {
height: 100%;
width: 50%;
@ -73,18 +70,10 @@
width: 50%;
}
.captcha {
float: right;
}
.red-fonts {
color: red;
}
.field-error {
text-align: left;
}
.form-group.has-error {
margin-bottom: 0;
}
@ -109,14 +98,6 @@
padding-top: 10px;
}
.radio, .checkbox {
margin: 0;
}
#github_star {
float: right;
margin: 10px 10px 0 0;
}
.more-login-item {
border-right: 1px dashed #dedede;
padding-left: 5px;
@ -127,11 +108,18 @@
border: none;
}
.select-con {
width: 22%;
}
.mfa-div {
width: 100%;
}
</style>
</head>
<body>
<div class="login-content ">
<div class="login-content">
<div class="right-image-box">
<a href="{% if not XPACK_ENABLED %}https://github.com/jumpserver/jumpserver{% endif %}">
<img src="{{ LOGIN_IMAGE_URL }}" style="height: 100%; width: 100%"/>
@ -170,6 +158,10 @@
</div>
{% if form.challenge %}
{% bootstrap_field form.challenge show_label=False %}
{% elif form.mfa_type %}
<div class="form-group" style="display: flex">
{% include '_mfa_otp_login.html' %}
</div>
{% elif form.captcha %}
<div class="captch-field">
{% 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 () {

@ -13,78 +13,19 @@
<p class="red-fonts">{{ form.code.errors.as_text }}</p>
{% endif %}
<div class="form-group">
<select id="verify-method-select" name="mfa_type" class="form-control" onchange="select_change(this.value)">
{% for method in methods %}
<option value="{{ method.name }}" {% if method.selected %} selected {% endif %} {% if not method.enable %} disabled {% endif %}>{{ method.label }}</option>
{% endfor %}
</select>
{% include '_mfa_otp_login.html' %}
</div>
<div class="form-group" style="display: flex">
<input id="mfa-code" required type="text" class="form-control" name="code" placeholder="{% trans 'Please enter the verification code' %}" autofocus="autofocus">
<button id='send-sms-verify-code' type="button" class="btn btn-info full-width m-b" onclick="sendSMSVerifyCode()" style="width: 150px!important;">{% trans 'Send verification code' %}</button>
</div>
<button id='submit_button' type="submit" class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button>
<button id='submit_button' type="submit"
class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button>
<div>
<small>{% trans "Can't provide security? Please contact the administrator!" %}</small>
</div>
</form>
<style type="text/css">
.disabledBtn {
background: #e6e4e4!important;
border-color: #d8d5d5!important;
color: #949191!important;
}
.mfa-div {
margin-top: 15px;
}
</style>
<script>
var methodSelect = document.getElementById('verify-method-select');
if (methodSelect.value !== null) {
select_change(methodSelect.value);
}
function select_change(type){
var currentBtn = document.getElementById('send-sms-verify-code');
if (type == "sms") {
currentBtn.style.display = "block";
currentBtn.disabled = false;
}
else {
currentBtn.style.display = "none";
currentBtn.disabled = true;
}
}
function sendSMSVerifyCode(){
var currentBtn = document.getElementById('send-sms-verify-code');
var time = 60
var url = "{% url 'api-auth:sms-verify-code-send' %}";
requestApi({
url: url,
method: "POST",
success: function (data) {
currentBtn.innerHTML = `{% trans 'Wait: ' %} ${time}`;
currentBtn.disabled = true
currentBtn.classList.add("disabledBtn" )
var TimeInterval = setInterval(()=>{
--time
currentBtn.innerHTML = `{% trans 'Wait: ' %} ${time}`;
if(time === 0) {
currentBtn.innerHTML = "{% trans 'Send verification code' %}"
currentBtn.disabled = false
currentBtn.classList.remove("disabledBtn")
clearInterval(TimeInterval)
}
},1000)
alert("{% trans 'The verification code has been sent' %}");
},
error: function (text, data) {
alert(data.detail)
},
flash_message: false
})
}
</script>
{% endblock %}

@ -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/<uuid:user_id>/', api.WeComQRUnBindForAdminApi.as_view(), name='wecom-qr-unbind-for-admin'),

@ -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,10 +123,19 @@ 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)

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

@ -1,4 +1,3 @@
import json
from collections import OrderedDict
from django.conf import settings

@ -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,

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

@ -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 <ibuler@qq.com>\n"
"Language-Team: JumpServer team<ibuler@qq.com>\n"
@ -25,7 +25,7 @@ msgstr ""
#: orgs/models.py:24 perms/models/base.py:44 settings/models.py:29
#: settings/serializers/sms.py:6 terminal/models/storage.py:23
#: terminal/models/task.py:16 terminal/models/terminal.py:100
#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py: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 <b>{}</b> confirm, You also can copy link to her/him <br/>\n"
" Don't close this page"
@ -1982,26 +1981,18 @@ msgstr ""
"等待 <b>{}</b> 确认, 你也可以复制链接发给他/她 <br/>\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"
#| " <br>\n"
#| " Your account will expire in %(date_expired)s,\n"
#| " <br>\n"
#| " In order not to affect your normal work, please contact the "
#| "administrator for confirmation.\n"
#| " <br>\n"
#| " "
msgid ""
"\n"
" Hello %(name)s:\n"
" <br>\n"
" Your permissions for the following assets may expire in three "
"days:\n"
" <br>\n"
" %(assets)s\n"
" <br>\n"
" Please contact the administrator\n"
" "
msgstr ""
"\n"
" 您好 %(name)s:\n"
" <br>\n"
" 您的账户会在 %(date_expired)s 过期,\n"
" <br>\n"
" 为了不影响您正常工作,请联系管理员确认。\n"
" <br>\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"
" <br>\n"
" The following asset permissions of organization %(org) will "
"expire in three days\n"
" <br>\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"
#| " <br>\n"
#| " Your account will expire in %(date_expired)s,\n"
#| " <br>\n"
#| " In order not to affect your normal work, please contact the "
#| "administrator for confirmation.\n"
#| " <br>\n"
#| " "
msgid ""
"\n"
" Hello %(name)s:\n"
" <br>\n"
" The following asset permissions will expire in three days\n"
" <br>\n"
" %(content)s\n"
" "
msgstr ""
"\n"
" 您好 %(name)s:\n"
" <br>\n"
" 您的账户会在 %(date_expired)s 过期,\n"
" <br>\n"
" 为了不影响您正常工作,请联系管理员确认。\n"
" <br>\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"
#| " <br>\n"
#| " Your account will expire in %(date_expired)s,\n"
#| " <br>\n"
#| " In order not to affect your normal work, please contact the "
#| "administrator for confirmation.\n"
#| " <br>\n"
#| " "
msgid ""
"\n"
" Hello %(name)s:\n"
" <br>\n"
" Your permissions for the following applications may expire in "
"three days:\n"
" <br>\n"
" %(apps)s\n"
" <br>\n"
" Please contact the administrator\n"
" "
msgstr ""
"\n"
" 您好 %(name)s:\n"
" <br>\n"
" 您的账户会在 %(date_expired)s 过期,\n"
" <br>\n"
" 为了不影响您正常工作,请联系管理员确认。\n"
" <br>\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"
" <br>\n"
" The following application permissions of organization %(org) "
"will expire in three days\n"
" <br>\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"
#| " <br>\n"
#| " Your account will expire in %(date_expired)s,\n"
#| " <br>\n"
#| " In order not to affect your normal work, please contact the "
#| "administrator for confirmation.\n"
#| " <br>\n"
#| " "
msgid ""
"\n"
" Hello %(name)s:\n"
" <br>\n"
" The following application permissions will expire in three days\n"
" <br>\n"
" %(content)s\n"
" "
msgstr ""
"\n"
" 您好 %(name)s:\n"
" <br>\n"
" 您的账户会在 %(date_expired)s 过期,\n"
" <br>\n"
" 为了不影响您正常工作,请联系管理员确认。\n"
" <br>\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\"> 链接 </a> 更新\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"

@ -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")
)

@ -0,0 +1,81 @@
{% load i18n %}
<select id="verify-method-select" name="mfa_type" class="form-control select-con" onchange="selectChange(this.value)">
{% for method in methods %}
<option value="{{ method.name }}"
{% if method.selected %} selected {% endif %}
{% if not method.enable %} disabled {% endif %}
>
{{ method.label }}
</option>
{% endfor %}
</select>
<div class="mfa-div">
<input id="mfa-code" type="text" class="form-control input-style" required name="code"
placeholder="{% trans 'Please enter verification code' %}">
<button id='send-sms-verify-code' type="button" class="btn btn-primary full-width" onclick="sendSMSVerifyCode()"
style="margin-left: 10px!important;height: 100%">{% trans 'Send verification code' %}</button>
</div>
<style type="text/css">
.input-style {
width: calc(100% - 114px);
display: inline-block;
}
#send-sms-verify-code {
width: 110px !important;
height: 100%;
vertical-align: top;
}
</style>
<script>
var methodSelect = document.getElementById('verify-method-select');
if (methodSelect.value !== null) {
selectChange(methodSelect.value);
}
function selectChange(type) {
var otpPlaceholder = '{% trans 'Please enter MFA code' %}';
var smsPlaceholder = '{% trans 'Please enter SMS code' %}';
if (type === "sms") {
$("#mfa-code").css("cssText", "width: calc(100% - 114px)").attr('placeholder', smsPlaceholder);
$("#send-sms-verify-code").css("cssText", "display: inline-block !important");
} else {
$("#mfa-code").css("cssText", "width: 100% !important").attr('placeholder', otpPlaceholder);
$("#send-sms-verify-code").css("cssText", "display: none !important");
}
}
function sendSMSVerifyCode() {
var currentBtn = document.getElementById('send-sms-verify-code');
var time = 60
var url = "{% url 'api-auth:sms-verify-code-send' %}";
var data = {
username: $("#id_username").val()
};
requestApi({
url: url,
method: "POST",
body: JSON.stringify(data),
success: function (data) {
currentBtn.innerHTML = `{% trans 'Wait: ' %} ${time}`;
currentBtn.disabled = true
currentBtn.classList.add("disabledBtn")
var TimeInterval = setInterval(() => {
--time
currentBtn.innerHTML = `{% trans 'Wait: ' %} ${time}`;
if (time === 0) {
currentBtn.innerHTML = "{% trans 'Send verification code' %}"
currentBtn.disabled = false
currentBtn.classList.remove("disabledBtn")
clearInterval(TimeInterval)
}
}, 1000)
alert("{% trans 'The verification code has been sent' %}");
},
error: function (text, data) {
alert(data.detail)
},
flash_message: false
})
}
</script>

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

Loading…
Cancel
Save