From a6cc8a8b0574119f93ff9ed5adaaf1b4d90d95c4 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Mon, 4 Jul 2022 11:29:39 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96confirm=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=20(#8451)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: 优化confirm接口 * perf: 修改 校验 * perf: 优化 confirm API 逻辑 * Delete django.po Co-authored-by: feng626 <1304903146@qq.com> Co-authored-by: ibuler Co-authored-by: Jiangjie.Bai Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com> --- apps/applications/api/account.py | 7 +- apps/applications/hands.py | 1 - apps/assets/api/accounts.py | 7 +- apps/audits/signal_handlers.py | 1 - apps/authentication/api/confirm.py | 88 +++++++------------ apps/authentication/api/dingtalk.py | 5 +- apps/authentication/api/feishu.py | 5 +- apps/authentication/api/mfa.py | 34 +------ apps/authentication/api/wecom.py | 5 +- apps/authentication/confirm/__init__.py | 5 ++ apps/authentication/confirm/base.py | 30 +++++++ apps/authentication/confirm/mfa.py | 26 ++++++ apps/authentication/confirm/password.py | 17 ++++ apps/authentication/confirm/relogin.py | 30 +++++++ apps/authentication/const.py | 31 ++++++- apps/authentication/serializers/confirm.py | 7 +- .../serializers/password_mfa.py | 7 +- apps/authentication/urls/api_urls.py | 3 +- apps/authentication/views/dingtalk.py | 12 +-- apps/authentication/views/feishu.py | 11 +-- apps/authentication/views/wecom.py | 11 +-- apps/common/exceptions.py | 11 ++- apps/common/mixins/views.py | 18 +++- apps/common/permissions.py | 36 +++++--- apps/locale/ja/LC_MESSAGES/django.mo | 4 +- apps/locale/zh/LC_MESSAGES/django.mo | 4 +- apps/locale/zh/LC_MESSAGES/django.po | 36 ++++---- apps/users/permissions.py | 9 +- 28 files changed, 273 insertions(+), 188 deletions(-) create mode 100644 apps/authentication/confirm/__init__.py create mode 100644 apps/authentication/confirm/base.py create mode 100644 apps/authentication/confirm/mfa.py create mode 100644 apps/authentication/confirm/password.py create mode 100644 apps/authentication/confirm/relogin.py diff --git a/apps/applications/api/account.py b/apps/applications/api/account.py index 0a1581b28..bad9a02bd 100644 --- a/apps/applications/api/account.py +++ b/apps/applications/api/account.py @@ -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', diff --git a/apps/applications/hands.py b/apps/applications/hands.py index 74d6bf9cb..c2bf4fd20 100644 --- a/apps/applications/hands.py +++ b/apps/applications/hands.py @@ -11,5 +11,4 @@ """ -from common.permissions import NeedMFAVerify from users.models import User, UserGroup diff --git a/apps/assets/api/accounts.py b/apps/assets/api/accounts.py index 4a3629e46..33d2f517a 100644 --- a/apps/assets/api/accounts.py +++ b/apps/assets/api/accounts.py @@ -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', diff --git a/apps/audits/signal_handlers.py b/apps/audits/signal_handlers.py index 902c0896e..9f20057a3 100644 --- a/apps/audits/signal_handlers.py +++ b/apps/audits/signal_handlers.py @@ -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) diff --git a/apps/authentication/api/confirm.py b/apps/authentication/api/confirm.py index 9041f6e85..c085bb017 100644 --- a/apps/authentication/api/confirm.py +++ b/apps/authentication/api/confirm.py @@ -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) diff --git a/apps/authentication/api/dingtalk.py b/apps/authentication/api/dingtalk.py index ccc3db64d..817b5276e 100644 --- a/apps/authentication/api/dingtalk.py +++ b/apps/authentication/api/dingtalk.py @@ -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): diff --git a/apps/authentication/api/feishu.py b/apps/authentication/api/feishu.py index 13637b247..878ec6e2d 100644 --- a/apps/authentication/api/feishu.py +++ b/apps/authentication/api/feishu.py @@ -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): diff --git a/apps/authentication/api/mfa.py b/apps/authentication/api/mfa.py index 90aeb7fc4..fe81149e3 100644 --- a/apps/authentication/api/mfa.py +++ b/apps/authentication/api/mfa.py @@ -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() diff --git a/apps/authentication/api/wecom.py b/apps/authentication/api/wecom.py index e64bc9919..cdde00bc9 100644 --- a/apps/authentication/api/wecom.py +++ b/apps/authentication/api/wecom.py @@ -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): diff --git a/apps/authentication/confirm/__init__.py b/apps/authentication/confirm/__init__.py new file mode 100644 index 000000000..1c3acd05b --- /dev/null +++ b/apps/authentication/confirm/__init__.py @@ -0,0 +1,5 @@ +from .mfa import ConfirmMFA +from .password import ConfirmPassword +from .relogin import ConfirmReLogin + +CONFIRM_BACKENDS = [ConfirmReLogin, ConfirmPassword, ConfirmMFA] diff --git a/apps/authentication/confirm/base.py b/apps/authentication/confirm/base.py new file mode 100644 index 000000000..63258abce --- /dev/null +++ b/apps/authentication/confirm/base.py @@ -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' diff --git a/apps/authentication/confirm/mfa.py b/apps/authentication/confirm/mfa.py new file mode 100644 index 000000000..aee272e9d --- /dev/null +++ b/apps/authentication/confirm/mfa.py @@ -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 diff --git a/apps/authentication/confirm/password.py b/apps/authentication/confirm/password.py new file mode 100644 index 000000000..944ab8f24 --- /dev/null +++ b/apps/authentication/confirm/password.py @@ -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 diff --git a/apps/authentication/confirm/relogin.py b/apps/authentication/confirm/relogin.py new file mode 100644 index 000000000..447a17ab0 --- /dev/null +++ b/apps/authentication/confirm/relogin.py @@ -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, '' diff --git a/apps/authentication/const.py b/apps/authentication/const.py index 4c8578dff..ffe92ffbf 100644 --- a/apps/authentication/const.py +++ b/apps/authentication/const.py @@ -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 diff --git a/apps/authentication/serializers/confirm.py b/apps/authentication/serializers/confirm.py index fe5984190..3133a8cf4 100644 --- a/apps/authentication/serializers/confirm.py +++ b/apps/authentication/serializers/confirm.py @@ -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() diff --git a/apps/authentication/serializers/password_mfa.py b/apps/authentication/serializers/password_mfa.py index cf3452af3..f52274e49 100644 --- a/apps/authentication/serializers/password_mfa.py +++ b/apps/authentication/serializers/password_mfa.py @@ -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) diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index 44a89bf97..02c920c5b 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -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'), ] diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py index 6b93b3d4c..0d19d3fcd 100644 --- a/apps/authentication/views/dingtalk.py +++ b/apps/authentication/views/dingtalk.py @@ -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 diff --git a/apps/authentication/views/feishu.py b/apps/authentication/views/feishu.py index ea0db44e1..da7999b95 100644 --- a/apps/authentication/views/feishu.py +++ b/apps/authentication/views/feishu.py @@ -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 diff --git a/apps/authentication/views/wecom.py b/apps/authentication/views/wecom.py index cb5cc2178..c9315bdb5 100644 --- a/apps/authentication/views/wecom.py +++ b/apps/authentication/views/wecom.py @@ -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 diff --git a/apps/common/exceptions.py b/apps/common/exceptions.py index 067750ec6..5fa8861b0 100644 --- a/apps/common/exceptions.py +++ b/apps/common/exceptions.py @@ -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): diff --git a/apps/common/mixins/views.py b/apps/common/mixins/views.py index 0028ccf0a..9f824e5e2 100644 --- a/apps/common/mixins/views.py +++ b/apps/common/mixins/views.py @@ -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): diff --git a/apps/common/permissions.py b/apps/common/permissions.py index b52c0cc5c..7242c255b 100644 --- a/apps/common/permissions.py +++ b/apps/common/permissions.py @@ -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}) diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index f85e303fe..f3f84db91 100644 --- a/apps/locale/ja/LC_MESSAGES/django.mo +++ b/apps/locale/ja/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:896e5302ad4e775f9160b51385466525e67f0e5a0a8cc10981ea843626114494 -size 125939 +oid sha256:50095e2b80c1235a812856f709061dc171509a3883752af48b3c1bb0c112f025 +size 259 diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index a0958caa0..c4fe0af59 100644 --- a/apps/locale/zh/LC_MESSAGES/django.mo +++ b/apps/locale/zh/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:521d1a529b430f1ac6a519f9c659abe79f25fc9b05a03519f4abe945ae2d1972 -size 103946 +oid sha256:e4ec3219881a5a03c49f988a0ea48668ff8403356d210b7e7c6fbb9e8be26a6b +size 259 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 33f0febc8..038ddf895 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -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 "验证码无效: {}" diff --git a/apps/users/permissions.py b/apps/users/permissions.py index a0df71116..ee821f8d7 100644 --- a/apps/users/permissions.py +++ b/apps/users/permissions.py @@ -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)