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
fit2bot 2022-07-04 11:29:39 +08:00 committed by GitHub
parent ca19e45905
commit a6cc8a8b05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 273 additions and 188 deletions

View File

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

View File

@ -11,5 +11,4 @@
"""
from common.permissions import NeedMFAVerify
from users.models import User, UserGroup

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
from .mfa import ConfirmMFA
from .password import ConfirmPassword
from .relogin import ConfirmReLogin
CONFIRM_BACKENDS = [ConfirmReLogin, ConfirmPassword, ConfirmMFA]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:896e5302ad4e775f9160b51385466525e67f0e5a0a8cc10981ea843626114494
size 125939
oid sha256:50095e2b80c1235a812856f709061dc171509a3883752af48b3c1bb0c112f025
size 259

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:521d1a529b430f1ac6a519f9c659abe79f25fc9b05a03519f4abe945ae2d1972
size 103946
oid sha256:e4ec3219881a5a03c49f988a0ea48668ff8403356d210b7e7c6fbb9e8be26a6b
size 259

View File

@ -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 "验证码无效: {}"

View File

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