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() | ||||
|     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(data={'error': msg}, status=status.HTTP_404_NOT_FOUND) | ||||
| 
 | ||||
|         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}) | ||||
|         msg = _('This action require verify your MFA') | ||||
|         return Response({'error': msg, 'backends': data}, status=400) | ||||
|         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: | ||||
|             return True | ||||
|         confirm_level = request.session.get('CONFIRM_LEVEL') | ||||
|         confirm_time = request.session.get('CONFIRM_TIME') | ||||
| 
 | ||||
|         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() | ||||
|         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 | ||||
| 
 | ||||
| 
 | ||||
| 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
	
	 fit2bot
						fit2bot