diff --git a/apps/authentication/api/__init__.py b/apps/authentication/api/__init__.py index 53d59c9a6..1a57acc8e 100644 --- a/apps/authentication/api/__init__.py +++ b/apps/authentication/api/__init__.py @@ -15,3 +15,4 @@ from .ssh_key import * from .sso import * from .temp_token import * from .token import * +from .face import * diff --git a/apps/authentication/api/face.py b/apps/authentication/api/face.py new file mode 100644 index 000000000..108816afb --- /dev/null +++ b/apps/authentication/api/face.py @@ -0,0 +1,115 @@ +from django.core.cache import cache +from rest_framework.generics import CreateAPIView, RetrieveAPIView +from rest_framework.response import Response +from rest_framework.serializers import ValidationError +from rest_framework.permissions import AllowAny +from rest_framework.exceptions import NotFound + +from common.permissions import IsServiceAccount +from common.utils import get_logger +from orgs.utils import tmp_to_root_org + +from .. import serializers +from ..mixins import AuthMixin +from ..const import FACE_CONTEXT_CACHE_KEY_PREFIX, FACE_SESSION_KEY, FACE_CONTEXT_CACHE_TTL +from ..models import ConnectionToken + +logger = get_logger(__name__) + +__all__ = [ + 'FaceCallbackApi', 'FaceContextApi' +] + + +class FaceCallbackApi(AuthMixin, CreateAPIView): + permission_classes = (IsServiceAccount,) + serializer_class = serializers.FaceCallbackSerializer + + def perform_create(self, serializer): + token = serializer.validated_data.get('token') + context = self._get_context_from_cache(token) + + if not serializer.validated_data.get('success', False): + self._update_context_with_error( + context, + serializer.validated_data.get('error_message', 'Unknown error') + ) + return Response(status=200) + + face_code = serializer.validated_data.get('face_code') + if not face_code: + self._update_context_with_error(context, "missing field 'face_code'") + raise ValidationError({'error': "missing field 'face_code'"}) + try: + self._handle_success(context, face_code) + except Exception as e: + self._update_context_with_error(context, str(e)) + return Response(status=200) + + @staticmethod + def get_face_cache_key(token): + return f"{FACE_CONTEXT_CACHE_KEY_PREFIX}_{token}" + + def _get_context_from_cache(self, token): + cache_key = self.get_face_cache_key(token) + context = cache.get(cache_key) + if not context: + raise ValidationError({'error': "token not exists or expired"}) + return context + + def _update_context_with_error(self, context, error_message): + context.update({ + 'is_finished': True, + 'success': False, + 'error_message': error_message, + }) + self._update_cache(context) + + def _update_cache(self, context): + cache_key = self.get_face_cache_key(context['token']) + cache.set(cache_key, context, FACE_CONTEXT_CACHE_TTL) + + def _handle_success(self, context, face_code): + context.update({ + 'is_finished': True, + 'success': True, + 'face_code': face_code + }) + action = context.get('action', None) + if action == 'login_asset': + with tmp_to_root_org(): + connection_token_id = context.get('connection_token_id') + token = ConnectionToken.objects.filter(id=connection_token_id).first() + token.is_active = True + token.save() + self._update_cache(context) + + +class FaceContextApi(AuthMixin, RetrieveAPIView, CreateAPIView): + permission_classes = (AllowAny,) + face_token_session_key = FACE_SESSION_KEY + + @staticmethod + def get_face_cache_key(token): + return f"{FACE_CONTEXT_CACHE_KEY_PREFIX}_{token}" + + def new_face_context(self): + return self.create_face_verify_context() + + def post(self, request, *args, **kwargs): + token = self.new_face_context() + return Response({'token': token}) + + def get(self, request, *args, **kwargs): + token = self.request.session.get(self.face_token_session_key) + + cache_key = self.get_face_cache_key(token) + context = cache.get(cache_key) + if not context: + raise NotFound({'error': "Token does not exist or has expired."}) + + return Response({ + "is_finished": context.get('is_finished', False), + "success": context.get('success', False), + "error_message": context.get("error_message", '') + }) diff --git a/apps/authentication/api/mfa.py b/apps/authentication/api/mfa.py index 0561753ad..4a22ad6a1 100644 --- a/apps/authentication/api/mfa.py +++ b/apps/authentication/api/mfa.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- # -import uuid - -from django.core.cache import cache from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ from rest_framework import exceptions @@ -10,120 +7,22 @@ from rest_framework.generics import CreateAPIView, RetrieveAPIView from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.serializers import ValidationError -from rest_framework.exceptions import NotFound from common.exceptions import JMSException, UnexpectError -from common.permissions import WithBootstrapToken, IsServiceAccount from common.utils import get_logger -from orgs.utils import tmp_to_root_org from users.models.user import User from .. import errors from .. import serializers -from ..const import FACE_CONTEXT_CACHE_KEY_PREFIX, FACE_SESSION_KEY, FACE_CONTEXT_CACHE_TTL from ..errors import SessionEmptyError from ..mixins import AuthMixin -from ..models import ConnectionToken logger = get_logger(__name__) __all__ = [ 'MFAChallengeVerifyApi', 'MFASendCodeApi', - 'MFAFaceCallbackApi', 'MFAFaceContextApi' ] -class MFAFaceCallbackApi(AuthMixin, CreateAPIView): - permission_classes = (IsServiceAccount,) - serializer_class = serializers.MFAFaceCallbackSerializer - - def perform_create(self, serializer): - token = serializer.validated_data.get('token') - context = self._get_context_from_cache(token) - - if not serializer.validated_data.get('success', False): - self._update_context_with_error( - context, - serializer.validated_data.get('error_message', 'Unknown error') - ) - return Response(status=200) - - face_code = serializer.validated_data.get('face_code') - if not face_code: - self._update_context_with_error(context, "missing field 'face_code'") - raise ValidationError({'error': "missing field 'face_code'"}) - try: - self._handle_success(context, face_code) - except Exception as e: - self._update_context_with_error(context, str(e)) - return Response(status=200) - - @staticmethod - def get_face_cache_key(token): - return f"{FACE_CONTEXT_CACHE_KEY_PREFIX}_{token}" - - def _get_context_from_cache(self, token): - cache_key = self.get_face_cache_key(token) - context = cache.get(cache_key) - if not context: - raise ValidationError({'error': "token not exists or expired"}) - return context - - def _update_context_with_error(self, context, error_message): - context.update({ - 'is_finished': True, - 'success': False, - 'error_message': error_message, - }) - self._update_cache(context) - - def _update_cache(self, context): - cache_key = self.get_face_cache_key(context['token']) - cache.set(cache_key, context, FACE_CONTEXT_CACHE_TTL) - - def _handle_success(self, context, face_code): - context.update({ - 'is_finished': True, - 'success': True, - 'face_code': face_code - }) - action = context.get('action', None) - if action == 'login_asset': - with tmp_to_root_org(): - connection_token_id = context.get('connection_token_id') - token = ConnectionToken.objects.filter(id=connection_token_id).first() - token.is_active = True - token.save() - self._update_cache(context) - - -class MFAFaceContextApi(AuthMixin, RetrieveAPIView, CreateAPIView): - permission_classes = (AllowAny,) - face_token_session_key = FACE_SESSION_KEY - - @staticmethod - def get_face_cache_key(token): - return f"{FACE_CONTEXT_CACHE_KEY_PREFIX}_{token}" - - def new_face_context(self): - return self.create_face_verify_context() - - def post(self, request, *args, **kwargs): - token = self.new_face_context() - return Response({'token': token}) - - def get(self, request, *args, **kwargs): - token = self.request.session.get(self.face_token_session_key) - - cache_key = self.get_face_cache_key(token) - context = cache.get(cache_key) - if not context: - raise NotFound({'error': "Token does not exist or has expired."}) - - return Response({ - "is_finished": context.get('is_finished', False), - "success": context.get('success', False), - "error_message": context.get("error_message", '') - }) # MFASelectAPi 原来的名字 diff --git a/apps/authentication/serializers/__init__.py b/apps/authentication/serializers/__init__.py index 9614b3efb..99dfabf57 100644 --- a/apps/authentication/serializers/__init__.py +++ b/apps/authentication/serializers/__init__.py @@ -4,3 +4,4 @@ from .connection_token import * from .password_mfa import * from .ssh_key import * from .token import * +from .face import * diff --git a/apps/authentication/serializers/face.py b/apps/authentication/serializers/face.py new file mode 100644 index 000000000..52094a5cc --- /dev/null +++ b/apps/authentication/serializers/face.py @@ -0,0 +1,18 @@ +from rest_framework import serializers + +__all__ = [ + 'FaceCallbackSerializer' +] + + +class FaceCallbackSerializer(serializers.Serializer): + token = serializers.CharField(required=True, allow_blank=False) + success = serializers.BooleanField(required=True, allow_null=False) + error_message = serializers.CharField(required=False, allow_null=True, allow_blank=True) + face_code = serializers.CharField(required=False, allow_null=True, allow_blank=True) + + def update(self, instance, validated_data): + pass + + def create(self, validated_data): + pass diff --git a/apps/authentication/serializers/password_mfa.py b/apps/authentication/serializers/password_mfa.py index 826fdfdcc..3e733b6ec 100644 --- a/apps/authentication/serializers/password_mfa.py +++ b/apps/authentication/serializers/password_mfa.py @@ -8,7 +8,6 @@ from common.serializers.fields import EncryptedField __all__ = [ 'MFAChallengeSerializer', 'MFASelectTypeSerializer', 'PasswordVerifySerializer', 'ResetPasswordCodeSerializer', - 'MFAFaceCallbackSerializer' ] @@ -52,16 +51,3 @@ class MFAChallengeSerializer(serializers.Serializer): def update(self, instance, validated_data): pass - - -class MFAFaceCallbackSerializer(serializers.Serializer): - token = serializers.CharField(required=True, allow_blank=False) - success = serializers.BooleanField(required=True, allow_null=False) - error_message = serializers.CharField(required=False, allow_null=True, allow_blank=True) - face_code = serializers.CharField(required=False, allow_null=True, allow_blank=True) - - def update(self, instance, validated_data): - pass - - def create(self, validated_data): - pass diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index 70d0f2796..cb545c626 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -26,6 +26,9 @@ urlpatterns = [ path('lark/event/subscription/callback/', api.LarkEventSubscriptionCallback.as_view(), name='lark-event-subscription-callback'), + path('face/callback/', api.FaceCallbackApi.as_view(), name='mfa-face-callback'), + path('face/context/', api.FaceContextApi.as_view(), name='mfa-face-context'), + path('auth/', api.TokenCreateApi.as_view(), name='user-auth'), path('confirm-oauth/', api.ConfirmBindORUNBindOAuth.as_view(), name='confirm-oauth'), path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'), @@ -33,8 +36,6 @@ urlpatterns = [ 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-code'), - path('mfa/face/callback/', api.MFAFaceCallbackApi.as_view(), name='mfa-face-callback'), - path('mfa/face/context/', api.MFAFaceContextApi.as_view(), name='mfa-face-context'), path('password/reset-code/', api.UserResetPasswordSendCodeApi.as_view(), name='reset-password-code'), 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'),