Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
104 changes: 41 additions & 63 deletions apps/chat/serializers/chat_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,21 @@
from rest_framework import serializers

from application.models import ApplicationAccessToken, Application, ApplicationVersion
from common.auth.common import ChatToken
from common.auth.common import ChatToken, parse_token
from common.auth.constants.operate_constants import Operate
from common.constants.authentication_type import AuthenticationType
from common.constants.cache_version import Cache_Version
from common.database_model_manage.database_model_manage import DatabaseModelManage
from common.exception.app_exception import NotFound404, AppUnauthorizedFailed, AppApiException
from common.utils.rsa_util import get_key_pair_by_sql
from portal.models import Portal


class AnonymousAuthenticationSerializer(serializers.Serializer):
"""v3 匿名认证:application_id 为可选 query 参数。
传入时颁发应用级令牌,未传入时颁发全局令牌。"""

application_id = serializers.UUIDField(required=False, label=_("application_id"))
access_token = serializers.CharField(required=False, label=_("access_token"))

def auth(self, request):
self.is_valid(raise_exception=True)
token = request.META.get("HTTP_AUTHORIZATION")
token_details = {}
try:
Expand All @@ -42,60 +41,29 @@ def auth(self, request):
chat_user_id = token_details.get("id") or str(uuid.uuid7())
_type = AuthenticationType.CHAT_USER

application_id = self.validated_data.get("application_id")
if application_id:
application_access_token = QuerySet(ApplicationAccessToken).filter(application_id=application_id).first()
access_token = self.data.get("access_token")
if access_token:
application_access_token = QuerySet(ApplicationAccessToken).filter(access_token=access_token).first()
if application_access_token is None or not application_access_token.is_active:
raise AppApiException(500, _("Invalid application_id"))
application_id = str(application_id)
application_id = str(application_access_token.application_id)
return ChatToken(
chat_user_id, _type, str(Operate.ANNOTATION_AUTH), application_id=application_id
).to_token()
return (ChatToken(chat_user_id, _type, str(Operate.ANNOTATION_AUTH)).to_token(),)
return ChatToken(chat_user_id, _type, str(Operate.ANNOTATION_AUTH)).to_token()


class AnonymousAuthenticationV2Serializer(serializers.Serializer):
"""v2 匿名认证:application_id 不在 path,从 access_token 解出并写进 token,
供 ChatUserToken handler 收窄到该应用。"""

class ChatAuthProfileSerializer(serializers.Serializer):
access_token = serializers.CharField(required=True, label=_("access_token"))

def auth(self, request, with_valid=True):
token = request.META.get("HTTP_AUTHORIZATION")
token_details = {}
try:
# 校验token
if token is not None:
token_details = signing.loads(token[7:])
except Exception:
pass
if with_valid:
self.is_valid(raise_exception=True)
access_token = self.data.get("access_token")
application_access_token = QuerySet(ApplicationAccessToken).filter(access_token=access_token).first()
if application_access_token is None or not application_access_token.is_active:
raise NotFound404(404, _("Invalid access_token"))
chat_user_id = token_details.get("user_id") or token_details.get("id") or str(uuid.uuid7())
_type = AuthenticationType.CHAT_USER
application_id = str(application_access_token.application_id)
return ChatToken(
chat_user_id, _type, str(Operate.ANNOTATION_AUTH), application_id=application_id
).to_token(), FileToken(chat_user_id, _type, application_id=application_id).to_token()


class AuthProfileSerializer(serializers.Serializer):
"""v3: 直接通过 application_id 获取认证 profile"""

application_id = serializers.UUIDField(required=True, label=_("application_id"))

def profile(self):
self.is_valid(raise_exception=True)
application_id = self.validated_data.get("application_id")
application_access_token = QuerySet(ApplicationAccessToken).filter(application_id=application_id).first()
access_token = self.data.get("access_token")
application_access_token = QuerySet(ApplicationAccessToken).filter(access_token=access_token).first()
if application_access_token is None:
raise NotFound404(404, _("Invalid application_id"))
raise NotFound404(404, _("Invalid access_token"))
if not application_access_token.is_active:
raise NotFound404(404, _("Invalid application_id"))
raise NotFound404(404, _("Invalid access_token"))
login_value = application_access_token.authentication_value.get("login_value", [])
chat_platform = DatabaseModelManage.get_model("chat_platform")
if chat_platform is not None:
Expand All @@ -104,29 +72,39 @@ def profile(self):
if "LOCAL" in application_access_token.authentication_value.get("login_value", []):
login_value.insert(0, "LOCAL")
return {
"application_name": application_access_token.application.name,
"authentication": application_access_token.authentication,
"authentication_type": application_access_token.authentication_value.get("type", "password"),
"max_attempts": application_access_token.authentication_value.get("max_attempts", 1),
"enable_auth": application_access_token.authentication,
"login_value": login_value,
"rsaKey": get_key_pair_by_sql().get("key"),
"rsa_key": get_key_pair_by_sql().get("key"),
"meta": {
"application_name": application_access_token.application.name,
"application_id": str(application_access_token.application_id),
},
}


class AuthProfileV2Serializer(serializers.Serializer):
"""v2: 通过 access_token 查表得到 application_id,委托给 AuthProfileSerializer"""

access_token = serializers.CharField(required=True, label=_("access_token"))

def profile(self):
self.is_valid(raise_exception=True)
access_token = self.validated_data.get("access_token")
application_access_token = QuerySet(ApplicationAccessToken).filter(access_token=access_token).first()
if application_access_token is None:
raise NotFound404(404, _("Invalid access_token"))
if not application_access_token.is_active:
class PortalAuthProfileSerializer(serializers.Serializer):
@staticmethod
def profile():
portal = Portal.objects.first()
if portal is None:
return {
"max_attempts": 1,
"enable_auth": False,
"login_value": ["LOCAL"],
"rsa_key": get_key_pair_by_sql().get("key"),
"meta": {},
}
if not portal.enable_public_access:
raise NotFound404(404, _("Invalid access_token"))
return AuthProfileSerializer(data={"application_id": application_access_token.application_id}).profile()
auth_config = portal.auth_config
return {
"max_attempts": auth_config.get("max_attempts", 1),
"enable_auth": portal.enable_auth,
"login_value": auth_config.get("login_value", []),
"rsa_key": get_key_pair_by_sql().get("key"),
"meta": {},
}


class ApplicationProfileSerializer(serializers.Serializer):
Expand Down
4 changes: 3 additions & 1 deletion apps/chat/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
path('chat/<str:chat_id>/chat_record/<int:current_page>/<int:page_size>', v3_views.HistoricalConversationRecordView.PageView.as_view(), name='v3_historical_conversation_record_page'),
path('chat/<str:chat_id>/chat_record/<str:chat_record_id>',v3_views.ChatRecordView.as_view(),name='v3_conversation_details'),
path('chat/<str:chat_id>/chat_record/<str:chat_record_id>/vote',v3_views.VoteView.as_view(), name='v3_vote'),
path('chat/<str:chat_id>/chat_record/<str:chat_record_id>/resume_chat_message',v3_views.ResumeStreamView.as_view(), name='v3_resume_chat_message'),
path('chat/<str:chat_id>/share_chat',ChatRecordLinkView.as_view(),name='v3_share_chat'),
path('chat/<str:chat_id>',v3_views.HistoricalConversationView.Operate.as_view(), name='v3_historical_conversation_operate'),
])),
Expand All @@ -33,7 +34,8 @@
path('auth/anonymous', v3_views.AnonymousAuthentication.as_view()),
path('auth/login', v3_views.LocalLoginView.as_view()),
path('auth/logout', v3_views.Logout.as_view(), name='v3_logout'),
path('profile', v3_views.AuthProfile.as_view()),
path('profile', v3_views.ChatAuthProfile.as_view()),
path('portal/profile', v3_views.PortalAuthProfile.as_view()),
path('captcha', v3_views.CaptchaView.as_view(), name='v3_captcha'),
path('share/<str:link>', ChatRecordDetailView.as_view()),
path('chat_message/<str:chat_id>/cancel', v3_views.CancelWorkflowView.as_view(), name='v3_cancel_workflow'),
Expand Down
12 changes: 6 additions & 6 deletions apps/chat/views/v2/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
TextToSpeechSerializers,
)
from chat.serializers.chat_authentication import (
AnonymousAuthenticationV2Serializer,
AnonymousAuthenticationSerializer,
ApplicationProfileSerializer,
AuthProfileV2Serializer,
ChatAuthProfileSerializer,
)
from common.auth import ChatTokenAuth
from common.auth.authentication import has_permissions
Expand Down Expand Up @@ -137,12 +137,12 @@ def options(self, request, *args, **kwargs):
description=_("Application Anonymous Certification"),
summary=_("Application Anonymous Certification"),
operation_id=_("Application Anonymous Certification"), # type: ignore
request=AnonymousAuthenticationV2Serializer,
request=AnonymousAuthenticationSerializer,
responses=None,
tags=[_("Chat")], # type: ignore
)
def post(self, request: Request):
token, f_token = AnonymousAuthenticationV2Serializer(data=request.data).auth(request)
token = AnonymousAuthenticationSerializer(data=request.data).auth(request)
response = result.success(
token,
headers={
Expand All @@ -156,7 +156,7 @@ def post(self, request: Request):

response.set_cookie(
key="mk_file_auth",
value=f_token,
value=token,
max_age=7 * 24 * 3600,
path=f"{CONFIG.get_chat_path()}/{request.data.get('access_token')}",
secure=is_https,
Expand Down Expand Up @@ -196,7 +196,7 @@ class AuthProfile(APIView):
)
def get(self, request: Request):
return result.success(
AuthProfileV2Serializer(data={"access_token": request.query_params.get("access_token")}).profile()
ChatAuthProfileSerializer(data={"access_token": request.query_params.get("access_token")}).profile()
)


Expand Down
58 changes: 51 additions & 7 deletions apps/chat/views/v3/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,21 @@
from rest_framework.views import APIView

from application.api.application_api import SpeechToTextAPI, TextToSpeechAPI
from application.models import ChatUserType, ChatSourceChoices, ApplicationAccessToken
from application.models import ChatUserType, ChatSourceChoices, ApplicationAccessToken, ChatRecord
from chat.api.chat_api import ChatAPI
from chat.api.chat_authentication_api import ChatAuthenticationAPI, ChatAuthenticationProfileAPI, ChatOpenAPI, OpenAIAPI
from chat.serializers.chat import (
OpenAIChatSerializer,
ChatSerializers,
OpenChatSerializers,
ResumeSerializers,
SpeechToTextSerializers,
TextToSpeechSerializers,
)
from chat.serializers.chat_authentication import (
AnonymousAuthenticationSerializer,
ApplicationProfileSerializer,
AuthProfileSerializer,
ChatAuthProfileSerializer,
)
from common.auth import ChatTokenAuth
from common.auth.authentication import has_permissions
Expand Down Expand Up @@ -138,7 +139,7 @@ def options(self, request, *args, **kwargs):
tags=[_("V3 Chat")], # type: ignore
)
def post(self, request: Request):
serializer = AnonymousAuthenticationSerializer(data=request.query_params)
serializer = AnonymousAuthenticationSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
token = serializer.auth(request)
response = result.success(
Expand All @@ -152,8 +153,10 @@ def post(self, request: Request):
)
is_https = request.scheme == "https"

application_id = serializer.validated_data.get("application_id")
cookie_path = f"{CONFIG.get_chat_path()}/{application_id}" if application_id else CONFIG.get_chat_path()
access_token = request.data.get("access_token")
cookie_path = (
f"{CONFIG.get_chat_path()}/{access_token}/" if access_token else f"{CONFIG.get_chat_path()}/portal/"
)
response.set_cookie(
key="mk_file_auth",
value=token,
Expand Down Expand Up @@ -183,7 +186,7 @@ def get(self, request: Request, application_id: str):
return result.success(ApplicationProfileSerializer(data={"application_id": application_id}).profile())


class AuthProfile(APIView):
class ChatAuthProfile(APIView):
@extend_schema(
methods=["GET"],
description=_("Get application authentication information"),
Expand All @@ -195,7 +198,7 @@ class AuthProfile(APIView):
)
def get(self, request: Request):
return result.success(
AuthProfileSerializer(data={"application_id": request.query_params.get("application_id")}).profile()
ChatAuthProfileSerializer(data={"access_token": request.query_params.get("access_token")}).profile()
)


Expand Down Expand Up @@ -293,6 +296,47 @@ def post(self, request: Request, chat_id: str):
return result.fail(500, _("Failed to cancel workflow"))


class ResumeStreamView(APIView):
authentication_classes = [ChatTokenAuth]

@extend_schema(
methods=["POST"],
description=_("Resume stream for workflow"),
summary=_("Resume stream for workflow"),
operation_id=_("V3 Resume stream for workflow"), # type: ignore
parameters=[
OpenApiParameter(
name="application_id",
type=OpenApiTypes.UUID,
location=OpenApiParameter.PATH,
description=_("Application ID"),
),
OpenApiParameter(
name="chat_id", type=OpenApiTypes.UUID, location=OpenApiParameter.PATH, description=_("Chat ID")
),
OpenApiParameter(
name="chat_record_id",
type=OpenApiTypes.UUID,
location=OpenApiParameter.PATH,
description=_("Conversation record id"),
),
],
responses=None,
tags=[_("V3 Chat")], # type: ignore
)
@has_permissions(ChatPermissionConstants.get_aggregate_permissions())
def post(self, request: Request, application_id: str, chat_id: str, chat_record_id: str):
# 对话端只能续传当前对话用户在该应用下的会话记录
if not ChatRecord.objects.filter(
id=chat_record_id,
chat_id=chat_id,
chat__application_id=application_id,
chat__chat_user_id=str(request.user.id),
).exists():
raise AppApiException(500, _("Conversation does not exist"))
return ResumeSerializers(data={"chat_id": chat_id, "chat_record_id": chat_record_id}).resume(request)


class CaptchaView(APIView):
@extend_schema(
methods=["GET"],
Expand Down
15 changes: 15 additions & 0 deletions apps/chat/views/v3/portal.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from rest_framework.request import Request
from rest_framework.views import APIView

from chat.serializers.chat_authentication import PortalAuthProfileSerializer
from common import result
from common.auth import ChatTokenAuth
from common.utils.common import query_params_to_single_dict
Expand All @@ -23,6 +24,20 @@
)


class PortalAuthProfile(APIView):
@extend_schema(
methods=["GET"],
description=_("Get portal authentication information"),
summary=_("Get portal authentication information"),
operation_id=_("Get portal authentication information"), # type: ignore
parameters=None,
responses=None,
tags=[_("V3 Chat")], # type: ignore
)
def get(self, request: Request):
return result.success(PortalAuthProfileSerializer.profile())


class PortalApplicationView(APIView):
authentication_classes = [ChatTokenAuth]

Expand Down
Loading
Loading