perf: 统一校验当前用户api (#8324)

Co-authored-by: feng626 <1304903146@qq.com>
pull/8348/head
fit2bot 2022-06-07 19:26:07 +08:00 committed by GitHub
parent 2366f02d10
commit a5acdb9f60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 148 additions and 31 deletions

View File

@ -274,6 +274,7 @@ def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
logger.debug('User login success: {}'.format(user.username))
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")
data.update({'mfa': int(user.mfa_enabled), 'status': True})
write_login_log(**data)

View File

@ -5,6 +5,7 @@ from .connection_token import *
from .token import *
from .mfa import *
from .access_key import *
from .confirm import *
from .login_confirm import *
from .sso import *
from .wecom import *

View File

@ -0,0 +1,85 @@
# -*- 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.response import Response
from common.permissions import IsValidUser
from ..mfa import MFAOtp
from ..const import ConfirmType
from ..mixins import authenticate
from ..serializers import ConfirmSerializer
class ConfirmViewSet(ListCreateAPIView):
permission_classes = (IsValidUser,)
serializer_class = ConfirmSerializer
def check(self, confirm_type: str):
if confirm_type == ConfirmType.MFA:
return bool(MFAOtp(self.user).is_active())
if confirm_type == ConfirmType.PASSWORD:
return self.user.is_password_authenticate()
if confirm_type == ConfirmType.RELOGIN:
return not self.user.is_password_authenticate()
def authenticate(self, confirm_type, secret_key):
if confirm_type == ConfirmType.MFA:
ok, msg = MFAOtp(self.user).check_code(secret_key)
return ok, msg
if confirm_type == ConfirmType.PASSWORD:
ok = authenticate(self.request, username=self.user.username, password=secret_key)
msg = '' if ok else _('Authentication failed password incorrect')
return ok, msg
if confirm_type == ConfirmType.RELOGIN:
now = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
now = datetime.strptime(now, '%Y-%m-%d %H:%M:%S')
login_time = self.request.session.get('login_time')
SPECIFIED_TIME = 5
msg = _('Login time has exceeded {} minutes, please login again').format(SPECIFIED_TIME)
if not login_time:
return False, msg
login_time = datetime.strptime(login_time, '%Y-%m-%d %H:%M:%S')
if (now - login_time).seconds >= SPECIFIED_TIME * 60:
return False, msg
return True, ''
@property
def user(self):
return self.request.user
def list(self, request, *args, **kwargs):
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
return Response('ok')
mfa_verify_time = request.session.get('MFA_VERIFY_TIME', 0)
if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL:
return Response('ok')
data = []
for i, confirm_type in enumerate(ConfirmType.values, 1):
if self.check(confirm_type):
data.append({'name': confirm_type, 'level': i})
msg = _('This action require verify your MFA')
return Response({'error': msg, 'backends': data}, status=400)
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')
secret_key = validated_data.get('secret_key')
ok, msg = self.authenticate(confirm_type, secret_key)
if ok:
request.session["MFA_VERIFY_TIME"] = int(time.time())
return Response('ok')
return Response({'error': msg}, status=400)

View File

@ -2,7 +2,7 @@ from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from users.permissions import IsAuthPasswdTimeValid
from users.permissions import IsAuthConfirmTimeValid
from users.models import User
from common.utils import get_logger
from common.mixins.api import RoleUserMixin, RoleAdminMixin
@ -26,9 +26,8 @@ class DingTalkQRUnBindBase(APIView):
class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
permission_classes = (IsAuthPasswdTimeValid,)
permission_classes = (IsAuthConfirmTimeValid,)
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
user_id_url_kwarg = 'user_id'

View File

@ -2,7 +2,7 @@ from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from users.permissions import IsAuthPasswdTimeValid
from users.permissions import IsAuthConfirmTimeValid
from users.models import User
from common.utils import get_logger
from common.mixins.api import RoleUserMixin, RoleAdminMixin
@ -26,7 +26,7 @@ class FeiShuQRUnBindBase(APIView):
class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
permission_classes = (IsAuthPasswdTimeValid,)
permission_classes = (IsAuthConfirmTimeValid,)
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):

View File

@ -2,7 +2,7 @@ from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from users.permissions import IsAuthPasswdTimeValid
from users.permissions import IsAuthConfirmTimeValid
from users.models import User
from common.utils import get_logger
from common.mixins.api import RoleUserMixin, RoleAdminMixin
@ -26,9 +26,8 @@ class WeComQRUnBindBase(APIView):
class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
permission_classes = (IsAuthPasswdTimeValid,)
permission_classes = (IsAuthConfirmTimeValid,)
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
user_id_url_kwarg = 'user_id'

View File

@ -1,2 +1,10 @@
from django.db.models import TextChoices
RSA_PRIVATE_KEY = 'rsa_private_key'
RSA_PUBLIC_KEY = 'rsa_public_key'
class ConfirmType(TextChoices):
RELOGIN = 'relogin', 'Re-Login'
PASSWORD = 'password', 'Password'
MFA = 'mfa', 'MFA'

View File

@ -1,3 +1,4 @@
from .token import *
from .connect_token import *
from .password_mfa import *
from .confirm import *

View File

@ -0,0 +1,11 @@
from rest_framework import serializers
from common.drf.fields import EncryptedField
from ..const import ConfirmType
class ConfirmSerializer(serializers.Serializer):
confirm_type = serializers.ChoiceField(
required=True, choices=ConfirmType.choices
)
secret_key = EncryptedField()

View File

@ -26,6 +26,7 @@ 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('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'),

View File

@ -9,8 +9,9 @@ from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.exceptions import APIException
from users.views import UserVerifyPasswordView
from users.utils import is_auth_password_time_valid
from users.utils import is_auth_confirm_time_valid
from users.models import User
from users.permissions import IsAuthConfirmTimeValid
from common.utils import get_logger, FlashMessageUtil
from common.utils.random import random_string
from common.utils.django import reverse, get_object_or_none
@ -118,17 +119,12 @@ class DingTalkOAuthMixin(DingTalkBaseMixin, View):
class DingTalkQRBindView(DingTalkQRMixin, View):
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
def get(self, request: HttpRequest):
user = request.user
redirect_url = request.GET.get('redirect_url')
if not is_auth_password_time_valid(request.session):
msg = _('Please verify your password first')
response = self.get_failed_response(redirect_url, msg, msg)
return response
redirect_uri = reverse('authentication:dingtalk-qr-bind-callback', kwargs={'user_id': user.id}, external=True)
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})

View File

@ -8,7 +8,7 @@ from django.db.utils import IntegrityError
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.exceptions import APIException
from users.utils import is_auth_password_time_valid
from users.permissions import IsAuthConfirmTimeValid
from users.views import UserVerifyPasswordView
from users.models import User
from common.utils import get_logger, FlashMessageUtil
@ -89,17 +89,12 @@ class FeiShuQRMixin(PermissionsMixin, View):
class FeiShuQRBindView(FeiShuQRMixin, View):
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
def get(self, request: HttpRequest):
user = request.user
redirect_url = request.GET.get('redirect_url')
if not is_auth_password_time_valid(request.session):
msg = _('Please verify your password first')
response = self.get_failed_response(redirect_url, msg, msg)
return response
redirect_uri = reverse('authentication:feishu-qr-bind-callback', external=True)
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})

View File

@ -9,8 +9,8 @@ from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.exceptions import APIException
from users.views import UserVerifyPasswordView
from users.utils import is_auth_password_time_valid
from users.models import User
from users.permissions import IsAuthConfirmTimeValid
from common.utils import get_logger, FlashMessageUtil
from common.utils.random import random_string
from common.utils.django import reverse, get_object_or_none
@ -118,17 +118,12 @@ class WeComOAuthMixin(WeComBaseMixin, View):
class WeComQRBindView(WeComQRMixin, View):
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
def get(self, request: HttpRequest):
user = request.user
redirect_url = request.GET.get('redirect_url')
if not is_auth_password_time_valid(request.session):
msg = _('Please verify your password first')
response = self.get_failed_response(redirect_url, msg, msg)
return response
redirect_uri = reverse('authentication:wecom-qr-bind-callback', kwargs={'user_id': user.id}, external=True)
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})

View File

@ -791,6 +791,11 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
def is_local(self):
return self.source == self.Source.local.value
def is_password_authenticate(self):
cas = self.Source.cas
saml2 = self.Source.saml2
return self.source not in [cas, saml2]
def set_unprovide_attr_if_need(self):
if not self.name:
self.name = self.username

View File

@ -1,10 +1,17 @@
from rest_framework import permissions
from .utils import is_auth_password_time_valid
from .utils import is_auth_password_time_valid, is_auth_confirm_time_valid
class IsAuthPasswdTimeValid(permissions.IsAuthenticated):
def has_permission(self, request, view):
return super().has_permission(request, view) \
and is_auth_password_time_valid(request.session)
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)

View File

@ -255,3 +255,16 @@ def is_auth_password_time_valid(session):
def is_auth_otp_time_valid(session):
return is_auth_time_valid(session, 'auth_otp_expired_at')
def is_confirm_time_valid(session, key):
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
return True
mfa_verify_time = session.get(key, 0)
if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL:
return True
return False
def is_auth_confirm_time_valid(session):
return is_confirm_time_valid(session, 'MFA_VERIFY_TIME')