From 452ee1224cc364a01d27d7b084680560afe562b4 Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 13 Oct 2023 14:40:40 +0800 Subject: [PATCH 1/5] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=A1=AE=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/confirm.py | 17 +++++++++++++---- apps/authentication/backends/passkey/api.py | 15 +++++++++++++-- apps/authentication/confirm/relogin.py | 1 + apps/authentication/const.py | 1 + apps/authentication/serializers/confirm.py | 2 +- apps/authentication/urls/api_urls.py | 2 +- apps/common/exceptions.py | 3 +++ 7 files changed, 33 insertions(+), 8 deletions(-) diff --git a/apps/authentication/api/confirm.py b/apps/authentication/api/confirm.py index 4bd8d2c72..2bb371e48 100644 --- a/apps/authentication/api/confirm.py +++ b/apps/authentication/api/confirm.py @@ -4,10 +4,12 @@ import time from django.utils.translation import gettext_lazy as _ from rest_framework import status -from rest_framework.generics import RetrieveAPIView, CreateAPIView +from rest_framework.decorators import action +from rest_framework.generics import RetrieveAPIView from rest_framework.response import Response from authentication.permissions import UserConfirmation +from common.api import JMSGenericViewSet from common.permissions import IsValidUser from ..const import ConfirmType from ..serializers import ConfirmSerializer @@ -20,10 +22,17 @@ class ConfirmBindORUNBindOAuth(RetrieveAPIView): return Response('ok') -class ConfirmApi(RetrieveAPIView, CreateAPIView): +class UserConfirmationViewSet(JMSGenericViewSet): permission_classes = (IsValidUser,) serializer_class = ConfirmSerializer + @action(methods=['get'], detail=False) + def check(self, request): + confirm_type = request.query_params.get('confirm_type', 'password') + permission = UserConfirmation.require(confirm_type)() + permission.has_permission(request, self) + return Response('ok') + def get_confirm_backend(self, confirm_type): backend_classes = ConfirmType.get_prop_backends(confirm_type) if not backend_classes: @@ -34,12 +43,12 @@ class ConfirmApi(RetrieveAPIView, CreateAPIView): continue return backend - def retrieve(self, request, *args, **kwargs): + def list(self, request, *args, **kwargs): confirm_type = request.query_params.get('confirm_type', 'password') 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) + return Response(data={'error': msg}, status=status.HTTP_400_BAD_REQUEST) data = { 'confirm_type': backend.name, diff --git a/apps/authentication/backends/passkey/api.py b/apps/authentication/backends/passkey/api.py index 644636dd2..ace882a92 100644 --- a/apps/authentication/backends/passkey/api.py +++ b/apps/authentication/backends/passkey/api.py @@ -4,19 +4,30 @@ from django.shortcuts import render from django.utils.translation import gettext as _ from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated, AllowAny -from rest_framework.viewsets import ModelViewSet from authentication.mixins import AuthMixin +from common.api import JMSModelViewSet from .fido import register_begin, register_complete, auth_begin, auth_complete from .models import Passkey from .serializer import PasskeySerializer +from ...const import ConfirmType +from ...permissions import UserConfirmation from ...views import FlashMessageMixin -class PasskeyViewSet(AuthMixin, FlashMessageMixin, ModelViewSet): +class PasskeyViewSet(AuthMixin, FlashMessageMixin, JMSModelViewSet): serializer_class = PasskeySerializer permission_classes = (IsAuthenticated,) + def get_permissions(self): + if self.is_swagger_request(): + return super().get_permissions() + if self.action == 'register': + self.permission_classes = [ + IsAuthenticated, UserConfirmation.require(ConfirmType.PASSWORD) + ] + return super().get_permissions() + def get_queryset(self): return Passkey.objects.filter(user=self.request.user) diff --git a/apps/authentication/confirm/relogin.py b/apps/authentication/confirm/relogin.py index 2b27ef5fe..51e5d33c6 100644 --- a/apps/authentication/confirm/relogin.py +++ b/apps/authentication/confirm/relogin.py @@ -15,6 +15,7 @@ class ConfirmReLogin(BaseConfirm): display_name = 'Re-Login' def check(self): + return True return not self.user.is_password_authenticate() def authenticate(self, secret_key, mfa_type): diff --git a/apps/authentication/const.py b/apps/authentication/const.py index 1e06a4d35..bef4841b6 100644 --- a/apps/authentication/const.py +++ b/apps/authentication/const.py @@ -19,6 +19,7 @@ class ConfirmType(TextChoices): def get_can_confirm_types(cls, confirm_type): start = cls.values.index(confirm_type) types = cls.values[start:] + types = [tp for tp in types if tp != 'password'] types.reverse() return types diff --git a/apps/authentication/serializers/confirm.py b/apps/authentication/serializers/confirm.py index 5d747c656..dc7c997a6 100644 --- a/apps/authentication/serializers/confirm.py +++ b/apps/authentication/serializers/confirm.py @@ -7,4 +7,4 @@ from ..const import ConfirmType, MFAType class ConfirmSerializer(serializers.Serializer): 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(allow_blank=True) + secret_key = EncryptedField(allow_blank=True, required=False) diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index 6d40f68e8..e7e561ffd 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -13,6 +13,7 @@ router.register('sso', api.SSOViewSet, 'sso') router.register('temp-tokens', api.TempTokenViewSet, 'temp-token') router.register('connection-token', api.ConnectionTokenViewSet, 'connection-token') router.register('super-connection-token', api.SuperConnectionTokenViewSet, 'super-connection-token') +router.register('confirm', api.UserConfirmationViewSet, 'confirm') urlpatterns = [ path('wecom/qr/unbind/', api.WeComQRUnBindForUserApi.as_view(), name='wecom-qr-unbind'), @@ -29,7 +30,6 @@ urlpatterns = [ name='feishu-event-subscription-callback'), path('auth/', api.TokenCreateApi.as_view(), name='user-auth'), - path('confirm/', api.ConfirmApi.as_view(), name='user-confirm'), path('confirm-oauth/', api.ConfirmBindORUNBindOAuth.as_view(), name='confirm-oauth'), path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'), path('mfa/verify/', api.MFAChallengeVerifyApi.as_view(), name='mfa-verify'), diff --git a/apps/common/exceptions.py b/apps/common/exceptions.py index a8dfcafbd..0c22d6723 100644 --- a/apps/common/exceptions.py +++ b/apps/common/exceptions.py @@ -42,8 +42,11 @@ class ReferencedByOthers(JMSException): class UserConfirmRequired(JMSException): + status_code = status.HTTP_412_PRECONDITION_FAILED + def __init__(self, code=None): detail = { + 'type': 'user_confirm_required', 'code': code, 'detail': _('This action require confirm current user') } From 1ca912373fadbd5c80635156a15b53ff5bb654dc Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 13 Oct 2023 14:40:40 +0800 Subject: [PATCH 2/5] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=A1=AE=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/confirm.py | 17 +++++++++++++---- apps/authentication/backends/passkey/api.py | 15 +++++++++++++-- apps/authentication/serializers/confirm.py | 2 +- apps/authentication/urls/api_urls.py | 2 +- apps/common/exceptions.py | 3 +++ 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/apps/authentication/api/confirm.py b/apps/authentication/api/confirm.py index 4bd8d2c72..2bb371e48 100644 --- a/apps/authentication/api/confirm.py +++ b/apps/authentication/api/confirm.py @@ -4,10 +4,12 @@ import time from django.utils.translation import gettext_lazy as _ from rest_framework import status -from rest_framework.generics import RetrieveAPIView, CreateAPIView +from rest_framework.decorators import action +from rest_framework.generics import RetrieveAPIView from rest_framework.response import Response from authentication.permissions import UserConfirmation +from common.api import JMSGenericViewSet from common.permissions import IsValidUser from ..const import ConfirmType from ..serializers import ConfirmSerializer @@ -20,10 +22,17 @@ class ConfirmBindORUNBindOAuth(RetrieveAPIView): return Response('ok') -class ConfirmApi(RetrieveAPIView, CreateAPIView): +class UserConfirmationViewSet(JMSGenericViewSet): permission_classes = (IsValidUser,) serializer_class = ConfirmSerializer + @action(methods=['get'], detail=False) + def check(self, request): + confirm_type = request.query_params.get('confirm_type', 'password') + permission = UserConfirmation.require(confirm_type)() + permission.has_permission(request, self) + return Response('ok') + def get_confirm_backend(self, confirm_type): backend_classes = ConfirmType.get_prop_backends(confirm_type) if not backend_classes: @@ -34,12 +43,12 @@ class ConfirmApi(RetrieveAPIView, CreateAPIView): continue return backend - def retrieve(self, request, *args, **kwargs): + def list(self, request, *args, **kwargs): confirm_type = request.query_params.get('confirm_type', 'password') 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) + return Response(data={'error': msg}, status=status.HTTP_400_BAD_REQUEST) data = { 'confirm_type': backend.name, diff --git a/apps/authentication/backends/passkey/api.py b/apps/authentication/backends/passkey/api.py index 644636dd2..ace882a92 100644 --- a/apps/authentication/backends/passkey/api.py +++ b/apps/authentication/backends/passkey/api.py @@ -4,19 +4,30 @@ from django.shortcuts import render from django.utils.translation import gettext as _ from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated, AllowAny -from rest_framework.viewsets import ModelViewSet from authentication.mixins import AuthMixin +from common.api import JMSModelViewSet from .fido import register_begin, register_complete, auth_begin, auth_complete from .models import Passkey from .serializer import PasskeySerializer +from ...const import ConfirmType +from ...permissions import UserConfirmation from ...views import FlashMessageMixin -class PasskeyViewSet(AuthMixin, FlashMessageMixin, ModelViewSet): +class PasskeyViewSet(AuthMixin, FlashMessageMixin, JMSModelViewSet): serializer_class = PasskeySerializer permission_classes = (IsAuthenticated,) + def get_permissions(self): + if self.is_swagger_request(): + return super().get_permissions() + if self.action == 'register': + self.permission_classes = [ + IsAuthenticated, UserConfirmation.require(ConfirmType.PASSWORD) + ] + return super().get_permissions() + def get_queryset(self): return Passkey.objects.filter(user=self.request.user) diff --git a/apps/authentication/serializers/confirm.py b/apps/authentication/serializers/confirm.py index 5d747c656..dc7c997a6 100644 --- a/apps/authentication/serializers/confirm.py +++ b/apps/authentication/serializers/confirm.py @@ -7,4 +7,4 @@ from ..const import ConfirmType, MFAType class ConfirmSerializer(serializers.Serializer): 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(allow_blank=True) + secret_key = EncryptedField(allow_blank=True, required=False) diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index 6d40f68e8..e7e561ffd 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -13,6 +13,7 @@ router.register('sso', api.SSOViewSet, 'sso') router.register('temp-tokens', api.TempTokenViewSet, 'temp-token') router.register('connection-token', api.ConnectionTokenViewSet, 'connection-token') router.register('super-connection-token', api.SuperConnectionTokenViewSet, 'super-connection-token') +router.register('confirm', api.UserConfirmationViewSet, 'confirm') urlpatterns = [ path('wecom/qr/unbind/', api.WeComQRUnBindForUserApi.as_view(), name='wecom-qr-unbind'), @@ -29,7 +30,6 @@ urlpatterns = [ name='feishu-event-subscription-callback'), path('auth/', api.TokenCreateApi.as_view(), name='user-auth'), - path('confirm/', api.ConfirmApi.as_view(), name='user-confirm'), path('confirm-oauth/', api.ConfirmBindORUNBindOAuth.as_view(), name='confirm-oauth'), path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'), path('mfa/verify/', api.MFAChallengeVerifyApi.as_view(), name='mfa-verify'), diff --git a/apps/common/exceptions.py b/apps/common/exceptions.py index a8dfcafbd..0c22d6723 100644 --- a/apps/common/exceptions.py +++ b/apps/common/exceptions.py @@ -42,8 +42,11 @@ class ReferencedByOthers(JMSException): class UserConfirmRequired(JMSException): + status_code = status.HTTP_412_PRECONDITION_FAILED + def __init__(self, code=None): detail = { + 'type': 'user_confirm_required', 'code': code, 'detail': _('This action require confirm current user') } From 1daf1acaf327fd2f7b1f77174a07a91987a6d134 Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 13 Oct 2023 16:31:05 +0800 Subject: [PATCH 3/5] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=20access=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/access_key.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/authentication/api/access_key.py b/apps/authentication/api/access_key.py index 1ce992629..027fdf75f 100644 --- a/apps/authentication/api/access_key.py +++ b/apps/authentication/api/access_key.py @@ -7,6 +7,8 @@ from rest_framework.response import Response from common.api import JMSModelViewSet from rbac.permissions import RBACPermission +from ..const import ConfirmType +from ..permissions import UserConfirmation from ..serializers import AccessKeySerializer, AccessKeyCreateSerializer @@ -27,20 +29,20 @@ class AccessKeyViewSet(JMSModelViewSet): if self.action == 'create': self.permission_classes = [ - RBACPermission, + RBACPermission, UserConfirmation.require(ConfirmType.PASSWORD) ] return super().get_permissions() - def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - key = self.perform_create(serializer) - serializer = self.get_serializer(instance=key) - return Response(serializer.data, status=201) - def perform_create(self, serializer): user = self.request.user if user.access_keys.count() >= 10: raise serializers.ValidationError(_('Access keys can be created at most 10')) key = user.create_access_key() return key + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + key = self.perform_create(serializer) + serializer = self.get_serializer(instance=key) + return Response(serializer.data, status=201) From d6b450f32a76f9c2254e12f92c954eecba382aff Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 13 Oct 2023 16:33:25 +0800 Subject: [PATCH 4/5] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=20ak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/confirm/relogin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/authentication/confirm/relogin.py b/apps/authentication/confirm/relogin.py index 51e5d33c6..2b27ef5fe 100644 --- a/apps/authentication/confirm/relogin.py +++ b/apps/authentication/confirm/relogin.py @@ -15,7 +15,6 @@ class ConfirmReLogin(BaseConfirm): display_name = 'Re-Login' def check(self): - return True return not self.user.is_password_authenticate() def authenticate(self, secret_key, mfa_type): From d7ac08f6d9c7e29bb4cfa9ddef9826f7f8e18ec6 Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 13 Oct 2023 16:36:23 +0800 Subject: [PATCH 5/5] =?UTF-8?q?perf:=20=E5=8E=BB=E6=8E=89=20debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/const.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/authentication/const.py b/apps/authentication/const.py index bef4841b6..1e06a4d35 100644 --- a/apps/authentication/const.py +++ b/apps/authentication/const.py @@ -19,7 +19,6 @@ class ConfirmType(TextChoices): def get_can_confirm_types(cls, confirm_type): start = cls.values.index(confirm_type) types = cls.values[start:] - types = [tp for tp in types if tp != 'password'] types.reverse() return types