mirror of https://github.com/jumpserver/jumpserver
perf: 优化confirm接口 (#8451)
* perf: 优化confirm接口 * perf: 修改 校验 * perf: 优化 confirm API 逻辑 * Delete django.po Co-authored-by: feng626 <1304903146@qq.com> Co-authored-by: ibuler <ibuler@qq.com> Co-authored-by: Jiangjie.Bai <bugatti_it@163.com> Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com>pull/8527/head
parent
ca19e45905
commit
a6cc8a8b05
|
@ -2,15 +2,16 @@
|
|||
#
|
||||
|
||||
from django_filters import rest_framework as filters
|
||||
from django.db.models import F, Q
|
||||
from django.db.models import Q
|
||||
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from common.drf.api import JMSBulkModelViewSet
|
||||
from common.mixins import RecordViewLogMixin
|
||||
from common.permissions import UserConfirmation
|
||||
from authentication.const import ConfirmType
|
||||
from rbac.permissions import RBACPermission
|
||||
from assets.models import SystemUser
|
||||
from ..models import Account
|
||||
from ..hands import NeedMFAVerify
|
||||
from .. import serializers
|
||||
|
||||
|
||||
|
@ -57,7 +58,7 @@ class SystemUserAppRelationViewSet(ApplicationAccountViewSet):
|
|||
|
||||
class ApplicationAccountSecretViewSet(RecordViewLogMixin, ApplicationAccountViewSet):
|
||||
serializer_class = serializers.AppAccountSecretSerializer
|
||||
permission_classes = [RBACPermission, NeedMFAVerify]
|
||||
permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
|
||||
http_method_names = ['get', 'options']
|
||||
rbac_perms = {
|
||||
'retrieve': 'applications.view_applicationaccountsecret',
|
||||
|
|
|
@ -11,5 +11,4 @@
|
|||
"""
|
||||
|
||||
|
||||
from common.permissions import NeedMFAVerify
|
||||
from users.models import User, UserGroup
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from django.db.models import F, Q
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_filters import rest_framework as filters
|
||||
from rest_framework.decorators import action
|
||||
|
@ -9,7 +9,8 @@ from orgs.mixins.api import OrgBulkModelViewSet
|
|||
from rbac.permissions import RBACPermission
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from common.mixins import RecordViewLogMixin
|
||||
from common.permissions import NeedMFAVerify
|
||||
from common.permissions import UserConfirmation
|
||||
from authentication.const import ConfirmType
|
||||
from ..tasks.account_connectivity import test_accounts_connectivity_manual
|
||||
from ..models import AuthBook, Node
|
||||
from .. import serializers
|
||||
|
@ -88,7 +89,7 @@ class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
|
|||
'default': serializers.AccountSecretSerializer
|
||||
}
|
||||
http_method_names = ['get']
|
||||
permission_classes = [RBACPermission, NeedMFAVerify]
|
||||
permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
|
||||
rbac_perms = {
|
||||
'list': 'assets.view_assetaccountsecret',
|
||||
'retrieve': 'assets.view_assetaccountsecret',
|
||||
|
|
|
@ -277,7 +277,6 @@ def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
|
|||
check_different_city_login_if_need(user, request)
|
||||
data = generate_data(user.username, request, login_type=login_type)
|
||||
request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S")
|
||||
request.session["MFA_VERIFY_TIME"] = int(time.time())
|
||||
data.update({'mfa': int(user.mfa_enabled), 'status': True})
|
||||
write_login_log(**data)
|
||||
|
||||
|
|
|
@ -1,85 +1,57 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.generics import ListCreateAPIView
|
||||
from rest_framework.generics import RetrieveAPIView, CreateAPIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from common.permissions import IsValidUser
|
||||
from ..mfa import MFAOtp
|
||||
from ..const import ConfirmType
|
||||
from ..mixins import authenticate
|
||||
from ..serializers import ConfirmSerializer
|
||||
|
||||
|
||||
class ConfirmViewSet(ListCreateAPIView):
|
||||
class ConfirmApi(RetrieveAPIView, CreateAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
serializer_class = ConfirmSerializer
|
||||
|
||||
def check(self, confirm_type: str):
|
||||
if confirm_type == ConfirmType.MFA:
|
||||
return self.user.mfa_enabled
|
||||
def get_confirm_backend(self, confirm_type):
|
||||
backend_classes = ConfirmType.get_can_confirm_backend_classes(confirm_type)
|
||||
if not backend_classes:
|
||||
return
|
||||
for backend_cls in backend_classes:
|
||||
backend = backend_cls(self.request.user, self.request)
|
||||
if not backend.check():
|
||||
continue
|
||||
return backend
|
||||
|
||||
if confirm_type == ConfirmType.PASSWORD:
|
||||
return self.user.is_password_authenticate()
|
||||
|
||||
if confirm_type == ConfirmType.RELOGIN:
|
||||
return not self.user.is_password_authenticate()
|
||||
|
||||
def authenticate(self, confirm_type, secret_key):
|
||||
if confirm_type == ConfirmType.MFA:
|
||||
ok, msg = MFAOtp(self.user).check_code(secret_key)
|
||||
return ok, msg
|
||||
|
||||
if confirm_type == ConfirmType.PASSWORD:
|
||||
ok = authenticate(self.request, username=self.user.username, password=secret_key)
|
||||
msg = '' if ok else _('Authentication failed password incorrect')
|
||||
return ok, msg
|
||||
|
||||
if confirm_type == ConfirmType.RELOGIN:
|
||||
now = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
now = datetime.strptime(now, '%Y-%m-%d %H:%M:%S')
|
||||
login_time = self.request.session.get('login_time')
|
||||
SPECIFIED_TIME = 5
|
||||
msg = _('Login time has exceeded {} minutes, please login again').format(SPECIFIED_TIME)
|
||||
if not login_time:
|
||||
return False, msg
|
||||
login_time = datetime.strptime(login_time, '%Y-%m-%d %H:%M:%S')
|
||||
if (now - login_time).seconds >= SPECIFIED_TIME * 60:
|
||||
return False, msg
|
||||
return True, ''
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return self.request.user
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
return Response('ok')
|
||||
|
||||
mfa_verify_time = request.session.get('MFA_VERIFY_TIME', 0)
|
||||
if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL:
|
||||
return Response('ok')
|
||||
|
||||
data = []
|
||||
for i, confirm_type in enumerate(ConfirmType.values, 1):
|
||||
if self.check(confirm_type):
|
||||
data.append({'name': confirm_type, 'level': i})
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
confirm_type = request.query_params.get('confirm_type')
|
||||
backend = self.get_confirm_backend(confirm_type)
|
||||
if backend is None:
|
||||
msg = _('This action require verify your MFA')
|
||||
return Response({'error': msg, 'backends': data}, status=400)
|
||||
return Response(data={'error': msg}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
data = {
|
||||
'confirm_type': backend.name,
|
||||
'content': backend.content,
|
||||
}
|
||||
return Response(data=data)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
validated_data = serializer.validated_data
|
||||
|
||||
confirm_type = validated_data.get('confirm_type')
|
||||
mfa_type = validated_data.get('mfa_type')
|
||||
secret_key = validated_data.get('secret_key')
|
||||
ok, msg = self.authenticate(confirm_type, secret_key)
|
||||
|
||||
backend = self.get_confirm_backend(confirm_type)
|
||||
ok, msg = backend.authenticate(secret_key, mfa_type)
|
||||
if ok:
|
||||
request.session["MFA_VERIFY_TIME"] = int(time.time())
|
||||
request.session['CONFIRM_LEVEL'] = ConfirmType.values.index(confirm_type) + 1
|
||||
request.session['CONFIRM_TIME'] = int(time.time())
|
||||
return Response('ok')
|
||||
return Response({'error': msg}, status=400)
|
||||
|
|
|
@ -2,10 +2,11 @@ from rest_framework.views import APIView
|
|||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.permissions import UserConfirmation
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
from authentication.const import ConfirmType
|
||||
from authentication import errors
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
@ -26,7 +27,7 @@ class DingTalkQRUnBindBase(APIView):
|
|||
|
||||
|
||||
class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
|
||||
permission_classes = (IsAuthConfirmTimeValid,)
|
||||
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
|
||||
|
||||
|
||||
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
|
||||
|
|
|
@ -2,10 +2,11 @@ from rest_framework.views import APIView
|
|||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.permissions import UserConfirmation
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
from authentication.const import ConfirmType
|
||||
from authentication import errors
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
@ -26,7 +27,7 @@ class FeiShuQRUnBindBase(APIView):
|
|||
|
||||
|
||||
class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
|
||||
permission_classes = (IsAuthConfirmTimeValid,)
|
||||
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
|
||||
|
||||
|
||||
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):
|
||||
|
|
|
@ -10,22 +10,17 @@ from rest_framework.generics import CreateAPIView
|
|||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.permissions import IsValidUser, NeedMFAVerify
|
||||
from common.utils import get_logger
|
||||
from common.exceptions import UnexpectError
|
||||
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
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
__all__ = [
|
||||
'MFAChallengeVerifyApi', 'UserOtpVerifyApi',
|
||||
'MFASendCodeApi'
|
||||
'MFAChallengeVerifyApi', 'MFASendCodeApi'
|
||||
]
|
||||
|
||||
|
||||
|
@ -88,30 +83,3 @@ class MFAChallengeVerifyApi(AuthMixin, CreateAPIView):
|
|||
raise ValidationError(data)
|
||||
except errors.NeedMoreInfoError as e:
|
||||
return Response(e.as_data(), status=200)
|
||||
|
||||
|
||||
class UserOtpVerifyApi(CreateAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
serializer_class = OtpVerifySerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return Response({'code': 'valid', 'msg': 'verified'})
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
code = serializer.validated_data["code"]
|
||||
otp = MFAOtp(request.user)
|
||||
|
||||
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, {}").format(error)}, status=400)
|
||||
|
||||
def get_permissions(self):
|
||||
if self.request.method.lower() == 'get' \
|
||||
and settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
self.permission_classes = [NeedMFAVerify]
|
||||
return super().get_permissions()
|
||||
|
|
|
@ -2,10 +2,11 @@ from rest_framework.views import APIView
|
|||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.permissions import UserConfirmation
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
from authentication.const import ConfirmType
|
||||
from authentication import errors
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
@ -26,7 +27,7 @@ class WeComQRUnBindBase(APIView):
|
|||
|
||||
|
||||
class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
|
||||
permission_classes = (IsAuthConfirmTimeValid,)
|
||||
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
|
||||
|
||||
|
||||
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
from .mfa import ConfirmMFA
|
||||
from .password import ConfirmPassword
|
||||
from .relogin import ConfirmReLogin
|
||||
|
||||
CONFIRM_BACKENDS = [ConfirmReLogin, ConfirmPassword, ConfirmMFA]
|
|
@ -0,0 +1,30 @@
|
|||
import abc
|
||||
|
||||
|
||||
class BaseConfirm(abc.ABC):
|
||||
|
||||
def __init__(self, user, request):
|
||||
self.user = user
|
||||
self.request = request
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def name(self) -> str:
|
||||
return ''
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def display_name(self) -> str:
|
||||
return ''
|
||||
|
||||
@abc.abstractmethod
|
||||
def check(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
return ''
|
||||
|
||||
@abc.abstractmethod
|
||||
def authenticate(self, secret_key, mfa_type) -> tuple:
|
||||
return False, 'Error msg'
|
|
@ -0,0 +1,26 @@
|
|||
from users.models import User
|
||||
|
||||
from .base import BaseConfirm
|
||||
|
||||
|
||||
class ConfirmMFA(BaseConfirm):
|
||||
name = 'mfa'
|
||||
display_name = 'MFA'
|
||||
|
||||
def check(self):
|
||||
return self.user.active_mfa_backends
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
backends = User.get_user_mfa_backends(self.user)
|
||||
return [{
|
||||
'name': backend.name,
|
||||
'disabled': not bool(backend.is_active()),
|
||||
'display_name': backend.display_name,
|
||||
'placeholder': backend.placeholder,
|
||||
} for backend in backends]
|
||||
|
||||
def authenticate(self, secret_key, mfa_type):
|
||||
mfa_backend = self.user.get_mfa_backend_by_type(mfa_type)
|
||||
ok, msg = mfa_backend.check_code(secret_key)
|
||||
return ok, msg
|
|
@ -0,0 +1,17 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from authentication.mixins import authenticate
|
||||
from .base import BaseConfirm
|
||||
|
||||
|
||||
class ConfirmPassword(BaseConfirm):
|
||||
name = 'password'
|
||||
display_name = _('Password')
|
||||
|
||||
def check(self):
|
||||
return self.user.is_password_authenticate()
|
||||
|
||||
def authenticate(self, secret_key, mfa_type):
|
||||
ok = authenticate(self.request, username=self.user.username, password=secret_key)
|
||||
msg = '' if ok else _('Authentication failed password incorrect')
|
||||
return ok, msg
|
|
@ -0,0 +1,30 @@
|
|||
from datetime import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .base import BaseConfirm
|
||||
|
||||
SPECIFIED_TIME = 5
|
||||
|
||||
RELOGIN_ERROR = _('Login time has exceeded {} minutes, please login again').format(SPECIFIED_TIME)
|
||||
|
||||
|
||||
class ConfirmReLogin(BaseConfirm):
|
||||
name = 'relogin'
|
||||
display_name = 'Re-Login'
|
||||
|
||||
def check(self):
|
||||
return not self.user.is_password_authenticate()
|
||||
|
||||
def authenticate(self, secret_key, mfa_type):
|
||||
now = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
now = datetime.strptime(now, '%Y-%m-%d %H:%M:%S')
|
||||
login_time = self.request.session.get('login_time')
|
||||
msg = RELOGIN_ERROR
|
||||
if not login_time:
|
||||
return False, msg
|
||||
login_time = datetime.strptime(login_time, '%Y-%m-%d %H:%M:%S')
|
||||
if (now - login_time).seconds >= SPECIFIED_TIME * 60:
|
||||
return False, msg
|
||||
return True, ''
|
|
@ -1,10 +1,35 @@
|
|||
from django.db.models import TextChoices
|
||||
|
||||
from authentication.confirm import CONFIRM_BACKENDS
|
||||
from .confirm import ConfirmMFA, ConfirmPassword, ConfirmReLogin
|
||||
from .mfa import MFAOtp, MFASms, MFARadius
|
||||
|
||||
RSA_PRIVATE_KEY = 'rsa_private_key'
|
||||
RSA_PUBLIC_KEY = 'rsa_public_key'
|
||||
|
||||
CONFIRM_BACKEND_MAP = {backend.name: backend for backend in CONFIRM_BACKENDS}
|
||||
|
||||
|
||||
class ConfirmType(TextChoices):
|
||||
RELOGIN = 'relogin', 'Re-Login'
|
||||
PASSWORD = 'password', 'Password'
|
||||
MFA = 'mfa', 'MFA'
|
||||
ReLogin = ConfirmReLogin.name, ConfirmReLogin.display_name
|
||||
PASSWORD = ConfirmPassword.name, ConfirmPassword.display_name
|
||||
MFA = ConfirmMFA.name, ConfirmMFA.display_name
|
||||
|
||||
@classmethod
|
||||
def get_can_confirm_types(cls, confirm_type):
|
||||
start = cls.values.index(confirm_type)
|
||||
return cls.values[start:]
|
||||
|
||||
@classmethod
|
||||
def get_can_confirm_backend_classes(cls, confirm_type):
|
||||
types = cls.get_can_confirm_types(confirm_type)
|
||||
backend_classes = [
|
||||
CONFIRM_BACKEND_MAP[tp] for tp in types if tp in CONFIRM_BACKEND_MAP
|
||||
]
|
||||
return backend_classes
|
||||
|
||||
|
||||
class MFAType(TextChoices):
|
||||
OTP = MFAOtp.name, MFAOtp.display_name
|
||||
SMS = MFASms.name, MFASms.display_name
|
||||
Radius = MFARadius.name, MFARadius.display_name
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..const import ConfirmType
|
||||
from ..const import ConfirmType, MFAType
|
||||
|
||||
|
||||
class ConfirmSerializer(serializers.Serializer):
|
||||
confirm_type = serializers.ChoiceField(
|
||||
required=True, choices=ConfirmType.choices
|
||||
)
|
||||
confirm_type = serializers.ChoiceField(required=True, allow_blank=True, choices=ConfirmType.choices)
|
||||
mfa_type = serializers.ChoiceField(required=False, allow_blank=True, choices=MFAType.choices)
|
||||
secret_key = EncryptedField()
|
||||
|
|
|
@ -4,9 +4,8 @@ from rest_framework import serializers
|
|||
|
||||
from common.drf.fields import EncryptedField
|
||||
|
||||
|
||||
__all__ = [
|
||||
'OtpVerifySerializer', 'MFAChallengeSerializer', 'MFASelectTypeSerializer',
|
||||
'MFAChallengeSerializer', 'MFASelectTypeSerializer',
|
||||
'PasswordVerifySerializer',
|
||||
]
|
||||
|
||||
|
@ -29,7 +28,3 @@ class MFAChallengeSerializer(serializers.Serializer):
|
|||
|
||||
def update(self, instance, validated_data):
|
||||
pass
|
||||
|
||||
|
||||
class OtpVerifySerializer(serializers.Serializer):
|
||||
code = serializers.CharField(max_length=6, min_length=6)
|
||||
|
|
|
@ -26,13 +26,12 @@ urlpatterns = [
|
|||
path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(), name='feishu-event-subscription-callback'),
|
||||
|
||||
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
|
||||
path('confirm/', api.ConfirmViewSet.as_view(), name='user-confirm'),
|
||||
path('confirm/', api.ConfirmApi.as_view(), name='user-confirm'),
|
||||
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
|
||||
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('password/verify/', api.UserPasswordVerifyApi.as_view(), name='user-password-verify'),
|
||||
path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'),
|
||||
]
|
||||
|
|
|
@ -8,17 +8,17 @@ from django.db.utils import IntegrityError
|
|||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
from users.views import UserVerifyPasswordView
|
||||
from users.utils import is_auth_confirm_time_valid
|
||||
from users.models import User
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from users.views import UserVerifyPasswordView
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
from common.sdk.im.dingtalk import URL
|
||||
from common.mixins.views import PermissionsMixin
|
||||
from common.mixins.views import UserConfirmRequiredExceptionMixin, PermissionsMixin
|
||||
from common.permissions import UserConfirmation
|
||||
from authentication import errors
|
||||
from authentication.mixins import AuthMixin
|
||||
from authentication.const import ConfirmType
|
||||
from common.sdk.im.dingtalk import DingTalk
|
||||
from common.utils.common import get_request_ip
|
||||
from authentication.notifications import OAuthBindMessage
|
||||
|
@ -30,7 +30,7 @@ logger = get_logger(__file__)
|
|||
DINGTALK_STATE_SESSION_KEY = '_dingtalk_state'
|
||||
|
||||
|
||||
class DingTalkBaseMixin(PermissionsMixin, View):
|
||||
class DingTalkBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, View):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
@ -119,7 +119,7 @@ class DingTalkOAuthMixin(DingTalkBaseMixin, View):
|
|||
|
||||
|
||||
class DingTalkQRBindView(DingTalkQRMixin, View):
|
||||
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
|
||||
permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.ReLogin))
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
user = request.user
|
||||
|
|
|
@ -8,16 +8,17 @@ from django.db.utils import IntegrityError
|
|||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from users.views import UserVerifyPasswordView
|
||||
from users.models import User
|
||||
from users.views import UserVerifyPasswordView
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
from common.mixins.views import PermissionsMixin
|
||||
from common.mixins.views import UserConfirmRequiredExceptionMixin, PermissionsMixin
|
||||
from common.permissions import UserConfirmation
|
||||
from common.sdk.im.feishu import FeiShu, URL
|
||||
from common.utils.common import get_request_ip
|
||||
from authentication import errors
|
||||
from authentication.const import ConfirmType
|
||||
from authentication.mixins import AuthMixin
|
||||
from authentication.notifications import OAuthBindMessage
|
||||
|
||||
|
@ -27,7 +28,7 @@ logger = get_logger(__file__)
|
|||
FEISHU_STATE_SESSION_KEY = '_feishu_state'
|
||||
|
||||
|
||||
class FeiShuQRMixin(PermissionsMixin, View):
|
||||
class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, View):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
@ -89,7 +90,7 @@ class FeiShuQRMixin(PermissionsMixin, View):
|
|||
|
||||
|
||||
class FeiShuQRBindView(FeiShuQRMixin, View):
|
||||
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
|
||||
permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.ReLogin))
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
user = request.user
|
||||
|
|
|
@ -8,18 +8,19 @@ from django.db.utils import IntegrityError
|
|||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
from users.views import UserVerifyPasswordView
|
||||
from users.models import User
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from users.views import UserVerifyPasswordView
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
from common.sdk.im.wecom import URL
|
||||
from common.sdk.im.wecom import WeCom
|
||||
from common.mixins.views import PermissionsMixin
|
||||
from common.mixins.views import UserConfirmRequiredExceptionMixin, PermissionsMixin
|
||||
from common.utils.common import get_request_ip
|
||||
from common.permissions import UserConfirmation
|
||||
from authentication import errors
|
||||
from authentication.mixins import AuthMixin
|
||||
from authentication.const import ConfirmType
|
||||
from authentication.notifications import OAuthBindMessage
|
||||
from .mixins import METAMixin
|
||||
|
||||
|
@ -29,7 +30,7 @@ logger = get_logger(__file__)
|
|||
WECOM_STATE_SESSION_KEY = '_wecom_state'
|
||||
|
||||
|
||||
class WeComBaseMixin(PermissionsMixin, View):
|
||||
class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, View):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
@ -118,7 +119,7 @@ class WeComOAuthMixin(WeComBaseMixin, View):
|
|||
|
||||
|
||||
class WeComQRBindView(WeComQRMixin, View):
|
||||
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
|
||||
permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.ReLogin))
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
user = request.user
|
||||
|
|
|
@ -41,10 +41,13 @@ class ReferencedByOthers(JMSException):
|
|||
default_detail = _('Is referenced by other objects and cannot be deleted')
|
||||
|
||||
|
||||
class MFAVerifyRequired(JMSException):
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
default_code = 'mfa_verify_required'
|
||||
default_detail = _('This action require verify your MFA')
|
||||
class UserConfirmRequired(JMSException):
|
||||
def __init__(self, code=None):
|
||||
detail = {
|
||||
'code': code,
|
||||
'detail': _('This action require confirm current user')
|
||||
}
|
||||
super().__init__(detail=detail, code=code)
|
||||
|
||||
|
||||
class UnexpectError(JMSException):
|
||||
|
|
|
@ -2,16 +2,26 @@
|
|||
#
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.http.response import JsonResponse
|
||||
from rest_framework import permissions
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.permissions import IsValidUser
|
||||
from common.exceptions import UserConfirmRequired
|
||||
from audits.utils import create_operate_log
|
||||
from audits.models import OperateLog
|
||||
|
||||
__all__ = ["PermissionsMixin", "RecordViewLogMixin"]
|
||||
__all__ = ["PermissionsMixin", "RecordViewLogMixin", "UserConfirmRequiredExceptionMixin"]
|
||||
|
||||
|
||||
class UserConfirmRequiredExceptionMixin:
|
||||
"""
|
||||
异常处理
|
||||
"""
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
except UserConfirmRequired as e:
|
||||
return JsonResponse(e.detail, status=e.status_code)
|
||||
|
||||
|
||||
class PermissionsMixin(UserPassesTestMixin):
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import time
|
||||
from rest_framework import permissions
|
||||
|
||||
from django.conf import settings
|
||||
from common.exceptions import MFAVerifyRequired
|
||||
from rest_framework import permissions
|
||||
|
||||
from authentication.const import ConfirmType
|
||||
from common.exceptions import UserConfirmRequired
|
||||
|
||||
|
||||
class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission):
|
||||
|
@ -29,18 +32,23 @@ class WithBootstrapToken(permissions.BasePermission):
|
|||
return settings.BOOTSTRAP_TOKEN == request_bootstrap_token
|
||||
|
||||
|
||||
class NeedMFAVerify(permissions.BasePermission):
|
||||
class UserConfirmation(permissions.BasePermission):
|
||||
ttl = 300
|
||||
min_level = 1
|
||||
confirm_type = ConfirmType.ReLogin
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
confirm_level = request.session.get('CONFIRM_LEVEL')
|
||||
confirm_time = request.session.get('CONFIRM_TIME')
|
||||
|
||||
if not confirm_level or not confirm_time or \
|
||||
confirm_level < self.min_level or \
|
||||
confirm_time < time.time() - self.ttl:
|
||||
raise UserConfirmRequired(code=self.confirm_type)
|
||||
return True
|
||||
|
||||
mfa_verify_time = request.session.get('MFA_VERIFY_TIME', 0)
|
||||
if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL:
|
||||
return True
|
||||
raise MFAVerifyRequired()
|
||||
|
||||
|
||||
class IsObjectOwner(IsValidUser):
|
||||
def has_object_permission(self, request, view, obj):
|
||||
return (super().has_object_permission(request, view, obj) and
|
||||
request.user == getattr(obj, 'user', None))
|
||||
@classmethod
|
||||
def require(cls, confirm_type=ConfirmType.ReLogin, ttl=300):
|
||||
min_level = ConfirmType.values.index(confirm_type) + 1
|
||||
name = 'UserConfirmationLevel{}TTL{}'.format(min_level, ttl)
|
||||
return type(name, (cls,), {'min_level': min_level, 'ttl': ttl, 'confirm_type': confirm_type})
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:896e5302ad4e775f9160b51385466525e67f0e5a0a8cc10981ea843626114494
|
||||
size 125939
|
||||
oid sha256:50095e2b80c1235a812856f709061dc171509a3883752af48b3c1bb0c112f025
|
||||
size 259
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:521d1a529b430f1ac6a519f9c659abe79f25fc9b05a03519f4abe945ae2d1972
|
||||
size 103946
|
||||
oid sha256:e4ec3219881a5a03c49f988a0ea48668ff8403356d210b7e7c6fbb9e8be26a6b
|
||||
size 259
|
||||
|
|
|
@ -1754,30 +1754,14 @@ msgstr "{ApplicationPermission} 添加 {SystemUser}"
|
|||
msgid "{ApplicationPermission} REMOVE {SystemUser}"
|
||||
msgstr "{ApplicationPermission} 移除 {SystemUser}"
|
||||
|
||||
#: authentication/api/confirm.py:40
|
||||
msgid "Authentication failed password incorrect"
|
||||
msgstr "认证失败 (用户名或密码不正确)"
|
||||
|
||||
#: authentication/api/confirm.py:48
|
||||
msgid "Login time has exceeded {} minutes, please login again"
|
||||
msgstr "登录时长已超过 {} 分钟,请重新登录"
|
||||
|
||||
#: authentication/api/confirm.py:72 common/exceptions.py:47
|
||||
msgid "This action require verify your MFA"
|
||||
msgstr "这个操作需要验证 MFA"
|
||||
|
||||
#: authentication/api/connection_token.py:326
|
||||
msgid "Invalid token"
|
||||
msgstr "无效的令牌"
|
||||
|
||||
#: authentication/api/mfa.py:64
|
||||
#: authentication/api/mfa.py:59
|
||||
msgid "Current user not support mfa type: {}"
|
||||
msgstr "当前用户不支持 MFA 类型: {}"
|
||||
|
||||
#: authentication/api/mfa.py:111
|
||||
msgid "Code is invalid, {}"
|
||||
msgstr "验证码无效: {}"
|
||||
|
||||
#: authentication/apps.py:7
|
||||
msgid "Authentication"
|
||||
msgstr "认证"
|
||||
|
@ -1833,6 +1817,15 @@ msgstr "无效的令牌头。符号字符串不应包含无效字符。"
|
|||
msgid "Invalid token or cache refreshed."
|
||||
msgstr "刷新的令牌或缓存无效。"
|
||||
|
||||
#: authentication/confirm/password.py:17
|
||||
msgid "Authentication failed password incorrect"
|
||||
msgstr "认证失败 (用户名或密码不正确)"
|
||||
|
||||
#: authentication/confirm/relogin.py:10
|
||||
msgid "Login time has exceeded {} minutes, please login again"
|
||||
msgstr "登录时长已超过 {} 分钟,请重新登录"
|
||||
|
||||
#: authentication/errors.py:26
|
||||
#: authentication/errors/const.py:18
|
||||
msgid "Username/password check failed"
|
||||
msgstr "用户名/密码 校验失败"
|
||||
|
@ -2561,7 +2554,11 @@ msgstr "多对多反向是不被允许的"
|
|||
msgid "Is referenced by other objects and cannot be deleted"
|
||||
msgstr "被其他对象关联,不能删除"
|
||||
|
||||
#: common/exceptions.py:53
|
||||
#: common/exceptions.py:46
|
||||
msgid "This action require confirm current user"
|
||||
msgstr "此操作需要确认当前用户"
|
||||
|
||||
#: common/exceptions.py:52
|
||||
msgid "Unexpect error occur"
|
||||
msgstr "发生意外错误"
|
||||
|
||||
|
@ -6653,3 +6650,6 @@ msgstr "旗舰版"
|
|||
#: xpack/plugins/license/models.py:77
|
||||
msgid "Community edition"
|
||||
msgstr "社区版"
|
||||
|
||||
#~ msgid "Code is invalid, {}"
|
||||
#~ msgstr "验证码无效: {}"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from rest_framework import permissions
|
||||
|
||||
from .utils import is_auth_password_time_valid, is_auth_confirm_time_valid
|
||||
from .utils import is_auth_password_time_valid
|
||||
|
||||
|
||||
class IsAuthPasswdTimeValid(permissions.IsAuthenticated):
|
||||
|
@ -8,10 +8,3 @@ class IsAuthPasswdTimeValid(permissions.IsAuthenticated):
|
|||
def has_permission(self, request, view):
|
||||
return super().has_permission(request, view) \
|
||||
and is_auth_password_time_valid(request.session)
|
||||
|
||||
|
||||
class IsAuthConfirmTimeValid(permissions.IsAuthenticated):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return super().has_permission(request, view) \
|
||||
and is_auth_confirm_time_valid(request.session)
|
||||
|
|
Loading…
Reference in New Issue