mirror of https://github.com/jumpserver/jumpserver
pref: 优化MFA (#7153)
* perf: 优化mfa 和登录 * perf: stash * stash * pref: 基本完成 * perf: remove init function * perf: 优化命名 * perf: 优化backends * perf: 基本完成优化 * perf: 修复首页登录时没有 toastr 的问题 Co-authored-by: ibuler <ibuler@qq.com> Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>pull/7163/head
parent
bac974b4f2
commit
17303c0550
|
@ -1,6 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import builtins
|
||||
import time
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
@ -12,55 +11,76 @@ from rest_framework.serializers import ValidationError
|
|||
from rest_framework.response import Response
|
||||
|
||||
from common.permissions import IsValidUser, NeedMFAVerify
|
||||
from users.models.user import MFAType, User
|
||||
from common.utils import get_logger
|
||||
from users.models.user import User
|
||||
from ..serializers import OtpVerifySerializer
|
||||
from .. import serializers
|
||||
from .. import errors
|
||||
from ..mfa.otp import MFAOtp
|
||||
from ..mixins import AuthMixin
|
||||
|
||||
|
||||
__all__ = ['MFAChallengeApi', 'UserOtpVerifyApi', 'SendSMSVerifyCodeApi', 'MFASelectTypeApi']
|
||||
logger = get_logger(__name__)
|
||||
|
||||
__all__ = [
|
||||
'MFAChallengeVerifyApi', 'UserOtpVerifyApi',
|
||||
'MFASendCodeApi'
|
||||
]
|
||||
|
||||
|
||||
class MFASelectTypeApi(AuthMixin, CreateAPIView):
|
||||
# MFASelectAPi 原来的名字
|
||||
class MFASendCodeApi(AuthMixin, CreateAPIView):
|
||||
"""
|
||||
选择 MFA 后对应操作 api,koko 目前在用
|
||||
"""
|
||||
permission_classes = (AllowAny,)
|
||||
serializer_class = serializers.MFASelectTypeSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
username = serializer.validated_data.get('username', '')
|
||||
mfa_type = serializer.validated_data['type']
|
||||
if mfa_type == MFAType.SMS_CODE:
|
||||
if not username:
|
||||
user = self.get_user_from_session()
|
||||
user.send_sms_code()
|
||||
else:
|
||||
user = get_object_or_404(User, username=username)
|
||||
|
||||
mfa_backend = user.get_mfa_backend_by_type(mfa_type)
|
||||
if not mfa_backend or not mfa_backend.challenge_required:
|
||||
raise ValidationError('MFA type not support: {} {}'.format(mfa_type, mfa_backend))
|
||||
mfa_backend.send_challenge()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
try:
|
||||
self.perform_create(serializer)
|
||||
return Response(serializer.data, status=201)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return Response({'error': str(e)}, status=400)
|
||||
|
||||
|
||||
class MFAChallengeApi(AuthMixin, CreateAPIView):
|
||||
class MFAChallengeVerifyApi(AuthMixin, CreateAPIView):
|
||||
permission_classes = (AllowAny,)
|
||||
serializer_class = serializers.MFAChallengeSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
try:
|
||||
user = self.get_user_from_session()
|
||||
code = serializer.validated_data.get('code')
|
||||
mfa_type = serializer.validated_data.get('type', MFAType.OTP)
|
||||
user = self.get_user_from_session()
|
||||
code = serializer.validated_data.get('code')
|
||||
mfa_type = serializer.validated_data.get('type', '')
|
||||
self._do_check_user_mfa(code, mfa_type, user)
|
||||
|
||||
valid = user.check_mfa(code, mfa_type=mfa_type)
|
||||
if not valid:
|
||||
self.request.session['auth_mfa'] = ''
|
||||
raise errors.MFAFailedError(
|
||||
username=user.username, request=self.request, ip=self.get_request_ip()
|
||||
)
|
||||
else:
|
||||
self.request.session['auth_mfa'] = '1'
|
||||
def create(self, request, *args, **kwargs):
|
||||
try:
|
||||
super().create(request, *args, **kwargs)
|
||||
return Response({'msg': 'ok'})
|
||||
except errors.AuthFailedError as e:
|
||||
data = {"error": e.error, "msg": e.msg}
|
||||
raise ValidationError(data)
|
||||
except errors.NeedMoreInfoError as e:
|
||||
return Response(e.as_data(), status=200)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
super().create(request, *args, **kwargs)
|
||||
return Response({'msg': 'ok'})
|
||||
|
||||
|
||||
class UserOtpVerifyApi(CreateAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
|
@ -73,30 +93,17 @@ class UserOtpVerifyApi(CreateAPIView):
|
|||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
code = serializer.validated_data["code"]
|
||||
otp = MFAOtp(request.user)
|
||||
|
||||
if request.user.check_mfa(code):
|
||||
ok, error = otp.check_code(code)
|
||||
if ok:
|
||||
request.session["MFA_VERIFY_TIME"] = int(time.time())
|
||||
return Response({"ok": "1"})
|
||||
else:
|
||||
return Response({"error": _("Code is invalid")}, status=400)
|
||||
return Response({"error": _("Code is invalid") + ", " + error}, status=400)
|
||||
|
||||
def get_permissions(self):
|
||||
if self.request.method.lower() == 'get' and settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
if self.request.method.lower() == 'get' \
|
||||
and settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
self.permission_classes = [NeedMFAVerify]
|
||||
return super().get_permissions()
|
||||
|
||||
|
||||
class SendSMSVerifyCodeApi(AuthMixin, CreateAPIView):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
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})
|
||||
|
|
|
@ -4,7 +4,7 @@ from rest_framework.response import Response
|
|||
from authentication.serializers import PasswordVerifySerializer
|
||||
from common.permissions import IsValidUser
|
||||
from authentication.mixins import authenticate
|
||||
from authentication.errors import PasswdInvalid
|
||||
from authentication.errors import PasswordInvalid
|
||||
from authentication.mixins import AuthMixin
|
||||
|
||||
|
||||
|
@ -20,7 +20,7 @@ class UserPasswordVerifyApi(AuthMixin, CreateAPIView):
|
|||
|
||||
user = authenticate(request=request, username=user.username, password=password)
|
||||
if not user:
|
||||
raise PasswdInvalid
|
||||
raise PasswordInvalid
|
||||
|
||||
self.set_passwd_verify_on_session(user)
|
||||
self.mark_password_ok(user)
|
||||
return Response()
|
||||
|
|
|
@ -40,5 +40,5 @@ class TokenCreateApi(AuthMixin, CreateAPIView):
|
|||
return Response(e.as_data(), status=400)
|
||||
except errors.NeedMoreInfoError as e:
|
||||
return Response(e.as_data(), status=200)
|
||||
except errors.PasswdTooSimple as e:
|
||||
except errors.PasswordTooSimple as e:
|
||||
return redirect(e.url)
|
||||
|
|
|
@ -8,7 +8,6 @@ from rest_framework import status
|
|||
from common.exceptions import JMSException
|
||||
from .signals import post_auth_failed
|
||||
from users.utils import LoginBlockUtil, MFABlockUtils
|
||||
from users.models import MFAType
|
||||
|
||||
reason_password_failed = 'password_failed'
|
||||
reason_password_decrypt_failed = 'password_decrypt_failed'
|
||||
|
@ -60,22 +59,11 @@ block_mfa_msg = _(
|
|||
"The account has been locked "
|
||||
"(please contact admin to unlock it or try again after {} minutes)"
|
||||
)
|
||||
otp_failed_msg = _(
|
||||
"One-time password invalid, or ntp sync server time, "
|
||||
mfa_error_msg = _(
|
||||
"{error},"
|
||||
"You can also try {times_try} times "
|
||||
"(The account will be temporarily locked for {block_time} minutes)"
|
||||
)
|
||||
sms_failed_msg = _(
|
||||
"SMS verify code invalid,"
|
||||
"You can also try {times_try} times "
|
||||
"(The account will be temporarily locked for {block_time} minutes)"
|
||||
)
|
||||
mfa_type_failed_msg = _(
|
||||
"The MFA type({mfa_type}) is not supported, "
|
||||
"You can also try {times_try} times "
|
||||
"(The account will be temporarily locked for {block_time} minutes)"
|
||||
)
|
||||
|
||||
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")
|
||||
|
@ -151,29 +139,19 @@ class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
|
|||
error = reason_mfa_failed
|
||||
msg: str
|
||||
|
||||
def __init__(self, username, request, ip, mfa_type=MFAType.OTP):
|
||||
util = MFABlockUtils(username, ip)
|
||||
util.incr_failed_count()
|
||||
def __init__(self, username, request, ip, mfa_type, error):
|
||||
super().__init__(username=username, request=request)
|
||||
|
||||
times_remainder = util.get_remainder_times()
|
||||
util = MFABlockUtils(username, ip)
|
||||
times_remainder = util.incr_failed_count()
|
||||
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
|
||||
|
||||
if times_remainder:
|
||||
if mfa_type == MFAType.OTP:
|
||||
self.msg = otp_failed_msg.format(
|
||||
times_try=times_remainder, block_time=block_time
|
||||
)
|
||||
elif mfa_type == MFAType.SMS_CODE:
|
||||
self.msg = sms_failed_msg.format(
|
||||
times_try=times_remainder, block_time=block_time
|
||||
)
|
||||
else:
|
||||
self.msg = mfa_type_failed_msg.format(
|
||||
mfa_type=mfa_type, times_try=times_remainder, block_time=block_time
|
||||
)
|
||||
self.msg = mfa_error_msg.format(
|
||||
error=error, times_try=times_remainder, block_time=block_time
|
||||
)
|
||||
else:
|
||||
self.msg = block_mfa_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
|
||||
super().__init__(username=username, request=request)
|
||||
|
||||
|
||||
class BlockMFAError(AuthFailedNeedLogMixin, AuthFailedError):
|
||||
|
@ -228,7 +206,7 @@ class MFARequiredError(NeedMoreInfoError):
|
|||
msg = mfa_required_msg
|
||||
error = 'mfa_required'
|
||||
|
||||
def __init__(self, error='', msg='', mfa_types=tuple(MFAType)):
|
||||
def __init__(self, error='', msg='', mfa_types=()):
|
||||
super().__init__(error=error, msg=msg)
|
||||
self.choices = mfa_types
|
||||
|
||||
|
@ -305,7 +283,7 @@ class SSOAuthClosed(JMSException):
|
|||
default_detail = _('SSO auth closed')
|
||||
|
||||
|
||||
class PasswdTooSimple(JMSException):
|
||||
class PasswordTooSimple(JMSException):
|
||||
default_code = 'passwd_too_simple'
|
||||
default_detail = _('Your password is too simple, please change it for security')
|
||||
|
||||
|
@ -314,7 +292,7 @@ class PasswdTooSimple(JMSException):
|
|||
self.url = url
|
||||
|
||||
|
||||
class PasswdNeedUpdate(JMSException):
|
||||
class PasswordNeedUpdate(JMSException):
|
||||
default_code = 'passwd_need_update'
|
||||
default_detail = _('You should to change your password before login')
|
||||
|
||||
|
@ -357,7 +335,7 @@ class FeiShuNotBound(JMSException):
|
|||
default_detail = 'FeiShu is not bound'
|
||||
|
||||
|
||||
class PasswdInvalid(JMSException):
|
||||
class PasswordInvalid(JMSException):
|
||||
default_code = 'passwd_invalid'
|
||||
default_detail = _('Your password is invalid')
|
||||
|
||||
|
@ -368,10 +346,6 @@ class NotHaveUpDownLoadPerm(JMSException):
|
|||
default_detail = _('No upload or download permission')
|
||||
|
||||
|
||||
class NotEnableMFAError(JMSException):
|
||||
default_detail = mfa_unset_msg
|
||||
|
||||
|
||||
class OTPBindRequiredError(JMSException):
|
||||
default_detail = otp_unset_msg
|
||||
|
||||
|
@ -380,11 +354,13 @@ class OTPBindRequiredError(JMSException):
|
|||
self.url = url
|
||||
|
||||
|
||||
class OTPCodeRequiredError(AuthFailedError):
|
||||
class MFACodeRequiredError(AuthFailedError):
|
||||
msg = _("Please enter MFA code")
|
||||
|
||||
|
||||
class SMSCodeRequiredError(AuthFailedError):
|
||||
msg = _("Please enter SMS code")
|
||||
|
||||
|
||||
class UserPhoneNotSet(AuthFailedError):
|
||||
msg = _('Phone not set')
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
from .otp import MFAOtp, otp_failed_msg
|
||||
from .sms import MFASms
|
||||
from .radius import MFARadius
|
||||
|
||||
MFA_BACKENDS = [MFAOtp, MFASms, MFARadius]
|
|
@ -0,0 +1,72 @@
|
|||
import abc
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class BaseMFA(abc.ABC):
|
||||
placeholder = _('Please input security code')
|
||||
|
||||
def __init__(self, user):
|
||||
"""
|
||||
:param user: Authenticated user, Anonymous or None
|
||||
因为首页登录时,可能没法获取到一些状态
|
||||
"""
|
||||
self.user = user
|
||||
|
||||
def is_authenticated(self):
|
||||
return self.user and self.user.is_authenticated
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def name(self):
|
||||
return ''
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def display_name(self):
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
def challenge_required():
|
||||
return False
|
||||
|
||||
def send_challenge(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def check_code(self, code) -> tuple:
|
||||
return False, 'Error msg'
|
||||
|
||||
@abc.abstractmethod
|
||||
def is_active(self):
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def global_enabled():
|
||||
return False
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_enable_url(self) -> str:
|
||||
return ''
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_disable_url(self) -> str:
|
||||
return ''
|
||||
|
||||
@abc.abstractmethod
|
||||
def disable(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def can_disable(self) -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def help_text_of_enable():
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
def help_text_of_disable():
|
||||
return ''
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from django.shortcuts import reverse
|
||||
|
||||
from .base import BaseMFA
|
||||
|
||||
|
||||
otp_failed_msg = _("OTP code invalid, or server time error")
|
||||
|
||||
|
||||
class MFAOtp(BaseMFA):
|
||||
name = 'otp'
|
||||
display_name = _('OTP')
|
||||
|
||||
def check_code(self, code):
|
||||
from users.utils import check_otp_code
|
||||
assert self.is_authenticated()
|
||||
|
||||
ok = check_otp_code(self.user.otp_secret_key, code)
|
||||
msg = '' if ok else otp_failed_msg
|
||||
return ok, msg
|
||||
|
||||
def is_active(self):
|
||||
if not self.is_authenticated():
|
||||
return True
|
||||
return self.user.otp_secret_key
|
||||
|
||||
@staticmethod
|
||||
def global_enabled():
|
||||
return True
|
||||
|
||||
def get_enable_url(self) -> str:
|
||||
return reverse('authentication:user-otp-enable-start')
|
||||
|
||||
def disable(self):
|
||||
assert self.is_authenticated()
|
||||
self.user.otp_secret_key = ''
|
||||
self.user.save(update_fields=['otp_secret_key'])
|
||||
|
||||
def can_disable(self) -> bool:
|
||||
return True
|
||||
|
||||
def get_disable_url(self):
|
||||
return reverse('authentication:user-otp-disable')
|
||||
|
||||
@staticmethod
|
||||
def help_text_of_enable():
|
||||
return _("Virtual OTP based MFA")
|
||||
|
||||
def help_text_of_disable(self):
|
||||
return ''
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
from .base import BaseMFA
|
||||
from ..backends.radius import RadiusBackend
|
||||
|
||||
mfa_failed_msg = _("Radius verify code invalid")
|
||||
|
||||
|
||||
class MFARadius(BaseMFA):
|
||||
name = 'otp_radius'
|
||||
display_name = _('Radius MFA')
|
||||
|
||||
def check_code(self, code):
|
||||
assert self.is_authenticated()
|
||||
backend = RadiusBackend()
|
||||
username = self.user.username
|
||||
user = backend.authenticate(
|
||||
None, username=username, password=code
|
||||
)
|
||||
ok = user is not None
|
||||
msg = '' if ok else mfa_failed_msg
|
||||
return ok, msg
|
||||
|
||||
def is_active(self):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def global_enabled():
|
||||
return settings.OTP_IN_RADIUS
|
||||
|
||||
def get_enable_url(self) -> str:
|
||||
return ''
|
||||
|
||||
def can_disable(self):
|
||||
return False
|
||||
|
||||
def disable(self):
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
def help_text_of_disable():
|
||||
return _("Radius global enabled, cannot disable")
|
||||
|
||||
def get_disable_url(self) -> str:
|
||||
return ''
|
|
@ -0,0 +1,60 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
from .base import BaseMFA
|
||||
from common.sdk.sms import SendAndVerifySMSUtil
|
||||
|
||||
sms_failed_msg = _("SMS verify code invalid")
|
||||
|
||||
|
||||
class MFASms(BaseMFA):
|
||||
name = 'sms'
|
||||
display_name = _("SMS")
|
||||
placeholder = _("SMS verification code")
|
||||
|
||||
def __init__(self, user):
|
||||
super().__init__(user)
|
||||
phone = user.phone if self.is_authenticated() else ''
|
||||
self.sms = SendAndVerifySMSUtil(phone)
|
||||
|
||||
def check_code(self, code):
|
||||
assert self.is_authenticated()
|
||||
ok = self.sms.verify(code)
|
||||
msg = '' if ok else sms_failed_msg
|
||||
return ok, msg
|
||||
|
||||
def is_active(self):
|
||||
if not self.is_authenticated():
|
||||
return True
|
||||
return self.user.phone
|
||||
|
||||
@staticmethod
|
||||
def challenge_required():
|
||||
return True
|
||||
|
||||
def send_challenge(self):
|
||||
self.sms.gen_and_send()
|
||||
|
||||
@staticmethod
|
||||
def global_enabled():
|
||||
return settings.SMS_ENABLED
|
||||
|
||||
def get_enable_url(self) -> str:
|
||||
return '/ui/#/users/profile/?activeTab=ProfileUpdate'
|
||||
|
||||
def can_disable(self) -> bool:
|
||||
return True
|
||||
|
||||
def disable(self):
|
||||
return '/ui/#/users/profile/?activeTab=ProfileUpdate'
|
||||
|
||||
@staticmethod
|
||||
def help_text_of_enable():
|
||||
return _("Set phone number to enable")
|
||||
|
||||
@staticmethod
|
||||
def help_text_of_disable():
|
||||
return _("Clear phone number to disable")
|
||||
|
||||
def get_disable_url(self) -> str:
|
||||
return '/ui/#/users/profile/?activeTab=ProfileUpdate'
|
|
@ -10,5 +10,5 @@ class MFAMiddleware:
|
|||
if request.path.find('/auth/login/otp/') > -1:
|
||||
return response
|
||||
if request.session.get('auth_mfa_required'):
|
||||
return redirect('authentication:login-otp')
|
||||
return redirect('authentication:login-mfa')
|
||||
return response
|
||||
|
|
|
@ -1,24 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import inspect
|
||||
from django.utils.http import urlencode
|
||||
from functools import partial
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
from django.utils.http import urlencode
|
||||
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 rest_framework.request import Request
|
||||
from django.contrib.auth import (
|
||||
BACKEND_SESSION_KEY, _get_backends,
|
||||
PermissionDenied, user_login_failed, _clean_credentials
|
||||
)
|
||||
from django.shortcuts import reverse, redirect
|
||||
from django.shortcuts import reverse, redirect, get_object_or_404
|
||||
|
||||
from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil
|
||||
from acls.models import LoginACL
|
||||
from users.models import User, MFAType
|
||||
from users.models import User
|
||||
from users.utils import LoginBlockUtil, MFABlockUtils
|
||||
from . import errors
|
||||
from .utils import rsa_decrypt, gen_key_pair
|
||||
|
@ -32,8 +34,7 @@ def check_backend_can_auth(username, backend_path, allowed_auth_backends):
|
|||
if allowed_auth_backends is not None and backend_path not in allowed_auth_backends:
|
||||
logger.debug('Skip user auth backend: {}, {} not in'.format(
|
||||
username, backend_path, ','.join(allowed_auth_backends)
|
||||
)
|
||||
)
|
||||
))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
@ -109,17 +110,18 @@ class PasswordEncryptionViewMixin:
|
|||
def decrypt_passwd(self, raw_passwd):
|
||||
# 获取解密密钥,对密码进行解密
|
||||
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
||||
if rsa_private_key is not None:
|
||||
try:
|
||||
return rsa_decrypt(raw_passwd, rsa_private_key)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
logger.error(
|
||||
f'Decrypt password failed: password[{raw_passwd}] '
|
||||
f'rsa_private_key[{rsa_private_key}]'
|
||||
)
|
||||
return None
|
||||
return raw_passwd
|
||||
if rsa_private_key is None:
|
||||
return raw_passwd
|
||||
|
||||
try:
|
||||
return rsa_decrypt(raw_passwd, rsa_private_key)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
logger.error(
|
||||
f'Decrypt password failed: password[{raw_passwd}] '
|
||||
f'rsa_private_key[{rsa_private_key}]'
|
||||
)
|
||||
return None
|
||||
|
||||
def get_request_ip(self):
|
||||
ip = ''
|
||||
|
@ -132,7 +134,7 @@ class PasswordEncryptionViewMixin:
|
|||
# 生成加解密密钥对,public_key传递给前端,private_key存入session中供解密使用
|
||||
rsa_public_key = self.request.session.get(RSA_PUBLIC_KEY)
|
||||
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
||||
if not all((rsa_private_key, rsa_public_key)):
|
||||
if not all([rsa_private_key, rsa_public_key]):
|
||||
rsa_private_key, rsa_public_key = gen_key_pair()
|
||||
rsa_public_key = rsa_public_key.replace('\n', '\\n')
|
||||
self.request.session[RSA_PRIVATE_KEY] = rsa_private_key
|
||||
|
@ -144,49 +146,9 @@ class PasswordEncryptionViewMixin:
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AuthMixin(PasswordEncryptionViewMixin):
|
||||
request = None
|
||||
partial_credential_error = None
|
||||
|
||||
key_prefix_captcha = "_LOGIN_INVALID_{}"
|
||||
|
||||
def get_user_from_session(self):
|
||||
if self.request.session.is_empty():
|
||||
raise errors.SessionEmptyError()
|
||||
|
||||
if all((self.request.user,
|
||||
not self.request.user.is_anonymous,
|
||||
BACKEND_SESSION_KEY in self.request.session)):
|
||||
user = self.request.user
|
||||
user.backend = self.request.session[BACKEND_SESSION_KEY]
|
||||
return user
|
||||
|
||||
user_id = self.request.session.get('user_id')
|
||||
if not user_id:
|
||||
user = None
|
||||
else:
|
||||
user = get_object_or_none(User, pk=user_id)
|
||||
if not user:
|
||||
raise errors.SessionEmptyError()
|
||||
user.backend = self.request.session.get("auth_backend")
|
||||
return user
|
||||
|
||||
def _check_is_block(self, username, raise_exception=True):
|
||||
ip = self.get_request_ip()
|
||||
if LoginBlockUtil(username, ip).is_block():
|
||||
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
|
||||
exception = errors.BlockLoginError(username=username, ip=ip)
|
||||
if raise_exception:
|
||||
raise errors.BlockLoginError(username=username, ip=ip)
|
||||
else:
|
||||
return exception
|
||||
|
||||
def check_is_block(self, raise_exception=True):
|
||||
if hasattr(self.request, 'data'):
|
||||
username = self.request.data.get("username")
|
||||
else:
|
||||
username = self.request.POST.get("username")
|
||||
self._check_is_block(username, raise_exception)
|
||||
class CommonMixin(PasswordEncryptionViewMixin):
|
||||
request: Request
|
||||
get_request_ip: Callable
|
||||
|
||||
def raise_credential_error(self, error):
|
||||
raise self.partial_credential_error(error=error)
|
||||
|
@ -197,6 +159,31 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
ip=ip, request=request
|
||||
)
|
||||
|
||||
def get_user_from_session(self):
|
||||
if self.request.session.is_empty():
|
||||
raise errors.SessionEmptyError()
|
||||
|
||||
if all([
|
||||
self.request.user,
|
||||
not self.request.user.is_anonymous,
|
||||
BACKEND_SESSION_KEY in self.request.session
|
||||
]):
|
||||
user = self.request.user
|
||||
user.backend = self.request.session[BACKEND_SESSION_KEY]
|
||||
return user
|
||||
|
||||
user_id = self.request.session.get('user_id')
|
||||
auth_password = self.request.session.get('auth_password')
|
||||
auth_expired_at = self.request.session.get('auth_password_expired_at')
|
||||
auth_expired = auth_expired_at < time.time() if auth_expired_at else False
|
||||
|
||||
if not user_id or not auth_password or auth_expired:
|
||||
raise errors.SessionEmptyError()
|
||||
|
||||
user = get_object_or_404(User, pk=user_id)
|
||||
user.backend = self.request.session.get("auth_backend")
|
||||
return user
|
||||
|
||||
def get_auth_data(self, decrypt_passwd=False):
|
||||
request = self.request
|
||||
if hasattr(request, 'data'):
|
||||
|
@ -214,6 +201,31 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
password = password + challenge.strip()
|
||||
return username, password, public_key, ip, auto_login
|
||||
|
||||
|
||||
class AuthPreCheckMixin:
|
||||
request: Request
|
||||
get_request_ip: Callable
|
||||
raise_credential_error: Callable
|
||||
|
||||
def _check_is_block(self, username, raise_exception=True):
|
||||
ip = self.get_request_ip()
|
||||
is_block = LoginBlockUtil(username, ip).is_block()
|
||||
if not is_block:
|
||||
return
|
||||
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
|
||||
exception = errors.BlockLoginError(username=username, ip=ip)
|
||||
if raise_exception:
|
||||
raise errors.BlockLoginError(username=username, ip=ip)
|
||||
else:
|
||||
return exception
|
||||
|
||||
def check_is_block(self, raise_exception=True):
|
||||
if hasattr(self.request, 'data'):
|
||||
username = self.request.data.get("username")
|
||||
else:
|
||||
username = self.request.POST.get("username")
|
||||
self._check_is_block(username, raise_exception)
|
||||
|
||||
def _check_only_allow_exists_user_auth(self, username):
|
||||
# 仅允许预先存在的用户认证
|
||||
if not settings.ONLY_ALLOW_EXIST_USER_AUTH:
|
||||
|
@ -224,105 +236,92 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
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)
|
||||
if not user:
|
||||
self.raise_credential_error(errors.reason_password_failed)
|
||||
elif user.is_expired:
|
||||
self.raise_credential_error(errors.reason_user_expired)
|
||||
elif not user.is_active:
|
||||
self.raise_credential_error(errors.reason_user_inactive)
|
||||
return user
|
||||
|
||||
def _check_login_mfa_login_if_need(self, user):
|
||||
class MFAMixin:
|
||||
request: Request
|
||||
get_user_from_session: Callable
|
||||
get_request_ip: Callable
|
||||
|
||||
def _check_login_page_mfa_if_need(self, user):
|
||||
if not settings.SECURITY_MFA_IN_LOGIN_PAGE:
|
||||
return
|
||||
|
||||
request = self.request
|
||||
if hasattr(request, 'data'):
|
||||
data = request.data
|
||||
else:
|
||||
data = request.POST
|
||||
data = request.data if hasattr(request, 'data') else request.POST
|
||||
code = data.get('code')
|
||||
mfa_type = data.get('mfa_type')
|
||||
if settings.SECURITY_MFA_IN_LOGIN_PAGE and mfa_type:
|
||||
if not code:
|
||||
if mfa_type == MFAType.OTP and bool(user.otp_secret_key):
|
||||
raise errors.OTPCodeRequiredError
|
||||
elif mfa_type == MFAType.SMS_CODE:
|
||||
raise errors.SMSCodeRequiredError
|
||||
self.check_user_mfa(code, mfa_type, user=user)
|
||||
mfa_type = data.get('mfa_type', 'otp')
|
||||
if not code:
|
||||
raise errors.MFACodeRequiredError
|
||||
self._do_check_user_mfa(code, mfa_type, user=user)
|
||||
|
||||
def _check_login_acl(self, user, ip):
|
||||
# ACL 限制用户登录
|
||||
is_allowed, limit_type = LoginACL.allow_user_to_login(user, ip)
|
||||
if not is_allowed:
|
||||
if limit_type == 'ip':
|
||||
raise errors.LoginIPNotAllowed(username=user.username, request=self.request)
|
||||
elif limit_type == 'time':
|
||||
raise errors.TimePeriodNotAllowed(username=user.username, request=self.request)
|
||||
def check_user_mfa_if_need(self, user):
|
||||
if self.request.session.get('auth_mfa'):
|
||||
return
|
||||
if not user.mfa_enabled:
|
||||
return
|
||||
|
||||
def set_login_failed_mark(self):
|
||||
active_mfa_mapper = user.active_mfa_backends_mapper
|
||||
if not active_mfa_mapper:
|
||||
url = reverse('authentication:user-otp-enable-start')
|
||||
raise errors.MFAUnsetError(user, self.request, url)
|
||||
raise errors.MFARequiredError(mfa_types=tuple(active_mfa_mapper.keys()))
|
||||
|
||||
def mark_mfa_ok(self, mfa_type):
|
||||
self.request.session['auth_mfa'] = 1
|
||||
self.request.session['auth_mfa_time'] = time.time()
|
||||
self.request.session['auth_mfa_required'] = 0
|
||||
self.request.session['auth_mfa_type'] = mfa_type
|
||||
|
||||
def clean_mfa_mark(self):
|
||||
keys = ['auth_mfa', 'auth_mfa_time', 'auth_mfa_required', 'auth_mfa_type']
|
||||
for k in keys:
|
||||
self.request.session.pop(k, '')
|
||||
|
||||
def check_mfa_is_block(self, username, ip, raise_exception=True):
|
||||
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 _do_check_user_mfa(self, code, mfa_type, user=None):
|
||||
user = user if user else self.get_user_from_session()
|
||||
if not user.mfa_enabled:
|
||||
return
|
||||
|
||||
# 监测 MFA 是不是屏蔽了
|
||||
ip = self.get_request_ip()
|
||||
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
|
||||
self.check_mfa_is_block(user.username, ip)
|
||||
|
||||
def set_passwd_verify_on_session(self, user: User):
|
||||
self.request.session['user_id'] = str(user.id)
|
||||
self.request.session['auth_password'] = 1
|
||||
self.request.session['auth_password_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS
|
||||
ok = False
|
||||
mfa_backend = user.get_mfa_backend_by_type(mfa_type)
|
||||
if mfa_backend:
|
||||
ok, msg = mfa_backend.check_code(code)
|
||||
else:
|
||||
msg = _('The MFA type({}) is not supported'.format(mfa_type))
|
||||
|
||||
def check_is_need_captcha(self):
|
||||
# 最近有登录失败时需要填写验证码
|
||||
ip = get_request_ip(self.request)
|
||||
need = cache.get(self.key_prefix_captcha.format(ip))
|
||||
return need
|
||||
if ok:
|
||||
self.mark_mfa_ok(mfa_type)
|
||||
return
|
||||
|
||||
def check_user_auth(self, decrypt_passwd=False):
|
||||
self.check_is_block()
|
||||
username, password, public_key, ip, auto_login = self.get_auth_data(decrypt_passwd)
|
||||
raise errors.MFAFailedError(
|
||||
username=user.username,
|
||||
request=self.request,
|
||||
ip=ip, mfa_type=mfa_type,
|
||||
error=msg
|
||||
)
|
||||
|
||||
self._check_only_allow_exists_user_auth(username)
|
||||
user = self._check_auth_user_is_valid(username, password, public_key)
|
||||
# 校验login-acl规则
|
||||
self._check_login_acl(user, ip)
|
||||
self._check_password_require_reset_or_not(user)
|
||||
self._check_passwd_is_too_simple(user, password)
|
||||
self._check_passwd_need_update(user)
|
||||
@staticmethod
|
||||
def get_user_mfa_context(user=None):
|
||||
mfa_backends = User.get_user_mfa_backends(user)
|
||||
return {'mfa_backends': mfa_backends}
|
||||
|
||||
# 校验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
|
||||
request.session['auth_backend'] = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL)
|
||||
return user
|
||||
|
||||
def _check_is_local_user(self, user: User):
|
||||
if user.source != User.Source.local:
|
||||
raise self.raise_credential_error(error=errors.only_local_users_are_allowed)
|
||||
|
||||
def check_oauth2_auth(self, user: User, auth_backend):
|
||||
ip = self.get_request_ip()
|
||||
request = self.request
|
||||
|
||||
self._set_partial_credential_error(user.username, ip, request)
|
||||
|
||||
if user.is_expired:
|
||||
self.raise_credential_error(errors.reason_user_expired)
|
||||
elif not user.is_active:
|
||||
self.raise_credential_error(errors.reason_user_inactive)
|
||||
|
||||
self._check_is_block(user.username)
|
||||
self._check_login_acl(user, ip)
|
||||
|
||||
LoginBlockUtil(user.username, ip).clean_failed_count()
|
||||
MFABlockUtils(user.username, ip).clean_failed_count()
|
||||
|
||||
request.session['auth_password'] = 1
|
||||
request.session['user_id'] = str(user.id)
|
||||
request.session['auth_backend'] = auth_backend
|
||||
return user
|
||||
|
||||
class AuthPostCheckMixin:
|
||||
@classmethod
|
||||
def generate_reset_password_url_with_flash_msg(cls, user, message):
|
||||
reset_passwd_url = reverse('authentication:reset-password')
|
||||
|
@ -344,14 +343,14 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
if user.is_superuser and password == 'admin':
|
||||
message = _('Your password is too simple, please change it for security')
|
||||
url = cls.generate_reset_password_url_with_flash_msg(user, message=message)
|
||||
raise errors.PasswdTooSimple(url)
|
||||
raise errors.PasswordTooSimple(url)
|
||||
|
||||
@classmethod
|
||||
def _check_passwd_need_update(cls, user: User):
|
||||
if user.need_update_password:
|
||||
message = _('You should to change your password before login')
|
||||
url = cls.generate_reset_password_url_with_flash_msg(user, message)
|
||||
raise errors.PasswdNeedUpdate(url)
|
||||
raise errors.PasswordNeedUpdate(url)
|
||||
|
||||
@classmethod
|
||||
def _check_password_require_reset_or_not(cls, user: User):
|
||||
|
@ -360,76 +359,20 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
url = cls.generate_reset_password_url_with_flash_msg(user, message)
|
||||
raise errors.PasswordRequireResetError(url)
|
||||
|
||||
def check_user_auth_if_need(self, decrypt_passwd=False):
|
||||
request = self.request
|
||||
if request.session.get('auth_password') and \
|
||||
request.session.get('user_id'):
|
||||
user = self.get_user_from_session()
|
||||
if user:
|
||||
return user
|
||||
return self.check_user_auth(decrypt_passwd=decrypt_passwd)
|
||||
|
||||
def check_user_mfa_if_need(self, user):
|
||||
if self.request.session.get('auth_mfa'):
|
||||
class AuthACLMixin:
|
||||
request: Request
|
||||
get_request_ip: Callable
|
||||
|
||||
def _check_login_acl(self, user, ip):
|
||||
# ACL 限制用户登录
|
||||
is_allowed, limit_type = LoginACL.allow_user_to_login(user, ip)
|
||||
if is_allowed:
|
||||
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)
|
||||
raise errors.MFARequiredError(mfa_types=user.get_supported_mfa_types())
|
||||
|
||||
def mark_mfa_ok(self, mfa_type=MFAType.OTP):
|
||||
self.request.session['auth_mfa'] = 1
|
||||
self.request.session['auth_mfa_time'] = time.time()
|
||||
self.request.session['auth_mfa_required'] = ''
|
||||
self.request.session['auth_mfa_type'] = mfa_type
|
||||
|
||||
def clean_mfa_mark(self):
|
||||
self.request.session['auth_mfa'] = ''
|
||||
self.request.session['auth_mfa_time'] = ''
|
||||
self.request.session['auth_mfa_required'] = ''
|
||||
self.request.session['auth_mfa_type'] = ''
|
||||
|
||||
def check_mfa_is_block(self, username, ip, raise_exception=True):
|
||||
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
|
||||
|
||||
if not bool(user.phone) and mfa_type == MFAType.SMS_CODE:
|
||||
raise errors.UserPhoneNotSet
|
||||
|
||||
if not bool(user.otp_secret_key) and mfa_type == MFAType.OTP:
|
||||
self.set_passwd_verify_on_session(user)
|
||||
raise errors.OTPBindRequiredError(reverse_lazy('authentication:user-otp-enable-bind'))
|
||||
|
||||
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
|
||||
|
||||
raise errors.MFAFailedError(
|
||||
username=user.username,
|
||||
request=self.request,
|
||||
ip=ip, mfa_type=mfa_type,
|
||||
)
|
||||
if limit_type == 'ip':
|
||||
raise errors.LoginIPNotAllowed(username=user.username, request=self.request)
|
||||
elif limit_type == 'time':
|
||||
raise errors.TimePeriodNotAllowed(username=user.username, request=self.request)
|
||||
|
||||
def get_ticket(self):
|
||||
from tickets.models import Ticket
|
||||
|
@ -480,11 +423,99 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
self.get_ticket_or_create(confirm_setting)
|
||||
self.check_user_login_confirm()
|
||||
|
||||
|
||||
class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPostCheckMixin):
|
||||
request = None
|
||||
partial_credential_error = None
|
||||
|
||||
key_prefix_captcha = "_LOGIN_INVALID_{}"
|
||||
|
||||
def _check_auth_user_is_valid(self, username, password, public_key):
|
||||
user = authenticate(
|
||||
self.request, username=username,
|
||||
password=password, public_key=public_key
|
||||
)
|
||||
if not user:
|
||||
self.raise_credential_error(errors.reason_password_failed)
|
||||
elif user.is_expired:
|
||||
self.raise_credential_error(errors.reason_user_expired)
|
||||
elif not user.is_active:
|
||||
self.raise_credential_error(errors.reason_user_inactive)
|
||||
return user
|
||||
|
||||
def set_login_failed_mark(self):
|
||||
ip = self.get_request_ip()
|
||||
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
|
||||
|
||||
def check_is_need_captcha(self):
|
||||
# 最近有登录失败时需要填写验证码
|
||||
ip = get_request_ip(self.request)
|
||||
need = cache.get(self.key_prefix_captcha.format(ip))
|
||||
return need
|
||||
|
||||
def check_user_auth(self, decrypt_passwd=False):
|
||||
# pre check
|
||||
self.check_is_block()
|
||||
username, password, public_key, ip, auto_login = self.get_auth_data(decrypt_passwd)
|
||||
self._check_only_allow_exists_user_auth(username)
|
||||
|
||||
# check auth
|
||||
user = self._check_auth_user_is_valid(username, password, public_key)
|
||||
|
||||
# 校验login-acl规则
|
||||
self._check_login_acl(user, ip)
|
||||
|
||||
# post check
|
||||
self._check_password_require_reset_or_not(user)
|
||||
self._check_passwd_is_too_simple(user, password)
|
||||
self._check_passwd_need_update(user)
|
||||
|
||||
# 校验login-mfa, 如果登录页面上显示 mfa 的话
|
||||
self._check_login_page_mfa_if_need(user)
|
||||
|
||||
# 标记密码验证成功
|
||||
self.mark_password_ok(user=user, auto_login=auto_login)
|
||||
LoginBlockUtil(user.username, ip).clean_failed_count()
|
||||
return user
|
||||
|
||||
def mark_password_ok(self, user, auto_login=False):
|
||||
request = self.request
|
||||
request.session['auth_password'] = 1
|
||||
request.session['auth_password_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS
|
||||
request.session['user_id'] = str(user.id)
|
||||
request.session['auto_login'] = auto_login
|
||||
request.session['auth_backend'] = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL)
|
||||
|
||||
def check_oauth2_auth(self, user: User, auth_backend):
|
||||
ip = self.get_request_ip()
|
||||
request = self.request
|
||||
|
||||
self._set_partial_credential_error(user.username, ip, request)
|
||||
|
||||
if user.is_expired:
|
||||
self.raise_credential_error(errors.reason_user_expired)
|
||||
elif not user.is_active:
|
||||
self.raise_credential_error(errors.reason_user_inactive)
|
||||
|
||||
self._check_is_block(user.username)
|
||||
self._check_login_acl(user, ip)
|
||||
|
||||
LoginBlockUtil(user.username, ip).clean_failed_count()
|
||||
MFABlockUtils(user.username, ip).clean_failed_count()
|
||||
|
||||
self.mark_password_ok(user, False)
|
||||
return user
|
||||
|
||||
def check_user_auth_if_need(self, decrypt_passwd=False):
|
||||
request = self.request
|
||||
if not request.session.get('auth_password'):
|
||||
return self.check_user_auth(decrypt_passwd=decrypt_passwd)
|
||||
return self.get_user_from_session()
|
||||
|
||||
def clear_auth_mark(self):
|
||||
self.request.session['auth_password'] = ''
|
||||
self.request.session['auth_user_id'] = ''
|
||||
self.request.session['auth_confirm'] = ''
|
||||
self.request.session['auth_ticket_id'] = ''
|
||||
keys = ['auth_password', 'user_id', 'auth_confirm', 'auth_ticket_id']
|
||||
for k in keys:
|
||||
self.request.session.pop(k, '')
|
||||
|
||||
def send_auth_signal(self, success=True, user=None, username='', reason=''):
|
||||
if success:
|
||||
|
@ -503,31 +534,3 @@ 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
|
||||
|
|
|
@ -78,6 +78,7 @@ class BearerTokenSerializer(serializers.Serializer):
|
|||
|
||||
class MFASelectTypeSerializer(serializers.Serializer):
|
||||
type = serializers.CharField()
|
||||
username = serializers.CharField(required=False, allow_blank=True, allow_null=True)
|
||||
|
||||
|
||||
class MFAChallengeSerializer(serializers.Serializer):
|
||||
|
|
|
@ -13,11 +13,11 @@ from .signals import post_auth_success, post_auth_failed
|
|||
|
||||
@receiver(user_logged_in)
|
||||
def on_user_auth_login_success(sender, user, request, **kwargs):
|
||||
# 开启了 MFA,且没有校验过
|
||||
|
||||
if user.mfa_enabled and not settings.OTP_IN_RADIUS and not request.session.get('auth_mfa'):
|
||||
# 开启了 MFA,且没有校验过, 可以全局校验, middleware 中可以全局管理 oidc 等第三方认证的 MFA
|
||||
if user.mfa_enabled and not request.session.get('auth_mfa'):
|
||||
request.session['auth_mfa_required'] = 1
|
||||
|
||||
# 单点登录,超过了自动退出
|
||||
if settings.USER_LOGIN_SINGLE_MACHINE_ENABLED:
|
||||
user_id = 'single_machine_login_' + str(user.id)
|
||||
session_key = cache.get(user_id)
|
||||
|
|
|
@ -160,7 +160,7 @@
|
|||
{% bootstrap_field form.challenge show_label=False %}
|
||||
{% elif form.mfa_type %}
|
||||
<div class="form-group" style="display: flex">
|
||||
{% include '_mfa_otp_login.html' %}
|
||||
{% include '_mfa_login_field.html' %}
|
||||
</div>
|
||||
{% elif form.captcha %}
|
||||
<div class="captch-field">
|
||||
|
@ -208,6 +208,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</body>
|
||||
{% include '_foot_js.html' %}
|
||||
<script type="text/javascript" src="/static/js/plugins/jsencrypt/jsencrypt.min.js"></script>
|
||||
<script>
|
||||
function encryptLoginPassword(password, rsaPublicKey) {
|
||||
|
|
|
@ -13,19 +13,18 @@
|
|||
<p class="red-fonts">{{ form.code.errors.as_text }}</p>
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
{% include '_mfa_otp_login.html' %}
|
||||
{% include '_mfa_login_field.html' %}
|
||||
</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">
|
||||
<style>
|
||||
.mfa-div {
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -25,10 +25,11 @@ urlpatterns = [
|
|||
|
||||
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
|
||||
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
|
||||
path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'),
|
||||
path('mfa/select/', api.MFASelectTypeApi.as_view(), name='mfa-select'),
|
||||
path('mfa/verify/', api.MFAChallengeVerifyApi.as_view(), name='mfa-verify'),
|
||||
path('mfa/challenge/', api.MFAChallengeVerifyApi.as_view(), name='mfa-challenge'),
|
||||
path('mfa/select/', api.MFASendCodeApi.as_view(), name='mfa-select'),
|
||||
path('mfa/send-code/', api.MFASendCodeApi.as_view(), name='mfa-send-codej'),
|
||||
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
|
||||
path('sms/verify-code/send/', api.SendSMSVerifyCodeApi.as_view(), name='sms-verify-code-send'),
|
||||
path('password/verify/', api.UserPasswordVerifyApi.as_view(), name='user-password-verify'),
|
||||
path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'),
|
||||
]
|
||||
|
|
|
@ -12,7 +12,7 @@ app_name = 'authentication'
|
|||
urlpatterns = [
|
||||
# login
|
||||
path('login/', non_atomic_requests(views.UserLoginView.as_view()), name='login'),
|
||||
path('login/otp/', views.UserLoginOtpView.as_view(), name='login-otp'),
|
||||
path('login/mfa/', views.UserLoginMFAView.as_view(), name='login-mfa'),
|
||||
path('login/wait-confirm/', views.UserLoginWaitConfirmView.as_view(), name='login-wait-confirm'),
|
||||
path('login/guard/', views.UserLoginGuardView.as_view(), name='login-guard'),
|
||||
path('logout/', views.UserLogoutView.as_view(), name='logout'),
|
||||
|
@ -42,14 +42,15 @@ urlpatterns = [
|
|||
|
||||
# Profile
|
||||
path('profile/pubkey/generate/', users_view.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'),
|
||||
path('profile/mfa/', users_view.MFASettingView.as_view(), name='user-mfa-setting'),
|
||||
|
||||
# OTP Setting
|
||||
path('profile/otp/enable/start/', users_view.UserOtpEnableStartView.as_view(), name='user-otp-enable-start'),
|
||||
path('profile/otp/enable/install-app/', users_view.UserOtpEnableInstallAppView.as_view(),
|
||||
name='user-otp-enable-install-app'),
|
||||
path('profile/otp/enable/bind/', users_view.UserOtpEnableBindView.as_view(), name='user-otp-enable-bind'),
|
||||
path('profile/otp/disable/authentication/', users_view.UserDisableMFAView.as_view(),
|
||||
name='user-otp-disable-authentication'),
|
||||
path('profile/otp/update/', users_view.UserOtpUpdateView.as_view(), name='user-otp-update'),
|
||||
path('profile/otp/settings-success/', users_view.UserOtpSettingsSuccessView.as_view(), name='user-otp-settings-success'),
|
||||
path('profile/otp/disable/', users_view.UserOtpDisableView.as_view(),
|
||||
name='user-otp-disable'),
|
||||
path('first-login/', users_view.UserFirstLoginView.as_view(), name='user-first-login'),
|
||||
|
||||
# openid
|
||||
|
|
|
@ -122,16 +122,16 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||
self.request.session.set_test_cookie()
|
||||
return self.render_to_response(context)
|
||||
except (
|
||||
errors.PasswdTooSimple,
|
||||
errors.PasswordTooSimple,
|
||||
errors.PasswordRequireResetError,
|
||||
errors.PasswdNeedUpdate,
|
||||
errors.PasswordNeedUpdate,
|
||||
errors.OTPBindRequiredError
|
||||
) as e:
|
||||
return redirect(e.url)
|
||||
except (
|
||||
errors.MFAFailedError,
|
||||
errors.BlockMFAError,
|
||||
errors.OTPCodeRequiredError,
|
||||
errors.MFACodeRequiredError,
|
||||
errors.SMSCodeRequiredError,
|
||||
errors.UserPhoneNotSet
|
||||
) as e:
|
||||
|
@ -199,7 +199,7 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||
'demo_mode': os.environ.get("DEMO_MODE"),
|
||||
'auth_methods': self.get_support_auth_methods(),
|
||||
'forgot_password_url': self.get_forgot_password_url(),
|
||||
'methods': self.get_user_mfa_methods(),
|
||||
**self.get_user_mfa_context(self.request.user)
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
@ -208,7 +208,7 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||
class UserLoginGuardView(mixins.AuthMixin, RedirectView):
|
||||
redirect_field_name = 'next'
|
||||
login_url = reverse_lazy('authentication:login')
|
||||
login_otp_url = reverse_lazy('authentication:login-otp')
|
||||
login_mfa_url = reverse_lazy('authentication:login-mfa')
|
||||
login_confirm_url = reverse_lazy('authentication:login-wait-confirm')
|
||||
|
||||
def format_redirect_url(self, url):
|
||||
|
@ -229,15 +229,16 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
|
|||
user = self.check_user_auth_if_need()
|
||||
self.check_user_mfa_if_need(user)
|
||||
self.check_user_login_confirm_if_need(user)
|
||||
except (errors.CredentialError, errors.SessionEmptyError):
|
||||
except (errors.CredentialError, errors.SessionEmptyError) as e:
|
||||
print("Error: ", e)
|
||||
return self.format_redirect_url(self.login_url)
|
||||
except errors.MFARequiredError:
|
||||
return self.format_redirect_url(self.login_otp_url)
|
||||
return self.format_redirect_url(self.login_mfa_url)
|
||||
except errors.LoginConfirmBaseError:
|
||||
return self.format_redirect_url(self.login_confirm_url)
|
||||
except errors.MFAUnsetError as e:
|
||||
return e.url
|
||||
except errors.PasswdTooSimple as e:
|
||||
except errors.PasswordTooSimple as e:
|
||||
return e.url
|
||||
else:
|
||||
self.login_it(user)
|
||||
|
|
|
@ -3,32 +3,39 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
from django.views.generic.edit import FormView
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
from .. import forms, errors, mixins
|
||||
from .utils import redirect_to_guard_view
|
||||
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
__all__ = ['UserLoginOtpView']
|
||||
__all__ = ['UserLoginMFAView']
|
||||
|
||||
|
||||
class UserLoginOtpView(mixins.AuthMixin, FormView):
|
||||
template_name = 'authentication/login_otp.html'
|
||||
class UserLoginMFAView(mixins.AuthMixin, FormView):
|
||||
template_name = 'authentication/login_mfa.html'
|
||||
form_class = forms.UserCheckOtpCodeForm
|
||||
redirect_field_name = 'next'
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
try:
|
||||
self.get_user_from_session()
|
||||
except errors.SessionEmptyError:
|
||||
return redirect_to_guard_view()
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
code = form.cleaned_data.get('code')
|
||||
mfa_type = form.cleaned_data.get('mfa_type')
|
||||
|
||||
try:
|
||||
self.check_user_mfa(code, mfa_type)
|
||||
self._do_check_user_mfa(code, mfa_type)
|
||||
return redirect_to_guard_view()
|
||||
except (errors.MFAFailedError, errors.BlockMFAError) as e:
|
||||
form.add_error('code', e.msg)
|
||||
return super().form_invalid(form)
|
||||
except errors.SessionEmptyError:
|
||||
return redirect_to_guard_view()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
import traceback
|
||||
|
@ -37,6 +44,7 @@ class UserLoginOtpView(mixins.AuthMixin, FormView):
|
|||
|
||||
def get_context_data(self, **kwargs):
|
||||
user = self.get_user_from_session()
|
||||
methods = self.get_user_mfa_methods(user)
|
||||
kwargs.update({'methods': methods})
|
||||
mfa_context = self.get_user_mfa_context(user)
|
||||
kwargs.update(mfa_context)
|
||||
return kwargs
|
||||
|
||||
|
|
|
@ -1,65 +1,2 @@
|
|||
from collections import OrderedDict
|
||||
import importlib
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db.models import TextChoices
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.exceptions import JMSException
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class BACKENDS(TextChoices):
|
||||
ALIBABA = 'alibaba', _('Alibaba cloud')
|
||||
TENCENT = 'tencent', _('Tencent cloud')
|
||||
|
||||
|
||||
class BaseSMSClient:
|
||||
"""
|
||||
短信终端的基类
|
||||
"""
|
||||
|
||||
SIGN_AND_TMPL_SETTING_FIELD_PREFIX: str
|
||||
|
||||
@classmethod
|
||||
def new_from_settings(cls):
|
||||
raise NotImplementedError
|
||||
|
||||
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SMS:
|
||||
client: BaseSMSClient
|
||||
|
||||
def __init__(self, backend=None):
|
||||
backend = backend or settings.SMS_BACKEND
|
||||
if backend not in BACKENDS:
|
||||
raise JMSException(
|
||||
code='sms_provider_not_support',
|
||||
detail=_('SMS provider not support: {}').format(backend)
|
||||
)
|
||||
m = importlib.import_module(f'.{backend or settings.SMS_BACKEND}', __package__)
|
||||
self.client = m.client.new_from_settings()
|
||||
|
||||
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
|
||||
return self.client.send_sms(
|
||||
phone_numbers=phone_numbers,
|
||||
sign_name=sign_name,
|
||||
template_code=template_code,
|
||||
template_param=template_param,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def send_verify_code(self, phone_number, code):
|
||||
sign_name = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_SIGN_NAME')
|
||||
template_code = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_TEMPLATE_CODE')
|
||||
|
||||
if not (sign_name and template_code):
|
||||
raise JMSException(
|
||||
code='verify_code_sign_tmpl_invalid',
|
||||
detail=_('SMS verification code signature or template invalid')
|
||||
)
|
||||
return self.send_sms([phone_number], sign_name, template_code, OrderedDict(code=code))
|
||||
from .endpoint import SMS, BACKENDS
|
||||
from .utils import SendAndVerifySMSUtil
|
||||
|
|
|
@ -9,7 +9,7 @@ from Tea.exceptions import TeaException
|
|||
|
||||
from common.utils import get_logger
|
||||
from common.exceptions import JMSException
|
||||
from . import BaseSMSClient
|
||||
from .base import BaseSMSClient
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class BaseSMSClient:
|
||||
"""
|
||||
短信终端的基类
|
||||
"""
|
||||
|
||||
SIGN_AND_TMPL_SETTING_FIELD_PREFIX: str
|
||||
|
||||
@classmethod
|
||||
def new_from_settings(cls):
|
||||
raise NotImplementedError
|
||||
|
||||
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
from collections import OrderedDict
|
||||
import importlib
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db.models import TextChoices
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.exceptions import JMSException
|
||||
from .base import BaseSMSClient
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class BACKENDS(TextChoices):
|
||||
ALIBABA = 'alibaba', _('Alibaba cloud')
|
||||
TENCENT = 'tencent', _('Tencent cloud')
|
||||
|
||||
|
||||
class SMS:
|
||||
client: BaseSMSClient
|
||||
|
||||
def __init__(self, backend=None):
|
||||
backend = backend or settings.SMS_BACKEND
|
||||
if backend not in BACKENDS:
|
||||
raise JMSException(
|
||||
code='sms_provider_not_support',
|
||||
detail=_('SMS provider not support: {}').format(backend)
|
||||
)
|
||||
m = importlib.import_module(f'.{backend or settings.SMS_BACKEND}', __package__)
|
||||
self.client = m.client.new_from_settings()
|
||||
|
||||
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
|
||||
return self.client.send_sms(
|
||||
phone_numbers=phone_numbers,
|
||||
sign_name=sign_name,
|
||||
template_code=template_code,
|
||||
template_param=template_param,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def send_verify_code(self, phone_number, code):
|
||||
sign_name = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_SIGN_NAME')
|
||||
template_code = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_TEMPLATE_CODE')
|
||||
|
||||
if not (sign_name and template_code):
|
||||
raise JMSException(
|
||||
code='verify_code_sign_tmpl_invalid',
|
||||
detail=_('SMS verification code signature or template invalid')
|
||||
)
|
||||
return self.send_sms([phone_number], sign_name, template_code, OrderedDict(code=code))
|
|
@ -10,7 +10,8 @@ from tencentcloud.sms.v20210111 import sms_client, models
|
|||
# 导入可选配置类
|
||||
from tencentcloud.common.profile.client_profile import ClientProfile
|
||||
from tencentcloud.common.profile.http_profile import HttpProfile
|
||||
from . import BaseSMSClient
|
||||
|
||||
from .base import BaseSMSClient
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import random
|
|||
from django.core.cache import cache
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.sdk.sms import SMS
|
||||
from .endpoint import SMS
|
||||
from common.utils import get_logger
|
||||
from common.exceptions import JMSException
|
||||
|
||||
|
@ -28,32 +28,24 @@ class CodeSendTooFrequently(JMSException):
|
|||
super().__init__(detail=self.default_detail.format(ttl))
|
||||
|
||||
|
||||
class VerifyCodeUtil:
|
||||
KEY_TMPL = 'auth-verify_code-{}'
|
||||
class SendAndVerifySMSUtil:
|
||||
KEY_TMPL = 'auth-verify-code-{}'
|
||||
TIMEOUT = 60
|
||||
|
||||
def __init__(self, account, key_suffix=None, timeout=None):
|
||||
self.account = account
|
||||
self.key_suffix = key_suffix
|
||||
def __init__(self, phone, key_suffix=None, timeout=None):
|
||||
self.phone = phone
|
||||
self.code = ''
|
||||
self.timeout = timeout or self.TIMEOUT
|
||||
self.key_suffix = key_suffix or str(phone)
|
||||
self.key = self.KEY_TMPL.format(key_suffix)
|
||||
|
||||
if key_suffix is not None:
|
||||
self.key = self.KEY_TMPL.format(key_suffix)
|
||||
else:
|
||||
self.key = self.KEY_TMPL.format(account)
|
||||
self.timeout = self.TIMEOUT if timeout is None else timeout
|
||||
|
||||
def touch(self):
|
||||
def gen_and_send(self):
|
||||
"""
|
||||
生成,保存,发送
|
||||
"""
|
||||
ttl = self.ttl()
|
||||
if ttl > 0:
|
||||
raise CodeSendTooFrequently(ttl)
|
||||
try:
|
||||
self.generate()
|
||||
self.save()
|
||||
self.send()
|
||||
code = self.generate()
|
||||
self.send(code)
|
||||
except JMSException:
|
||||
self.clear()
|
||||
raise
|
||||
|
@ -66,19 +58,18 @@ class VerifyCodeUtil:
|
|||
def clear(self):
|
||||
cache.delete(self.key)
|
||||
|
||||
def save(self):
|
||||
cache.set(self.key, self.code, self.timeout)
|
||||
|
||||
def send(self):
|
||||
def send(self, code):
|
||||
"""
|
||||
发送信息的方法,如果有错误直接抛出 api 异常
|
||||
"""
|
||||
account = self.account
|
||||
code = self.code
|
||||
|
||||
ttl = self.ttl()
|
||||
if ttl > 0:
|
||||
logger.error('Send sms too frequently, delay {}'.format(ttl))
|
||||
raise CodeSendTooFrequently(ttl)
|
||||
sms = SMS()
|
||||
sms.send_verify_code(account, code)
|
||||
logger.info(f'Send sms verify code: account={account} code={code}')
|
||||
sms.send_verify_code(self.phone, code)
|
||||
cache.set(self.key, self.code, self.timeout)
|
||||
logger.info(f'Send sms verify code to {self.phone}: {code}')
|
||||
|
||||
def verify(self, code):
|
||||
right = cache.get(self.key)
|
|
@ -106,7 +106,7 @@ TASK_LOG_KEEP_DAYS = CONFIG.TASK_LOG_KEEP_DAYS
|
|||
ORG_CHANGE_TO_URL = CONFIG.ORG_CHANGE_TO_URL
|
||||
WINDOWS_SKIP_ALL_MANUAL_PASSWORD = CONFIG.WINDOWS_SKIP_ALL_MANUAL_PASSWORD
|
||||
|
||||
AUTH_EXPIRED_SECONDS = 60 * 5
|
||||
AUTH_EXPIRED_SECONDS = 60 * 10
|
||||
|
||||
CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED = CONFIG.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3cb74767fc92b67608deb32d27bf945b7fd4ad46fc02f0cc5ef4cf4a42ebcd10
|
||||
size 91465
|
||||
oid sha256:925c5a219a4ee6835ad59e3b8e9f7ea5074ee3df6527c0f73ef1a50eaedaf59c
|
||||
size 91777
|
||||
|
|
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: JumpServer 0.3.3\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 15:08+0800\n"
|
||||
"POT-Creation-Date: 2021-11-10 10:53+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:597
|
||||
#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:541
|
||||
#: 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:630 xpack/plugins/change_auth_plan/models/base.py:41
|
||||
#: users/models/user.py:574 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"
|
||||
|
@ -86,8 +86,8 @@ msgstr "登录复核"
|
|||
#: templates/index.html:78 terminal/backends/command/models.py:18
|
||||
#: terminal/backends/command/serializers.py:12 terminal/models/session.py:38
|
||||
#: terminal/notifications.py:90 terminal/notifications.py:138
|
||||
#: tickets/models/comment.py:17 users/const.py:14 users/models/user.py:173
|
||||
#: users/models/user.py:801 users/models/user.py:827
|
||||
#: tickets/models/comment.py:17 users/const.py:14 users/models/user.py:169
|
||||
#: users/models/user.py:745 users/models/user.py:771
|
||||
#: users/serializers/group.py:19
|
||||
#: users/templates/users/user_asset_permission.html:38
|
||||
#: users/templates/users/user_asset_permission.html:64
|
||||
|
@ -162,7 +162,7 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. "
|
|||
#: assets/models/base.py:176 assets/models/gathered_user.py:15
|
||||
#: audits/models.py:105 authentication/forms.py:15 authentication/forms.py:17
|
||||
#: authentication/templates/authentication/_msg_different_city.html:9
|
||||
#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:595
|
||||
#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:539
|
||||
#: users/templates/users/_msg_user_created.html:12
|
||||
#: users/templates/users/_select_user_modal.html:14
|
||||
#: xpack/plugins/change_auth_plan/models/asset.py:35
|
||||
|
@ -323,7 +323,7 @@ msgid "Attrs"
|
|||
msgstr ""
|
||||
|
||||
#: applications/models/application.py:183
|
||||
#: perms/models/application_permission.py:27 users/models/user.py:174
|
||||
#: perms/models/application_permission.py:27 users/models/user.py:170
|
||||
msgid "Application"
|
||||
msgstr "应用程序"
|
||||
|
||||
|
@ -402,7 +402,7 @@ msgstr "目标URL"
|
|||
#: authentication/templates/authentication/login.html:151
|
||||
#: settings/serializers/auth/ldap.py:44 users/forms/profile.py:21
|
||||
#: users/templates/users/_msg_user_created.html:13
|
||||
#: users/templates/users/user_otp_check_password.html:13
|
||||
#: users/templates/users/user_otp_check_password.html:15
|
||||
#: users/templates/users/user_password_update.html:43
|
||||
#: users/templates/users/user_password_verify.html:18
|
||||
#: xpack/plugins/change_auth_plan/models/base.py:39
|
||||
|
@ -553,7 +553,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:638
|
||||
#: orgs/models.py:437 perms/models/base.py:51 users/models/user.py:582
|
||||
#: 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
|
||||
|
@ -566,7 +566,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:828 xpack/plugins/cloud/models.py:122
|
||||
#: users/models/user.py:772 xpack/plugins/cloud/models.py:122
|
||||
msgid "Date created"
|
||||
msgstr "创建日期"
|
||||
|
||||
|
@ -621,7 +621,7 @@ msgstr "带宽"
|
|||
msgid "Contact"
|
||||
msgstr "联系人"
|
||||
|
||||
#: assets/models/cluster.py:22 users/models/user.py:616
|
||||
#: assets/models/cluster.py:22 users/models/user.py:560
|
||||
msgid "Phone"
|
||||
msgstr "手机"
|
||||
|
||||
|
@ -647,7 +647,7 @@ msgid "Default"
|
|||
msgstr "默认"
|
||||
|
||||
#: assets/models/cluster.py:36 assets/models/label.py:14
|
||||
#: users/models/user.py:813
|
||||
#: users/models/user.py:757
|
||||
msgid "System"
|
||||
msgstr "系统"
|
||||
|
||||
|
@ -1219,8 +1219,8 @@ 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:619
|
||||
#: authentication/templates/authentication/login_mfa.html:6
|
||||
#: users/forms/profile.py:64 users/models/user.py:563
|
||||
#: users/serializers/profile.py:102
|
||||
msgid "MFA"
|
||||
msgstr "多因子认证"
|
||||
|
@ -1299,12 +1299,12 @@ msgid "Auth Token"
|
|||
msgstr "认证令牌"
|
||||
|
||||
#: audits/signals_handler.py:68 authentication/views/login.py:169
|
||||
#: notifications/backends/__init__.py:11 users/models/user.py:652
|
||||
#: notifications/backends/__init__.py:11 users/models/user.py:596
|
||||
msgid "WeCom"
|
||||
msgstr "企业微信"
|
||||
|
||||
#: audits/signals_handler.py:69 authentication/views/login.py:175
|
||||
#: notifications/backends/__init__.py:12 users/models/user.py:653
|
||||
#: notifications/backends/__init__.py:12 users/models/user.py:597
|
||||
msgid "DingTalk"
|
||||
msgstr "钉钉"
|
||||
|
||||
|
@ -1495,9 +1495,11 @@ msgstr "{ApplicationPermission} 移除 {SystemUser}"
|
|||
msgid "Invalid token"
|
||||
msgstr "无效的令牌"
|
||||
|
||||
#: authentication/api/mfa.py:81
|
||||
#: authentication/api/mfa.py:102
|
||||
#, fuzzy
|
||||
#| msgid "Code is invalid, "
|
||||
msgid "Code is invalid"
|
||||
msgstr "Code无效"
|
||||
msgstr "验证码无效"
|
||||
|
||||
#: authentication/backends/api.py:67
|
||||
msgid "Invalid signature header. No credentials provided."
|
||||
|
@ -1550,59 +1552,59 @@ msgstr ""
|
|||
msgid "Invalid token or cache refreshed."
|
||||
msgstr ""
|
||||
|
||||
#: authentication/errors.py:27
|
||||
#: authentication/errors.py:26
|
||||
msgid "Username/password check failed"
|
||||
msgstr "用户名/密码 校验失败"
|
||||
|
||||
#: authentication/errors.py:28
|
||||
#: authentication/errors.py:27
|
||||
msgid "Password decrypt failed"
|
||||
msgstr "密码解密失败"
|
||||
|
||||
#: authentication/errors.py:29
|
||||
#: authentication/errors.py:28
|
||||
msgid "MFA failed"
|
||||
msgstr "多因子认证失败"
|
||||
|
||||
#: authentication/errors.py:30
|
||||
#: authentication/errors.py:29
|
||||
msgid "MFA unset"
|
||||
msgstr "多因子认证没有设定"
|
||||
|
||||
#: authentication/errors.py:31
|
||||
#: authentication/errors.py:30
|
||||
msgid "Username does not exist"
|
||||
msgstr "用户名不存在"
|
||||
|
||||
#: authentication/errors.py:32
|
||||
#: authentication/errors.py:31
|
||||
msgid "Password expired"
|
||||
msgstr "密码已过期"
|
||||
|
||||
#: authentication/errors.py:33
|
||||
#: authentication/errors.py:32
|
||||
msgid "Disabled or expired"
|
||||
msgstr "禁用或失效"
|
||||
|
||||
#: authentication/errors.py:34
|
||||
#: authentication/errors.py:33
|
||||
msgid "This account is inactive."
|
||||
msgstr "此账户已禁用"
|
||||
|
||||
#: authentication/errors.py:35
|
||||
#: authentication/errors.py:34
|
||||
msgid "This account is expired"
|
||||
msgstr "此账户已过期"
|
||||
|
||||
#: authentication/errors.py:36
|
||||
#: authentication/errors.py:35
|
||||
msgid "Auth backend not match"
|
||||
msgstr "没有匹配到认证后端"
|
||||
|
||||
#: authentication/errors.py:37
|
||||
#: authentication/errors.py:36
|
||||
msgid "ACL is not allowed"
|
||||
msgstr "ACL 不被允许"
|
||||
|
||||
#: authentication/errors.py:38
|
||||
#: authentication/errors.py:37
|
||||
msgid "Only local users are allowed"
|
||||
msgstr "仅允许本地用户"
|
||||
|
||||
#: authentication/errors.py:48
|
||||
#: authentication/errors.py:47
|
||||
msgid "No session found, check your cookie"
|
||||
msgstr "会话已变更,刷新页面"
|
||||
|
||||
#: authentication/errors.py:50
|
||||
#: authentication/errors.py:49
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"The username or password you entered is incorrect, please enter it again. "
|
||||
|
@ -1612,105 +1614,85 @@ msgstr ""
|
|||
"您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将"
|
||||
"被临时 锁定 {block_time} 分钟)"
|
||||
|
||||
#: authentication/errors.py:56 authentication/errors.py:60
|
||||
#: authentication/errors.py:55 authentication/errors.py:59
|
||||
msgid ""
|
||||
"The account has been locked (please contact admin to unlock it or try again "
|
||||
"after {} minutes)"
|
||||
msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)"
|
||||
|
||||
#: authentication/errors.py:64
|
||||
#: authentication/errors.py:63
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"One-time password invalid, or ntp sync server time, You can also try "
|
||||
"{times_try} times (The account will be temporarily locked for {block_time} "
|
||||
"minutes)"
|
||||
"{error},You can also try {times_try} times (The account will be temporarily "
|
||||
"locked for {block_time} minutes)"
|
||||
msgstr ""
|
||||
"虚拟MFA 不正确,或者服务器端时间不对。 您还可以尝试 {times_try} 次(账号将被"
|
||||
"临时 锁定 {block_time} 分钟)"
|
||||
"{error},您还可以尝试 {times_try} 次(账号将被临时锁定 {block_time} 分钟)"
|
||||
|
||||
#: authentication/errors.py:69
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"SMS verify code invalid,You can also try {times_try} times (The account will "
|
||||
"be temporarily locked for {block_time} minutes)"
|
||||
msgstr ""
|
||||
"短信验证码不正确。 您还可以尝试 {times_try} 次(账号将被临时 锁定 "
|
||||
"{block_time} 分钟)"
|
||||
|
||||
#: authentication/errors.py:74
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"The MFA type({mfa_type}) is not supported, You can also try {times_try} "
|
||||
"times (The account will be temporarily locked for {block_time} minutes)"
|
||||
msgstr ""
|
||||
"该({mfa_type}) MFA 类型不支持, 您还可以尝试 {times_try} 次(账号将被临时 锁"
|
||||
"定 {block_time} 分钟)"
|
||||
|
||||
#: authentication/errors.py:79
|
||||
#: authentication/errors.py:67
|
||||
msgid "MFA required"
|
||||
msgstr "需要多因子认证"
|
||||
|
||||
#: authentication/errors.py:80
|
||||
#: authentication/errors.py:68
|
||||
msgid "MFA not set, please set it first"
|
||||
msgstr "多因子认证没有设置,请先完成设置"
|
||||
|
||||
#: authentication/errors.py:81
|
||||
#: authentication/errors.py:69
|
||||
msgid "OTP not set, please set it first"
|
||||
msgstr "OTP认证没有设置,请先完成设置"
|
||||
|
||||
#: authentication/errors.py:82
|
||||
#: authentication/errors.py:70
|
||||
msgid "Login confirm required"
|
||||
msgstr "需要登录复核"
|
||||
|
||||
#: authentication/errors.py:83
|
||||
#: authentication/errors.py:71
|
||||
msgid "Wait login confirm ticket for accept"
|
||||
msgstr "等待登录复核处理"
|
||||
|
||||
#: authentication/errors.py:84
|
||||
#: authentication/errors.py:72
|
||||
msgid "Login confirm ticket was {}"
|
||||
msgstr "登录复核 {}"
|
||||
|
||||
#: authentication/errors.py:265
|
||||
#: authentication/errors.py:243
|
||||
msgid "IP is not allowed"
|
||||
msgstr "来源 IP 不被允许登录"
|
||||
|
||||
#: authentication/errors.py:272
|
||||
#: authentication/errors.py:250
|
||||
msgid "Time Period is not allowed"
|
||||
msgstr "该 时间段 不被允许登录"
|
||||
|
||||
#: authentication/errors.py:305
|
||||
#: authentication/errors.py:283
|
||||
msgid "SSO auth closed"
|
||||
msgstr "SSO 认证关闭了"
|
||||
|
||||
#: authentication/errors.py:310 authentication/mixins.py:345
|
||||
#: authentication/errors.py:288 authentication/mixins.py:344
|
||||
msgid "Your password is too simple, please change it for security"
|
||||
msgstr "你的密码过于简单,为了安全,请修改"
|
||||
|
||||
#: authentication/errors.py:319 authentication/mixins.py:352
|
||||
#: authentication/errors.py:297 authentication/mixins.py:351
|
||||
msgid "You should to change your password before login"
|
||||
msgstr "登录完成前,请先修改密码"
|
||||
|
||||
#: authentication/errors.py:328 authentication/mixins.py:359
|
||||
#: authentication/errors.py:306 authentication/mixins.py:358
|
||||
msgid "Your password has expired, please reset before logging in"
|
||||
msgstr "您的密码已过期,先修改再登录"
|
||||
|
||||
#: authentication/errors.py:362
|
||||
#: authentication/errors.py:340
|
||||
msgid "Your password is invalid"
|
||||
msgstr "您的密码无效"
|
||||
|
||||
#: authentication/errors.py:368
|
||||
#: authentication/errors.py:346
|
||||
msgid "No upload or download permission"
|
||||
msgstr "没有上传下载权限"
|
||||
|
||||
#: authentication/errors.py:384 templates/_mfa_otp_login.html:37
|
||||
#: authentication/errors.py:358
|
||||
msgid "Please enter MFA code"
|
||||
msgstr "请输入6位动态安全码"
|
||||
|
||||
#: authentication/errors.py:387 templates/_mfa_otp_login.html:38
|
||||
#: authentication/errors.py:362
|
||||
msgid "Please enter SMS code"
|
||||
msgstr "请输入短信验证码"
|
||||
|
||||
#: authentication/errors.py:390 users/exceptions.py:15
|
||||
#: authentication/errors.py:366 users/exceptions.py:15
|
||||
msgid "Phone not set"
|
||||
msgstr "手机号没有设置"
|
||||
|
||||
|
@ -1734,14 +1716,62 @@ msgstr "多因子认证验证码"
|
|||
msgid "Dynamic code"
|
||||
msgstr "动态码"
|
||||
|
||||
#: authentication/mixins.py:335
|
||||
msgid "Please change your password"
|
||||
msgstr "请修改密码"
|
||||
#: authentication/mfa/base.py:7
|
||||
msgid "Please input security code"
|
||||
msgstr "请输入 6 位动态安全码"
|
||||
|
||||
#: authentication/mixins.py:523
|
||||
#: authentication/mfa/otp.py:7
|
||||
msgid "OTP code invalid, or server time error"
|
||||
msgstr "MFA (OTP) 验证码错误,或者服务器端时间不对"
|
||||
|
||||
#: authentication/mfa/otp.py:12
|
||||
msgid "OTP"
|
||||
msgstr "MFA (OTP)"
|
||||
|
||||
#: authentication/mfa/otp.py:47
|
||||
msgid "Virtual OTP based MFA"
|
||||
msgstr "虚拟 MFA (OTP)"
|
||||
|
||||
#: authentication/mfa/radius.py:7
|
||||
msgid "Radius verify code invalid"
|
||||
msgstr "Radius 校验失败"
|
||||
|
||||
#: authentication/mfa/radius.py:12
|
||||
msgid "Radius MFA"
|
||||
msgstr "Radius MFA"
|
||||
|
||||
#: authentication/mfa/radius.py:43
|
||||
msgid "Radius global enabled, cannot disable"
|
||||
msgstr "Radius MFA 全局开启,无法被禁用"
|
||||
|
||||
#: authentication/mfa/sms.py:7
|
||||
msgid "SMS verify code invalid"
|
||||
msgstr "短信验证码校验失败"
|
||||
|
||||
#: authentication/mfa/sms.py:12
|
||||
msgid "SMS"
|
||||
msgstr "短信"
|
||||
|
||||
#: authentication/mfa/sms.py:13
|
||||
msgid "SMS verification code"
|
||||
msgstr "短信验证码"
|
||||
|
||||
#: authentication/mfa/sms.py:53
|
||||
msgid "Set phone number to enable"
|
||||
msgstr "设置手机号码启用"
|
||||
|
||||
#: authentication/mfa/sms.py:57
|
||||
msgid "Clear phone number to disable"
|
||||
msgstr "清空手机号码禁用"
|
||||
|
||||
#: authentication/mixins.py:305
|
||||
msgid "The MFA type({}) is not supported"
|
||||
msgstr "该 MFA 方法 ({}) 不被支持"
|
||||
|
||||
#: authentication/mixins.py:334
|
||||
msgid "Please change your password"
|
||||
msgstr "请修改密码"
|
||||
|
||||
#: authentication/models.py:37
|
||||
msgid "Private Token"
|
||||
msgstr "SSH密钥"
|
||||
|
@ -1754,18 +1784,6 @@ msgstr "过期时间"
|
|||
msgid "Different city login reminder"
|
||||
msgstr "异地登录提醒"
|
||||
|
||||
#: authentication/sms_verify_code.py:15
|
||||
msgid "The verification code has expired. Please resend it"
|
||||
msgstr "验证码已过期,请重新发送"
|
||||
|
||||
#: authentication/sms_verify_code.py:20
|
||||
msgid "The verification code is incorrect"
|
||||
msgstr "验证码错误"
|
||||
|
||||
#: authentication/sms_verify_code.py:25
|
||||
msgid "Please wait {} seconds before sending"
|
||||
msgstr "请在 {} 秒后发送"
|
||||
|
||||
#: authentication/templates/authentication/_access_key_modal.html:6
|
||||
msgid "API key list"
|
||||
msgstr "API Key列表"
|
||||
|
@ -1799,14 +1817,16 @@ msgid "Show"
|
|||
msgstr "显示"
|
||||
|
||||
#: authentication/templates/authentication/_access_key_modal.html:66
|
||||
#: settings/serializers/security.py:25 users/models/user.py:462
|
||||
#: users/serializers/profile.py:99
|
||||
#: users/templates/users/user_verify_mfa.html:32
|
||||
#: settings/serializers/security.py:25 users/models/user.py:458
|
||||
#: users/serializers/profile.py:99 users/templates/users/mfa_setting.html:60
|
||||
#: users/templates/users/user_verify_mfa.html:36
|
||||
msgid "Disable"
|
||||
msgstr "禁用"
|
||||
|
||||
#: authentication/templates/authentication/_access_key_modal.html:67
|
||||
#: users/models/user.py:463 users/serializers/profile.py:100
|
||||
#: users/models/user.py:459 users/serializers/profile.py:100
|
||||
#: users/templates/users/mfa_setting.html:26
|
||||
#: users/templates/users/mfa_setting.html:67
|
||||
msgid "Enable"
|
||||
msgstr "启用"
|
||||
|
||||
|
@ -1931,15 +1951,15 @@ msgstr "登录"
|
|||
msgid "More login options"
|
||||
msgstr "更多登录方式"
|
||||
|
||||
#: authentication/templates/authentication/login_otp.html:19
|
||||
#: users/templates/users/user_otp_check_password.html:16
|
||||
#: authentication/templates/authentication/login_mfa.html:19
|
||||
#: users/templates/users/user_otp_check_password.html:18
|
||||
#: users/templates/users/user_otp_enable_bind.html:24
|
||||
#: users/templates/users/user_otp_enable_install_app.html:29
|
||||
#: users/templates/users/user_verify_mfa.html:26
|
||||
#: users/templates/users/user_verify_mfa.html:30
|
||||
msgid "Next"
|
||||
msgstr "下一步"
|
||||
|
||||
#: authentication/templates/authentication/login_otp.html:21
|
||||
#: authentication/templates/authentication/login_mfa.html:22
|
||||
msgid "Can't provide security? Please contact the administrator!"
|
||||
msgstr "如果不能提供多因子认证验证码,请联系管理员!"
|
||||
|
||||
|
@ -2055,11 +2075,11 @@ msgid "Please enable cookies and try again."
|
|||
msgstr "设置你的浏览器支持cookie"
|
||||
|
||||
#: authentication/views/login.py:181 notifications/backends/__init__.py:14
|
||||
#: users/models/user.py:654
|
||||
#: users/models/user.py:598
|
||||
msgid "FeiShu"
|
||||
msgstr "飞书"
|
||||
|
||||
#: authentication/views/login.py:269
|
||||
#: authentication/views/login.py:270
|
||||
msgid ""
|
||||
"Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>\n"
|
||||
" Don't close this page"
|
||||
|
@ -2067,15 +2087,15 @@ msgstr ""
|
|||
"等待 <b>{}</b> 确认, 你也可以复制链接发给他/她 <br/>\n"
|
||||
" 不要关闭本页面"
|
||||
|
||||
#: authentication/views/login.py:274
|
||||
#: authentication/views/login.py:275
|
||||
msgid "No ticket found"
|
||||
msgstr "没有发现工单"
|
||||
|
||||
#: authentication/views/login.py:306
|
||||
#: authentication/views/login.py:307
|
||||
msgid "Logout success"
|
||||
msgstr "退出登录成功"
|
||||
|
||||
#: authentication/views/login.py:307
|
||||
#: authentication/views/login.py:308
|
||||
msgid "Logout success, return login page"
|
||||
msgstr "退出登录成功,返回到登录页面"
|
||||
|
||||
|
@ -2218,26 +2238,38 @@ msgstr "网络错误,请联系系统管理员"
|
|||
msgid "WeCom error, please contact system administrator"
|
||||
msgstr "企业微信错误,请联系系统管理员"
|
||||
|
||||
#: common/sdk/sms/__init__.py:15
|
||||
msgid "Alibaba cloud"
|
||||
msgstr "阿里云"
|
||||
|
||||
#: common/sdk/sms/__init__.py:16
|
||||
msgid "Tencent cloud"
|
||||
msgstr "腾讯云"
|
||||
|
||||
#: common/sdk/sms/__init__.py:42
|
||||
msgid "SMS provider not support: {}"
|
||||
msgstr "短信服务商不支持:{}"
|
||||
|
||||
#: common/sdk/sms/__init__.py:63
|
||||
msgid "SMS verification code signature or template invalid"
|
||||
msgstr "短信验证码签名或模版无效"
|
||||
|
||||
#: common/sdk/sms/alibaba.py:56
|
||||
msgid "Signature does not match"
|
||||
msgstr "签名不匹配"
|
||||
|
||||
#: common/sdk/sms/endpoint.py:16
|
||||
msgid "Alibaba cloud"
|
||||
msgstr "阿里云"
|
||||
|
||||
#: common/sdk/sms/endpoint.py:17
|
||||
msgid "Tencent cloud"
|
||||
msgstr "腾讯云"
|
||||
|
||||
#: common/sdk/sms/endpoint.py:28
|
||||
msgid "SMS provider not support: {}"
|
||||
msgstr "短信服务商不支持:{}"
|
||||
|
||||
#: common/sdk/sms/endpoint.py:49
|
||||
msgid "SMS verification code signature or template invalid"
|
||||
msgstr "短信验证码签名或模版无效"
|
||||
|
||||
#: common/sdk/sms/utils.py:15
|
||||
msgid "The verification code has expired. Please resend it"
|
||||
msgstr "验证码已过期,请重新发送"
|
||||
|
||||
#: common/sdk/sms/utils.py:20
|
||||
msgid "The verification code is incorrect"
|
||||
msgstr "验证码错误"
|
||||
|
||||
#: common/sdk/sms/utils.py:25
|
||||
msgid "Please wait {} seconds before sending"
|
||||
msgstr "请在 {} 秒后发送"
|
||||
|
||||
#: common/utils/geoip/utils.py:17 common/utils/geoip/utils.py:30
|
||||
msgid "Invalid ip"
|
||||
msgstr "无效IP"
|
||||
|
@ -2302,7 +2334,7 @@ msgstr ""
|
|||
"div>"
|
||||
|
||||
#: notifications/backends/__init__.py:10 users/forms/profile.py:101
|
||||
#: users/models/user.py:599
|
||||
#: users/models/user.py:543
|
||||
msgid "Email"
|
||||
msgstr "邮件"
|
||||
|
||||
|
@ -2517,7 +2549,7 @@ msgstr "组织审计员"
|
|||
msgid "GLOBAL"
|
||||
msgstr "全局组织"
|
||||
|
||||
#: orgs/models.py:434 users/models/user.py:607 users/serializers/user.py:37
|
||||
#: orgs/models.py:434 users/models/user.py:551 users/serializers/user.py:37
|
||||
#: users/templates/users/_select_user_modal.html:15
|
||||
msgid "Role"
|
||||
msgstr "角色"
|
||||
|
@ -2578,7 +2610,7 @@ msgid "Favorite"
|
|||
msgstr "收藏夹"
|
||||
|
||||
#: perms/models/base.py:47 templates/_nav.html:21 users/models/group.py:31
|
||||
#: users/models/user.py:603 users/templates/users/_select_user_modal.html:16
|
||||
#: users/models/user.py:547 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
|
||||
|
@ -2589,7 +2621,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:635
|
||||
#: users/models/user.py:579
|
||||
msgid "Date expired"
|
||||
msgstr "失效日期"
|
||||
|
||||
|
@ -3634,7 +3666,7 @@ msgstr "下载更新模版"
|
|||
msgid "Help"
|
||||
msgstr "帮助"
|
||||
|
||||
#: templates/_header_bar.html:19 templates/_without_nav_base.html:27
|
||||
#: templates/_header_bar.html:19
|
||||
msgid "Docs"
|
||||
msgstr "文档"
|
||||
|
||||
|
@ -3737,19 +3769,15 @@ 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
|
||||
#: templates/_mfa_login_field.html:28
|
||||
msgid "Send verification code"
|
||||
msgstr "发送验证码"
|
||||
|
||||
#: templates/_mfa_otp_login.html:60 templates/_mfa_otp_login.html:65
|
||||
#: templates/_mfa_login_field.html:91
|
||||
msgid "Wait: "
|
||||
msgstr "等待:"
|
||||
|
||||
#: templates/_mfa_otp_login.html:73
|
||||
#: templates/_mfa_login_field.html:101
|
||||
msgid "The verification code has been sent"
|
||||
msgstr "验证码已发送"
|
||||
|
||||
|
@ -3889,7 +3917,7 @@ msgid ""
|
|||
"Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
|
||||
msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项"
|
||||
|
||||
#: templates/_without_nav_base.html:25
|
||||
#: templates/_without_nav_base.html:26
|
||||
msgid "Home page"
|
||||
msgstr "首页"
|
||||
|
||||
|
@ -4845,11 +4873,11 @@ msgstr "点击查看"
|
|||
msgid "Could not reset self otp, use profile reset instead"
|
||||
msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置"
|
||||
|
||||
#: users/const.py:10 users/models/user.py:171
|
||||
#: users/const.py:10 users/models/user.py:167
|
||||
msgid "System administrator"
|
||||
msgstr "系统管理员"
|
||||
|
||||
#: users/const.py:11 users/models/user.py:172
|
||||
#: users/const.py:11 users/models/user.py:168
|
||||
msgid "System auditor"
|
||||
msgstr "系统审计员"
|
||||
|
||||
|
@ -4940,56 +4968,48 @@ msgstr "不能和原来的密钥相同"
|
|||
msgid "Not a valid ssh public key"
|
||||
msgstr "SSH密钥不合法"
|
||||
|
||||
#: users/forms/profile.py:160 users/models/user.py:627
|
||||
#: users/forms/profile.py:160 users/models/user.py:571
|
||||
#: users/templates/users/user_password_update.html:48
|
||||
msgid "Public key"
|
||||
msgstr "SSH公钥"
|
||||
|
||||
#: users/models/user.py:36
|
||||
msgid "One-time password"
|
||||
msgstr "一次性密码"
|
||||
|
||||
#: users/models/user.py:37
|
||||
msgid "SMS verify code"
|
||||
msgstr "短信验证码"
|
||||
|
||||
#: users/models/user.py:464
|
||||
#: users/models/user.py:460
|
||||
msgid "Force enable"
|
||||
msgstr "强制启用"
|
||||
|
||||
#: users/models/user.py:576
|
||||
#: users/models/user.py:520
|
||||
msgid "Local"
|
||||
msgstr "数据库"
|
||||
|
||||
#: users/models/user.py:610
|
||||
#: users/models/user.py:554
|
||||
msgid "Avatar"
|
||||
msgstr "头像"
|
||||
|
||||
#: users/models/user.py:613
|
||||
#: users/models/user.py:557
|
||||
msgid "Wechat"
|
||||
msgstr "微信"
|
||||
|
||||
#: users/models/user.py:624
|
||||
#: users/models/user.py:568
|
||||
msgid "Private key"
|
||||
msgstr "ssh私钥"
|
||||
|
||||
#: users/models/user.py:643
|
||||
#: users/models/user.py:587
|
||||
msgid "Source"
|
||||
msgstr "来源"
|
||||
|
||||
#: users/models/user.py:647
|
||||
#: users/models/user.py:591
|
||||
msgid "Date password last updated"
|
||||
msgstr "最后更新密码日期"
|
||||
|
||||
#: users/models/user.py:650
|
||||
#: users/models/user.py:594
|
||||
msgid "Need update password"
|
||||
msgstr "需要更新密码"
|
||||
|
||||
#: users/models/user.py:809
|
||||
#: users/models/user.py:753
|
||||
msgid "Administrator"
|
||||
msgstr "管理员"
|
||||
|
||||
#: users/models/user.py:812
|
||||
#: users/models/user.py:756
|
||||
msgid "Administrator is the super user of system"
|
||||
msgstr "Administrator是初始的超级管理员"
|
||||
|
||||
|
@ -5122,18 +5142,6 @@ msgstr "角色只能为 {}"
|
|||
msgid "name not unique"
|
||||
msgstr "名称重复"
|
||||
|
||||
#: users/templates/users/_base_otp.html:14
|
||||
msgid "Please enter the password of"
|
||||
msgstr "请输入"
|
||||
|
||||
#: users/templates/users/_base_otp.html:14
|
||||
msgid "account"
|
||||
msgstr "账户"
|
||||
|
||||
#: users/templates/users/_base_otp.html:14
|
||||
msgid "to complete the binding operation"
|
||||
msgstr "的密码完成绑定操作"
|
||||
|
||||
#: users/templates/users/_granted_assets.html:7
|
||||
msgid "Loading"
|
||||
msgstr "加载中"
|
||||
|
@ -5256,6 +5264,18 @@ msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中"
|
|||
msgid "Submit"
|
||||
msgstr "提交"
|
||||
|
||||
#: users/templates/users/mfa_setting.html:24
|
||||
msgid "Enable MFA"
|
||||
msgstr "启用 MFA 多因子认证"
|
||||
|
||||
#: users/templates/users/mfa_setting.html:30
|
||||
msgid "MFA force enable, cannot disable"
|
||||
msgstr "MFA 已强制启用,无法禁用"
|
||||
|
||||
#: users/templates/users/mfa_setting.html:48
|
||||
msgid "MFA setting"
|
||||
msgstr "设置 MFA 多因子认证"
|
||||
|
||||
#: users/templates/users/reset_password.html:23
|
||||
#: users/templates/users/user_password_update.html:64
|
||||
msgid "Your password must satisfy"
|
||||
|
@ -5311,13 +5331,24 @@ msgid "Exclude"
|
|||
msgstr "不包含"
|
||||
|
||||
#: users/templates/users/user_otp_check_password.html:6
|
||||
#: users/templates/users/user_verify_mfa.html:6
|
||||
msgid "Authenticate"
|
||||
msgstr "验证身份"
|
||||
msgid "Enable OTP"
|
||||
msgstr "启用 MFA (OTP)"
|
||||
|
||||
#: users/templates/users/user_otp_check_password.html:10
|
||||
msgid "Please enter the password of"
|
||||
msgstr "请输入"
|
||||
|
||||
#: users/templates/users/user_otp_check_password.html:10
|
||||
msgid "account"
|
||||
msgstr "账户"
|
||||
|
||||
#: users/templates/users/user_otp_check_password.html:10
|
||||
msgid "to complete the binding operation"
|
||||
msgstr "的密码完成绑定操作"
|
||||
|
||||
#: users/templates/users/user_otp_enable_bind.html:6
|
||||
msgid "Bind one-time password authenticator"
|
||||
msgstr "绑定一次性密码验证器"
|
||||
msgstr "绑定MFA验证器"
|
||||
|
||||
#: users/templates/users/user_otp_enable_bind.html:13
|
||||
msgid ""
|
||||
|
@ -5326,7 +5357,7 @@ msgid ""
|
|||
msgstr "使用MFA验证器应用扫描以下二维码,获取6位验证码"
|
||||
|
||||
#: users/templates/users/user_otp_enable_bind.html:22
|
||||
#: users/templates/users/user_verify_mfa.html:23
|
||||
#: users/templates/users/user_verify_mfa.html:27
|
||||
msgid "Six figures"
|
||||
msgstr "6位数字"
|
||||
|
||||
|
@ -5363,38 +5394,49 @@ msgstr "重置"
|
|||
msgid "Verify password"
|
||||
msgstr "校验密码"
|
||||
|
||||
#: users/templates/users/user_verify_mfa.html:11
|
||||
#: users/templates/users/user_verify_mfa.html:9
|
||||
msgid "Authenticate"
|
||||
msgstr "验证身份"
|
||||
|
||||
#: users/templates/users/user_verify_mfa.html:15
|
||||
msgid ""
|
||||
"The account protection has been opened, please complete the following "
|
||||
"operations according to the prompts"
|
||||
msgstr "账号保护已开启,请根据提示完成以下操作"
|
||||
|
||||
#: users/templates/users/user_verify_mfa.html:13
|
||||
#: users/templates/users/user_verify_mfa.html:17
|
||||
msgid "Open MFA Authenticator and enter the 6-bit dynamic code"
|
||||
msgstr "请打开MFA验证器,输入6位动态码"
|
||||
|
||||
#: users/views/profile/otp.py:122 users/views/profile/otp.py:161
|
||||
#: users/views/profile/otp.py:181
|
||||
msgid "MFA code invalid, or ntp sync server time"
|
||||
msgstr "MFA验证码不正确,或者服务器端时间不对"
|
||||
#: users/views/profile/otp.py:80
|
||||
msgid "Already bound"
|
||||
msgstr "已经绑定"
|
||||
|
||||
#: users/views/profile/otp.py:205
|
||||
msgid "MFA enable success"
|
||||
msgstr "多因子认证启用成功"
|
||||
#: users/views/profile/otp.py:81
|
||||
msgid "MFA already bound, disable first, then bound"
|
||||
msgstr "MFA (OTP) 已经绑定,请先禁用,再绑定"
|
||||
|
||||
#: users/views/profile/otp.py:206
|
||||
msgid "MFA enable success, return login page"
|
||||
msgstr "多因子认证启用成功,返回到登录页面"
|
||||
#: users/views/profile/otp.py:108
|
||||
msgid "OTP enable success"
|
||||
msgstr "MFA (OTP) 启用成功"
|
||||
|
||||
#: users/views/profile/otp.py:208
|
||||
msgid "MFA disable success"
|
||||
msgstr "多因子认证禁用成功"
|
||||
#: users/views/profile/otp.py:109
|
||||
msgid "OTP enable success, return login page"
|
||||
msgstr "MFA (OTP) 启用成功,返回到登录页面"
|
||||
|
||||
#: users/views/profile/otp.py:209
|
||||
msgid "MFA disable success, return login page"
|
||||
msgstr "多因子认证禁用成功,返回登录页面"
|
||||
#: users/views/profile/otp.py:151
|
||||
msgid "Disable OTP"
|
||||
msgstr "禁用 MFA (OTP)"
|
||||
|
||||
#: users/views/profile/password.py:32 users/views/profile/password.py:36
|
||||
#: users/views/profile/otp.py:157
|
||||
msgid "OTP disable success"
|
||||
msgstr "MFA (OTP) 禁用成功"
|
||||
|
||||
#: users/views/profile/otp.py:158
|
||||
msgid "OTP disable success, return login page"
|
||||
msgstr "MFA (OTP) 禁用成功,返回登录页面"
|
||||
|
||||
#: users/views/profile/password.py:36 users/views/profile/password.py:41
|
||||
msgid "Password invalid"
|
||||
msgstr "用户名或密码无效"
|
||||
|
||||
|
@ -5441,8 +5483,8 @@ msgstr "* 新密码不能是最近 {} 次的密码"
|
|||
msgid "Reset password success, return to login page"
|
||||
msgstr "重置密码成功,返回到登录页面"
|
||||
|
||||
#: xpack/plugins/change_auth_plan/api/app.py:114
|
||||
#: xpack/plugins/change_auth_plan/api/asset.py:101
|
||||
#: xpack/plugins/change_auth_plan/api/app.py:113
|
||||
#: xpack/plugins/change_auth_plan/api/asset.py:100
|
||||
msgid "The parameter 'action' must be [{}]"
|
||||
msgstr "参数 'action' 必须是 [{}]"
|
||||
|
||||
|
@ -5573,15 +5615,15 @@ msgstr "* 请输入正确的密码长度"
|
|||
msgid "* Password length range 6-30 bits"
|
||||
msgstr "* 密码长度范围 6-30 位"
|
||||
|
||||
#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:249
|
||||
#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:248
|
||||
msgid "Invalid/incorrect password"
|
||||
msgstr "无效/错误 密码"
|
||||
|
||||
#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:251
|
||||
#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:250
|
||||
msgid "Failed to connect to the host"
|
||||
msgstr "连接主机失败"
|
||||
|
||||
#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:253
|
||||
#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:252
|
||||
msgid "Data could not be sent to remote"
|
||||
msgstr "无法将数据发送到远程"
|
||||
|
||||
|
@ -5939,7 +5981,7 @@ msgstr "执行次数"
|
|||
msgid "Instance count"
|
||||
msgstr "实例个数"
|
||||
|
||||
#: xpack/plugins/cloud/utils.py:68
|
||||
#: xpack/plugins/cloud/utils.py:65
|
||||
msgid "Account unavailable"
|
||||
msgstr "账户无效"
|
||||
|
||||
|
@ -6027,6 +6069,38 @@ msgstr "旗舰版"
|
|||
msgid "Community edition"
|
||||
msgstr "社区版"
|
||||
|
||||
#~ msgid "One-time password invalid, or ntp sync server time"
|
||||
#~ msgstr "MFA 验证码不正确,或者服务器端时间不对"
|
||||
|
||||
#~ msgid "Download MFA APP, Using dynamic code"
|
||||
#~ msgstr "下载 MFA APP, 使用一次性动态码"
|
||||
|
||||
#~ msgid "MFA Radius"
|
||||
#~ msgstr "Radius MFA"
|
||||
|
||||
#~ msgid "Please enter verification code"
|
||||
#~ msgstr "请输入验证码"
|
||||
|
||||
#, python-brace-format
|
||||
#~ msgid ""
|
||||
#~ "One-time password invalid, or ntp sync server time, You can also try "
|
||||
#~ "{times_try} times (The account will be temporarily locked for "
|
||||
#~ "{block_time} minutes)"
|
||||
#~ msgstr ""
|
||||
#~ "虚拟MFA 不正确,或者服务器端时间不对。 您还可以尝试 {times_try} 次(账号将"
|
||||
#~ "被临时 锁定 {block_time} 分钟)"
|
||||
|
||||
#, python-brace-format
|
||||
#~ msgid ""
|
||||
#~ "The MFA type({mfa_type}) is not supported, You can also try {times_try} "
|
||||
#~ "times (The account will be temporarily locked for {block_time} minutes)"
|
||||
#~ msgstr ""
|
||||
#~ "该({mfa_type}) MFA 类型不支持, 您还可以尝试 {times_try} 次(账号将被临时 "
|
||||
#~ "锁定 {block_time} 分钟)"
|
||||
|
||||
#~ msgid "One-time password"
|
||||
#~ msgstr "一次性密码"
|
||||
|
||||
#~ msgid "Go"
|
||||
#~ msgstr "立即"
|
||||
|
||||
|
|
|
@ -68,5 +68,8 @@ class SiteMsgWebsocket(JsonWebsocketConsumer):
|
|||
|
||||
def disconnect(self, close_code):
|
||||
if self.chan is not None:
|
||||
self.chan.close()
|
||||
try:
|
||||
self.chan.close()
|
||||
except:
|
||||
pass
|
||||
self.close()
|
||||
|
|
|
@ -1174,6 +1174,14 @@ button.dim:active:before {
|
|||
.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.onoffswitch-checkbox:disabled + .onoffswitch-label .onoffswitch-inner:before {
|
||||
background-color: #919191;
|
||||
}
|
||||
|
||||
.onoffswitch-checkbox:disabled + .onoffswitch-label, .onoffswitch-checkbox:disabled + .onoffswitch-label .onoffswitch-switch {
|
||||
border-color: #919191;
|
||||
}
|
||||
/* CHOSEN PLUGIN */
|
||||
.chosen-container-single .chosen-single {
|
||||
background: #ffffff;
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
|
||||
{% include '_head_css_js.html' %}
|
||||
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
|
||||
|
||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
||||
<style>
|
||||
.passwordBox {
|
||||
|
@ -43,5 +42,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</body>
|
||||
{% include '_foot_js.html' %}
|
||||
{% block custom_foot_js %} {% endblock %}
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
<select id="mfa-select" name="mfa_type" class="form-control select-con"
|
||||
onchange="selectChange(this.value)"
|
||||
>
|
||||
{% for backend in mfa_backends %}
|
||||
<option value="{{ backend.name }}"
|
||||
{% if not backend.is_active %} disabled {% endif %}
|
||||
>
|
||||
{{ backend.display_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="mfa-div">
|
||||
{% for backend in mfa_backends %}
|
||||
<div id="mfa-{{ backend.name }}" class="mfa-field
|
||||
{% if backend.challenge_required %}challenge-required{% endif %}"
|
||||
style="display: none"
|
||||
>
|
||||
<input type="text" class="form-control input-style"
|
||||
placeholder="{{ backend.placeholder }}"
|
||||
>
|
||||
{% if backend.challenge_required %}
|
||||
<button class="btn btn-primary full-width btn-challenge"
|
||||
type='button' onclick="sendChallengeCode(this)"
|
||||
>
|
||||
{% trans 'Send verification code' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.input-style {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.challenge-required .input-style {
|
||||
width: calc(100% - 114px);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-challenge {
|
||||
width: 110px !important;
|
||||
height: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
const preferMFAKey = 'mfaPrefer'
|
||||
$(document).ready(function () {
|
||||
const mfaSelectRef = document.getElementById('mfa-select');
|
||||
const preferMFA = localStorage.getItem(preferMFAKey);
|
||||
if (preferMFA) {
|
||||
mfaSelectRef.value = preferMFA;
|
||||
}
|
||||
const mfaSelect = mfaSelectRef.value;
|
||||
if (mfaSelect !== null) {
|
||||
selectChange(mfaSelect, true);
|
||||
}
|
||||
})
|
||||
|
||||
function selectChange(name, onLoad) {
|
||||
$('.mfa-field').hide()
|
||||
$('#mfa-' + name).show()
|
||||
if (!onLoad) {
|
||||
localStorage.setItem(preferMFAKey, name)
|
||||
}
|
||||
|
||||
$('.input-style').each(function (i, ele){
|
||||
$(ele).attr('name', '').attr('required', false)
|
||||
})
|
||||
$('#mfa-' + name + ' .input-style').attr('name', 'code').attr('required', true)
|
||||
}
|
||||
|
||||
function sendChallengeCode(currentBtn) {
|
||||
let time = 60;
|
||||
const url = "{% url 'api-auth:mfa-select' %}";
|
||||
const data = {
|
||||
type: $("#mfa-select").val(),
|
||||
username: $('input[name="username"]').val()
|
||||
};
|
||||
|
||||
function onSuccess() {
|
||||
const originBtnText = currentBtn.innerHTML;
|
||||
currentBtn.disabled = true
|
||||
|
||||
const interval = setInterval(function () {
|
||||
currentBtn.innerHTML = `{% trans 'Wait: ' %} ${time}`;
|
||||
time -= 1
|
||||
|
||||
if (time === 0) {
|
||||
currentBtn.innerHTML = originBtnText
|
||||
currentBtn.disabled = false
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 1000)
|
||||
setTimeout(function (){
|
||||
toastr.success("{% trans 'The verification code has been sent' %}");
|
||||
})
|
||||
}
|
||||
|
||||
requestApi({
|
||||
url: url,
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
success: onSuccess,
|
||||
error: function (text, data) {
|
||||
toastr.error(data.error)
|
||||
},
|
||||
flash_message: false
|
||||
})
|
||||
}
|
||||
</script>
|
|
@ -1,81 +0,0 @@
|
|||
{% 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>
|
|
@ -7,26 +7,23 @@
|
|||
<meta charset="UTF-8">
|
||||
<title> {{ JMS_TITLE }} </title>
|
||||
<link rel="shortcut icon" href="{{ FAVICON_URL }}" type="image/x-icon">
|
||||
{# <link rel="stylesheet" href="{% static 'fonts/font_otp/iconfont.css' %}" />#}
|
||||
{% include '_head_css_js.html' %}
|
||||
<link href="{% static 'css/jumpserver.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/style.css' %}" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{% static 'css/otp.css' %}" />
|
||||
<script src="{% static 'js/jquery-3.1.1.min.js' %}"></script>
|
||||
<script src="{% static "js/plugins/qrcode/qrcode.min.js" %}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body style="background-color: #f3f3f4">
|
||||
<header>
|
||||
<div class="logo">
|
||||
<a href="{% url 'index' %}">
|
||||
<img src="{{ LOGO_URL }}" alt="" width="50px" height="50px"/>
|
||||
</a>
|
||||
<a href="{% url 'index' %}">{{ JMS_TITLE }}</a>
|
||||
<span style="font-size: 18px; line-height: 50px">{{ JMS_TITLE }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'index' %}">{% trans 'Home page' %}</a>
|
||||
<b>丨</b>
|
||||
<a href="http://docs.jumpserver.org/zh/docs/">{% trans 'Docs' %}</a>
|
||||
<b>丨</b>
|
||||
<a href="https://www.github.com/jumpserver/">GitHub</a>
|
||||
</div>
|
||||
</header>
|
||||
<body>
|
||||
|
@ -34,10 +31,11 @@
|
|||
{% endblock %}
|
||||
</body>
|
||||
<footer>
|
||||
<div class="" style="margin-top: 100px;">
|
||||
<div style="margin-top: 100px;">
|
||||
{% include '_copyright.html' %}
|
||||
</div>
|
||||
</footer>
|
||||
{% include '_foot_js.html' %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
|
@ -12,33 +12,28 @@ from django.contrib.auth.models import AbstractUser
|
|||
from django.contrib.auth.hashers import check_password
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from django.shortcuts import reverse
|
||||
|
||||
from orgs.utils import current_org
|
||||
from orgs.models import OrganizationMember, Organization
|
||||
from common.exceptions import JMSException
|
||||
from common.utils import date_expired_default, get_logger, lazyproperty, random_string
|
||||
from common import fields
|
||||
from common.const import choices
|
||||
from common.db.models import TextChoices
|
||||
from users.exceptions import MFANotEnabled, PhoneNotSet
|
||||
from ..signals import post_user_change_password
|
||||
|
||||
__all__ = ['User', 'UserPasswordHistory', 'MFAType']
|
||||
__all__ = ['User', 'UserPasswordHistory']
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class MFAType(TextChoices):
|
||||
OTP = 'otp', _('One-time password')
|
||||
SMS_CODE = 'sms', _('SMS verify code')
|
||||
|
||||
|
||||
class AuthMixin:
|
||||
date_password_last_updated: datetime.datetime
|
||||
history_passwords: models.Manager
|
||||
need_update_password: bool
|
||||
public_key: str
|
||||
is_local: bool
|
||||
|
||||
@property
|
||||
|
@ -77,7 +72,8 @@ class AuthMixin:
|
|||
|
||||
def is_history_password(self, password):
|
||||
allow_history_password_count = settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT
|
||||
history_passwords = self.history_passwords.all().order_by('-date_created')[:int(allow_history_password_count)]
|
||||
history_passwords = self.history_passwords.all() \
|
||||
.order_by('-date_created')[:int(allow_history_password_count)]
|
||||
|
||||
for history_password in history_passwords:
|
||||
if check_password(password, history_password.password):
|
||||
|
@ -474,9 +470,11 @@ class MFAMixin:
|
|||
|
||||
@property
|
||||
def mfa_force_enabled(self):
|
||||
if settings.SECURITY_MFA_AUTH in [True, 1]:
|
||||
force_level = settings.SECURITY_MFA_AUTH
|
||||
if force_level in [True, 1]:
|
||||
return True
|
||||
if settings.SECURITY_MFA_AUTH == 2 and self.is_org_admin:
|
||||
# 2 管理员强制开启
|
||||
if force_level == 2 and self.is_org_admin:
|
||||
return True
|
||||
return self.mfa_level == 2
|
||||
|
||||
|
@ -489,86 +487,32 @@ class MFAMixin:
|
|||
|
||||
def disable_mfa(self):
|
||||
self.mfa_level = 0
|
||||
self.otp_secret_key = None
|
||||
|
||||
def reset_mfa(self):
|
||||
if self.mfa_is_otp():
|
||||
self.otp_secret_key = ''
|
||||
def no_active_mfa(self):
|
||||
return len(self.active_mfa_backends) == 0
|
||||
|
||||
@lazyproperty
|
||||
def active_mfa_backends(self):
|
||||
backends = self.get_user_mfa_backends(self)
|
||||
active_backends = [b for b in backends if b.is_active()]
|
||||
return active_backends
|
||||
|
||||
@property
|
||||
def active_mfa_backends_mapper(self):
|
||||
return {b.name: b for b in self.active_mfa_backends}
|
||||
|
||||
@staticmethod
|
||||
def mfa_is_otp():
|
||||
if settings.OTP_IN_RADIUS:
|
||||
return False
|
||||
return True
|
||||
def get_user_mfa_backends(user):
|
||||
from authentication.mfa import MFA_BACKENDS
|
||||
backends = [cls(user) for cls in MFA_BACKENDS if cls.global_enabled()]
|
||||
return backends
|
||||
|
||||
def check_radius(self, code):
|
||||
from authentication.backends.radius import RadiusBackend
|
||||
backend = RadiusBackend()
|
||||
user = backend.authenticate(None, username=self.username, password=code)
|
||||
if user:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_otp(self, code):
|
||||
from ..utils import check_otp_code
|
||||
return check_otp_code(self.otp_secret_key, code)
|
||||
|
||||
def check_mfa(self, code, mfa_type=MFAType.OTP):
|
||||
if not self.mfa_enabled:
|
||||
raise MFANotEnabled
|
||||
|
||||
if mfa_type == MFAType.OTP:
|
||||
if settings.OTP_IN_RADIUS:
|
||||
return self.check_radius(code)
|
||||
else:
|
||||
return self.check_otp(code)
|
||||
elif mfa_type == MFAType.SMS_CODE:
|
||||
return self.check_sms_code(code)
|
||||
|
||||
def get_supported_mfa_types(self):
|
||||
methods = []
|
||||
if self.otp_secret_key:
|
||||
methods.append(MFAType.OTP)
|
||||
if settings.XPACK_ENABLED and settings.SMS_ENABLED and self.phone:
|
||||
methods.append(MFAType.SMS_CODE)
|
||||
return methods
|
||||
|
||||
def check_sms_code(self, code):
|
||||
from authentication.sms_verify_code import VerifyCodeUtil
|
||||
|
||||
if not self.phone:
|
||||
raise PhoneNotSet
|
||||
|
||||
try:
|
||||
util = VerifyCodeUtil(self.phone)
|
||||
return util.verify(code)
|
||||
except JMSException:
|
||||
return False
|
||||
|
||||
def send_sms_code(self):
|
||||
from authentication.sms_verify_code import VerifyCodeUtil
|
||||
|
||||
if not self.phone:
|
||||
raise PhoneNotSet
|
||||
|
||||
util = VerifyCodeUtil(self.phone)
|
||||
util.touch()
|
||||
return util.timeout
|
||||
|
||||
def mfa_enabled_but_not_set(self):
|
||||
if not self.mfa_enabled:
|
||||
return False, None
|
||||
|
||||
if not self.mfa_is_otp():
|
||||
return False, None
|
||||
|
||||
if self.mfa_is_otp() and self.otp_secret_key:
|
||||
return False, None
|
||||
|
||||
if self.phone and settings.SMS_ENABLED and settings.XPACK_ENABLED:
|
||||
return False, None
|
||||
|
||||
return True, reverse('authentication:user-otp-enable-start')
|
||||
def get_mfa_backend_by_type(self, mfa_type):
|
||||
mfa_mapper = self.active_mfa_backends_mapper
|
||||
backend = mfa_mapper.get(mfa_type)
|
||||
if not backend:
|
||||
return None
|
||||
return backend
|
||||
|
||||
|
||||
class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
|
||||
|
|
|
@ -11,8 +11,6 @@
|
|||
</h2>
|
||||
</div>
|
||||
<div>
|
||||
<div class="verify">{% trans 'Please enter the password of' %} {% trans 'account' %} <span>{{ user.username }}</span> {% trans 'to complete the binding operation' %}</div>
|
||||
<hr style="width: 500px; margin: auto; margin-top: 10px;">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
{% extends '_without_nav_base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block body %}
|
||||
<style>
|
||||
.help-inline {
|
||||
color: #7d8293;
|
||||
font-size: 12px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.btn-xs {
|
||||
width: 54px;
|
||||
}
|
||||
|
||||
.onoffswitch-switch {
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
||||
<article>
|
||||
<div>
|
||||
{# // Todoi:#}
|
||||
<h3>{% trans 'Enable MFA' %}</h3>
|
||||
<div class="row" style="padding-top: 10px">
|
||||
<li class="col-sm-6" style="font-size: 14px">{% trans 'Enable' %} MFA</li>
|
||||
<div class="switch col-sm-6">
|
||||
<span class="help-inline">
|
||||
{% if user.mfa_force_enabled %}
|
||||
{% trans 'MFA force enable, cannot disable' %}
|
||||
{% endif %}
|
||||
</span>
|
||||
<div class="onoffswitch" style="float: right">
|
||||
<input type="checkbox" class="onoffswitch-checkbox"
|
||||
id="mfa-switch" onchange="switchMFA()"
|
||||
{% if user.mfa_force_enabled %} disabled {% endif %}
|
||||
{% if user.mfa_enabled %} checked {% endif %}
|
||||
>
|
||||
<label class="onoffswitch-label" for="mfa-switch">
|
||||
<span class="onoffswitch-inner"></span>
|
||||
<span class="onoffswitch-switch"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mfa-setting" style="display: none; padding-top: 30px">
|
||||
<h3>{% trans 'MFA setting' %}</h3>
|
||||
<div style="height: 100%; width: 100%;">
|
||||
{% for b in mfa_backends %}
|
||||
<div class="row" style="padding-top: 10px">
|
||||
<li class="col-sm-6" style="font-size: 14px">{{ b.display_name }}
|
||||
{{ b.enable }}</li>
|
||||
<span class="col-sm-6">
|
||||
{% if b.is_active %}
|
||||
<button class="btn btn-warning btn-xs" style="float: right"
|
||||
{% if not b.can_disable %} disabled {% endif %}
|
||||
onclick="goTo('{{ b.get_disable_url }}')"
|
||||
>
|
||||
{% trans 'Disable' %}
|
||||
</button>
|
||||
<span class="help-inline">{{ b.help_text_of_disable }}</span>
|
||||
{% else %}
|
||||
<button class="btn btn-primary btn-xs" style="float: right"
|
||||
onclick="goTo('{{ b.get_enable_url }}')"
|
||||
>
|
||||
{% trans 'Enable' %}
|
||||
</button>
|
||||
<span class="help-inline">{{ b.help_text_of_enable }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<script src="{% static 'js/jumpserver.js' %}"></script>
|
||||
<script>
|
||||
function goTo(url) {
|
||||
window.open(url, '_self')
|
||||
}
|
||||
|
||||
function switchMFA() {
|
||||
const switchRef = $('#mfa-switch')
|
||||
const enabled = switchRef.is(":checked")
|
||||
requestApi({
|
||||
url: '/api/v1/users/profile/',
|
||||
data: {
|
||||
mfa_level: enabled ? 1 : 0
|
||||
},
|
||||
method: 'PATCH',
|
||||
success() {
|
||||
showSettingOrNot()
|
||||
},
|
||||
error() {
|
||||
switchRef.prop('checked', !enabled)
|
||||
}
|
||||
})
|
||||
showSettingOrNot()
|
||||
}
|
||||
|
||||
function showSettingOrNot() {
|
||||
const enabled = $('#mfa-switch').is(":checked")
|
||||
const settingRef = $('#mfa-setting')
|
||||
if (enabled) {
|
||||
settingRef.show()
|
||||
} else {
|
||||
settingRef.hide()
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
showSettingOrNot()
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -3,10 +3,12 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block small_title %}
|
||||
{% trans 'Authenticate' %}
|
||||
{% trans 'Enable OTP' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="verify">{% trans 'Please enter the password of' %} {% trans 'account' %} <span>{{ user.username }}</span> {% trans 'to complete the binding operation' %}</div>
|
||||
<hr style="width: 500px; margin: auto; margin-top: 10px;">
|
||||
<form id="verify-form" class="" role="form" method="post" action="">
|
||||
{% csrf_token %}
|
||||
<div class="form-input">
|
||||
|
|
|
@ -3,7 +3,11 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block small_title %}
|
||||
{% trans 'Authenticate' %}
|
||||
{% if title %}
|
||||
{{ title }}
|
||||
{% else %}
|
||||
{% trans 'Authenticate' %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
|
@ -137,7 +137,7 @@ class BlockUtilBase:
|
|||
times_remainder = int(times_up) - int(times_failed)
|
||||
return times_remainder
|
||||
|
||||
def incr_failed_count(self):
|
||||
def incr_failed_count(self) -> int:
|
||||
limit_key = self.limit_key
|
||||
count = cache.get(limit_key, 0)
|
||||
count += 1
|
||||
|
@ -146,6 +146,7 @@ class BlockUtilBase:
|
|||
limit_count = settings.SECURITY_LOGIN_LIMIT_COUNT
|
||||
if count >= limit_count:
|
||||
cache.set(self.block_key, True, self.key_ttl)
|
||||
return limit_count - count
|
||||
|
||||
def get_failed_count(self):
|
||||
count = cache.get(self.limit_key, 0)
|
||||
|
@ -205,4 +206,4 @@ def is_auth_password_time_valid(session):
|
|||
|
||||
|
||||
def is_auth_otp_time_valid(session):
|
||||
return is_auth_time_valid(session, 'auth_opt_expired_at')
|
||||
return is_auth_time_valid(session, 'auth_otp_expired_at')
|
||||
|
|
|
@ -1,2 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from __future__ import unicode_literals
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
from common.permissions import IsValidUser
|
||||
from common.mixins.views import PermissionsMixin
|
||||
from users.models import User
|
||||
|
||||
__all__ = ['MFASettingView']
|
||||
|
||||
|
||||
class MFASettingView(PermissionsMixin, TemplateView):
|
||||
template_name = 'users/mfa_setting.html'
|
||||
permission_classes = [IsValidUser]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
mfa_backends = User.get_user_mfa_backends(self.request.user)
|
||||
context.update({
|
||||
'mfa_backends': mfa_backends,
|
||||
})
|
||||
return context
|
||||
|
||||
|
|
|
@ -1,31 +1,30 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
import time
|
||||
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.views.generic.edit import FormView
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
from django.conf import settings
|
||||
from django.http.response import HttpResponseForbidden
|
||||
from django.http.response import HttpResponseRedirect
|
||||
|
||||
from authentication.mixins import AuthMixin
|
||||
from users.models import User
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from authentication.mfa import MFAOtp, otp_failed_msg
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
from common.mixins.views import PermissionsMixin
|
||||
from common.permissions import IsValidUser
|
||||
from ... import forms
|
||||
from .password import UserVerifyPasswordView
|
||||
from ... import forms
|
||||
from ...utils import (
|
||||
generate_otp_uri, check_otp_code, get_user_or_pre_auth_user,
|
||||
is_auth_password_time_valid, is_auth_otp_time_valid
|
||||
generate_otp_uri, check_otp_code,
|
||||
get_user_or_pre_auth_user,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'UserOtpEnableStartView',
|
||||
'UserOtpEnableInstallAppView',
|
||||
'UserOtpEnableBindView', 'UserOtpSettingsSuccessView',
|
||||
'UserDisableMFAView', 'UserOtpUpdateView',
|
||||
'UserOtpEnableBindView',
|
||||
'UserOtpDisableView',
|
||||
]
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
@ -34,22 +33,8 @@ logger = get_logger(__name__)
|
|||
class UserOtpEnableStartView(UserVerifyPasswordView):
|
||||
template_name = 'users/user_otp_check_password.html'
|
||||
|
||||
def form_valid(self, form):
|
||||
# 开启了 OTP IN RADIUS 就不用绑定了
|
||||
resp = super().form_valid(form)
|
||||
if settings.OTP_IN_RADIUS:
|
||||
user_id = self.request.session.get('user_id')
|
||||
user = get_object_or_404(User, id=user_id)
|
||||
user.enable_mfa()
|
||||
user.save()
|
||||
return resp
|
||||
|
||||
def get_success_url(self):
|
||||
if settings.OTP_IN_RADIUS:
|
||||
success_url = reverse_lazy('authentication:user-otp-settings-success')
|
||||
else:
|
||||
success_url = reverse('authentication:user-otp-enable-install-app')
|
||||
return success_url
|
||||
return reverse('authentication:user-otp-enable-install-app')
|
||||
|
||||
|
||||
class UserOtpEnableInstallAppView(TemplateView):
|
||||
|
@ -65,69 +50,68 @@ class UserOtpEnableInstallAppView(TemplateView):
|
|||
class UserOtpEnableBindView(AuthMixin, TemplateView, FormView):
|
||||
template_name = 'users/user_otp_enable_bind.html'
|
||||
form_class = forms.UserCheckOtpCodeForm
|
||||
success_url = reverse_lazy('authentication:user-otp-settings-success')
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if self._check_can_bind():
|
||||
return super().get(request, *args, **kwargs)
|
||||
return HttpResponseForbidden()
|
||||
pre_response = self._pre_check_can_bind()
|
||||
if pre_response:
|
||||
return pre_response
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if self._check_can_bind():
|
||||
return super().post(request, *args, **kwargs)
|
||||
return HttpResponseForbidden()
|
||||
pre_response = self._pre_check_can_bind()
|
||||
if pre_response:
|
||||
return pre_response
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def _check_authenticated_user_can_bind(self):
|
||||
user = self.request.user
|
||||
session = self.request.session
|
||||
def _pre_check_can_bind(self):
|
||||
try:
|
||||
user = self.get_user_from_session()
|
||||
except:
|
||||
verify_url = reverse('authentication:user-otp-enable-start')
|
||||
return HttpResponseRedirect(verify_url)
|
||||
|
||||
if not user.mfa_enabled:
|
||||
return is_auth_password_time_valid(session)
|
||||
if user.otp_secret_key:
|
||||
return self.has_already_bound_message()
|
||||
return None
|
||||
|
||||
if not user.otp_secret_key:
|
||||
return is_auth_password_time_valid(session)
|
||||
|
||||
return is_auth_otp_time_valid(session)
|
||||
|
||||
def _check_unauthenticated_user_can_bind(self):
|
||||
session_user = None
|
||||
if not self.request.session.is_empty():
|
||||
user_id = self.request.session.get('user_id')
|
||||
session_user = get_object_or_none(User, pk=user_id)
|
||||
|
||||
if session_user:
|
||||
if all((
|
||||
is_auth_password_time_valid(self.request.session),
|
||||
session_user.mfa_enabled,
|
||||
not session_user.otp_secret_key
|
||||
)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _check_can_bind(self):
|
||||
if self.request.user.is_authenticated:
|
||||
return self._check_authenticated_user_can_bind()
|
||||
else:
|
||||
return self._check_unauthenticated_user_can_bind()
|
||||
@staticmethod
|
||||
def has_already_bound_message():
|
||||
message_data = {
|
||||
'title': _('Already bound'),
|
||||
'error': _('MFA already bound, disable first, then bound'),
|
||||
'interval': 10,
|
||||
'redirect_url': reverse('authentication:user-otp-disable'),
|
||||
}
|
||||
response = FlashMessageUtil.gen_and_redirect_to(message_data)
|
||||
return response
|
||||
|
||||
def form_valid(self, form):
|
||||
otp_code = form.cleaned_data.get('otp_code')
|
||||
otp_secret_key = self.request.session.get('otp_secret_key', '')
|
||||
|
||||
valid = check_otp_code(otp_secret_key, otp_code)
|
||||
if valid:
|
||||
self.save_otp(otp_secret_key)
|
||||
return super().form_valid(form)
|
||||
else:
|
||||
error = _("MFA code invalid, or ntp sync server time")
|
||||
form.add_error("otp_code", error)
|
||||
if not valid:
|
||||
form.add_error("otp_code", otp_failed_msg)
|
||||
return self.form_invalid(form)
|
||||
|
||||
self.save_otp(otp_secret_key)
|
||||
auth_logout(self.request)
|
||||
return super().form_valid(form)
|
||||
|
||||
def save_otp(self, otp_secret_key):
|
||||
user = get_user_or_pre_auth_user(self.request)
|
||||
user.enable_mfa()
|
||||
user.otp_secret_key = otp_secret_key
|
||||
user.save()
|
||||
user.save(update_fields=['otp_secret_key'])
|
||||
|
||||
def get_success_url(self):
|
||||
message_data = {
|
||||
'title': _('OTP enable success'),
|
||||
'message': _('OTP enable success, return login page'),
|
||||
'interval': 5,
|
||||
'redirect_url': reverse('authentication:login'),
|
||||
}
|
||||
url = FlashMessageUtil.gen_message_url(message_data)
|
||||
return url
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
user = get_user_or_pre_auth_user(self.request)
|
||||
|
@ -142,70 +126,40 @@ class UserOtpEnableBindView(AuthMixin, TemplateView, FormView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class UserDisableMFAView(FormView):
|
||||
class UserOtpDisableView(PermissionsMixin, FormView):
|
||||
template_name = 'users/user_verify_mfa.html'
|
||||
form_class = forms.UserCheckOtpCodeForm
|
||||
success_url = reverse_lazy('authentication:user-otp-settings-success')
|
||||
permission_classes = [IsValidUser]
|
||||
|
||||
def form_valid(self, form):
|
||||
user = self.request.user
|
||||
otp_code = form.cleaned_data.get('otp_code')
|
||||
otp = MFAOtp(user)
|
||||
|
||||
valid = user.check_mfa(otp_code)
|
||||
if valid:
|
||||
user.disable_mfa()
|
||||
user.save()
|
||||
return super().form_valid(form)
|
||||
else:
|
||||
error = _('MFA code invalid, or ntp sync server time')
|
||||
ok, error = otp.check_code(otp_code)
|
||||
if not ok:
|
||||
form.add_error('otp_code', error)
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class UserOtpUpdateView(FormView):
|
||||
template_name = 'users/user_verify_mfa.html'
|
||||
form_class = forms.UserCheckOtpCodeForm
|
||||
success_url = reverse_lazy('authentication:user-otp-enable-bind')
|
||||
permission_classes = [IsValidUser]
|
||||
|
||||
def form_valid(self, form):
|
||||
user = self.request.user
|
||||
otp_code = form.cleaned_data.get('otp_code')
|
||||
|
||||
valid = user.check_mfa(otp_code)
|
||||
if valid:
|
||||
self.request.session['auth_opt_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS
|
||||
return super().form_valid(form)
|
||||
else:
|
||||
error = _('MFA code invalid, or ntp sync server time')
|
||||
form.add_error('otp_code', error)
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class UserOtpSettingsSuccessView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
otp.disable()
|
||||
auth_logout(self.request)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
title, describe = self.get_title_describe()
|
||||
context = {
|
||||
'title': title,
|
||||
'message': describe,
|
||||
'interval': 1,
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update({
|
||||
'title': _("Disable OTP")
|
||||
})
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
message_data = {
|
||||
'title': _('OTP disable success'),
|
||||
'message': _('OTP disable success, return login page'),
|
||||
'interval': 5,
|
||||
'redirect_url': reverse('authentication:login'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
url = FlashMessageUtil.gen_message_url(message_data)
|
||||
return url
|
||||
|
||||
def get_title_describe(self):
|
||||
user = get_user_or_pre_auth_user(self.request)
|
||||
if self.request.user.is_authenticated:
|
||||
auth_logout(self.request)
|
||||
title = _('MFA enable success')
|
||||
describe = _('MFA enable success, return login page')
|
||||
if not user.mfa_enabled:
|
||||
title = _('MFA disable success')
|
||||
describe = _('MFA disable success, return login page')
|
||||
return title, describe
|
||||
|
||||
|
|
|
@ -6,7 +6,8 @@ from django.contrib.auth import authenticate
|
|||
from django.shortcuts import redirect
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic.edit import FormView
|
||||
from authentication.mixins import PasswordEncryptionViewMixin
|
||||
|
||||
from authentication.mixins import PasswordEncryptionViewMixin, AuthMixin
|
||||
from authentication import errors
|
||||
|
||||
from common.utils import get_logger
|
||||
|
@ -20,24 +21,27 @@ __all__ = ['UserVerifyPasswordView']
|
|||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class UserVerifyPasswordView(PasswordEncryptionViewMixin, FormView):
|
||||
class UserVerifyPasswordView(AuthMixin, FormView):
|
||||
template_name = 'users/user_password_verify.html'
|
||||
form_class = forms.UserCheckPasswordForm
|
||||
|
||||
def form_valid(self, form):
|
||||
user = get_user_or_pre_auth_user(self.request)
|
||||
if user is None:
|
||||
return redirect('authentication:login')
|
||||
|
||||
try:
|
||||
password = self.get_decrypted_password(username=user.username)
|
||||
except errors.AuthFailedError as e:
|
||||
form.add_error("password", _(f"Password invalid") + f'({e.msg})')
|
||||
return self.form_invalid(form)
|
||||
|
||||
user = authenticate(request=self.request, username=user.username, password=password)
|
||||
if not user:
|
||||
form.add_error("password", _("Password invalid"))
|
||||
return self.form_invalid(form)
|
||||
self.request.session['user_id'] = str(user.id)
|
||||
self.request.session['auth_password'] = 1
|
||||
self.request.session['auth_password_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS
|
||||
|
||||
self.mark_password_ok(user)
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
|
|
Loading…
Reference in New Issue