diff --git a/apps/chat/api/chat_authentication_api.py b/apps/chat/api/chat_authentication_api.py index 6f6b1b1a835..86dcde5e829 100644 --- a/apps/chat/api/chat_authentication_api.py +++ b/apps/chat/api/chat_authentication_api.py @@ -25,7 +25,7 @@ def get_request(): class ChatAuthenticationAPI(APIMixin): @staticmethod def get_request(): - return AnonymousAuthenticationSerializer + return None @staticmethod def get_parameters(): diff --git a/apps/chat/serializers/chat_authentication.py b/apps/chat/serializers/chat_authentication.py index a87f0976d86..9ebe7b2a1cb 100644 --- a/apps/chat/serializers/chat_authentication.py +++ b/apps/chat/serializers/chat_authentication.py @@ -13,9 +13,10 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from application.models import ApplicationAccessToken, ChatUserType, Application, ApplicationVersion +from application.models import ApplicationAccessToken, Application, ApplicationVersion from application.serializers.application import ApplicationSerializerModel -from common.auth.common import ChatUserToken, ChatAuthentication, FileToken +from common.auth.common import FileToken, ChatToken +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 @@ -24,9 +25,7 @@ class AnonymousAuthenticationSerializer(serializers.Serializer): - access_token = serializers.CharField(required=True, label=_("access_token")) - - def auth(self, request, with_valid=True): + def auth(self, request): token = request.META.get('HTTP_AUTHORIZATION') token_details = {} try: @@ -35,21 +34,11 @@ def auth(self, request, with_valid=True): token_details = signing.loads(token[7:]) except Exception as e: 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 not None and application_access_token.is_active: - chat_user_id = token_details.get('chat_user_id') or str(uuid.uuid7()) - _type = AuthenticationType.CHAT_ANONYMOUS_USER - return ChatUserToken(application_access_token.application_id, None, access_token, _type, - ChatUserType.ANONYMOUS_USER, - chat_user_id, ChatAuthentication(None)).to_token(), FileToken(chat_user_id, - AuthenticationType.CHAT_ANONYMOUS_USER.value, - application_id=str( - application_access_token.application_id)).to_token() - else: - raise NotFound404(404, _("Invalid access_token")) + chat_user_id = token_details.get('id') or str(uuid.uuid7()) + _type = AuthenticationType.CHAT_USER + return ChatToken(chat_user_id, _type, + str(Operate.ANNOTATION_AUTH)).to_token(), FileToken(chat_user_id, + _type).to_token() class AuthProfileSerializer(serializers.Serializer): diff --git a/apps/chat/views/chat.py b/apps/chat/views/chat.py index fa93ae03a5f..19e6c22f290 100644 --- a/apps/chat/views/chat.py +++ b/apps/chat/views/chat.py @@ -121,8 +121,7 @@ def options(self, request, *args, **kwargs): tags=[_('Chat')] # type: ignore ) def post(self, request: Request): - token, f_token = AnonymousAuthenticationSerializer( - data={'access_token': request.data.get("access_token")}).auth( + token, f_token = AnonymousAuthenticationSerializer().auth( request) response = result.success( token, @@ -369,7 +368,6 @@ def post(self, request: Request): return result.error(_("Failed to change password")) - class ChatUserProfileView(APIView): authentication_classes = [ChatTokenAuth] @@ -444,4 +442,3 @@ def post(self, request: Request): auth = request.META.get("HTTP_AUTHORIZATION") cache.delete(get_key(token=auth[7:]), version=version) return result.success(True) - diff --git a/apps/common/auth/common.py b/apps/common/auth/common.py index c64ceeb48a1..6f5f23fbc66 100644 --- a/apps/common/auth/common.py +++ b/apps/common/auth/common.py @@ -12,6 +12,8 @@ from django.core import signing, cache +from application.models import ChatUserType +from common.constants.authentication_type import AuthenticationType from common.constants.cache_version import Cache_Version from common.utils.rsa_util import encrypt, decrypt @@ -67,7 +69,7 @@ def to_dict(self): 'application_id': self.application_id } if self.application_id else { 'user_id': self.user_id, - 'type': self.type + 'type': str(self.type) }) def to_token(self): @@ -110,3 +112,20 @@ def new_instance(token_dict): token_dict.get('access_token'), token_dict.get('type'), token_dict.get('chat_user_type'), token_dict.get('chat_user_id'), ChatAuthentication.new_instance(token_dict.get('authentication'))) + + +class ChatToken: + def __init__(self, user_id, _type: AuthenticationType, login_type: str): + self.user_id = user_id + self.type = _type + self.login_type = login_type + + def to_dict(self): + return { + 'user_id': str(self.user_id), + 'type': str(self.type.value), + 'login_type': str(self.login_type) + } + + def to_token(self): + return signing.dumps(self.to_dict()) diff --git a/apps/common/auth/constants/chat_permission_constants.py b/apps/common/auth/constants/chat_permission_constants.py new file mode 100644 index 00000000000..75245522f61 --- /dev/null +++ b/apps/common/auth/constants/chat_permission_constants.py @@ -0,0 +1,40 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎虎 + @file: chat_permission_constants.py + @date:2026/8/6 16:38 + @desc: +""" +from enum import Enum + +from common.auth.constants.group_constants import Group +from common.auth.constants.operate_constants import Operate +from common.auth.struct.permission import Permission + + +class ChatPermissionConstants(Enum): + CHAT_USER_ANONYMOUS = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.ANNOTATION_AUTH, 0) + CHAT_USER_PASSWORD = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.PASSWORD, 1) + CHAT_USER_LOCAL = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.LOCAL, 2) + CHAT_USER_CAS = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.CAS, 3) + CHAT_USER_DINGTALK = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.DINGTALK, 4) + CHAT_USER_WECOM = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.WECOM, 5) + CHAT_USER_LARK = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.LARK, 6) + CHAT_USER_OIDC = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.OIDC, 7) + CHAT_USER_LDAP = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.LDAP, 8) + CHAT_USER_OAUTH2 = Permission(Group.CHAT_USER, Group.CHAT_USER, Operate.OAUTH2, 9) + + def get_permission(self): + return self._build_workspace_permission('application_id') + + def _build_workspace_permission(self, resource_id_key=None): + def permission_factory(_, kwargs): + return Permission(group=self.value.group, + sub_group=self.value.sub_group, + operate=self.value.operate, + bit_index=self.value.bit_index, + workspace_id=kwargs.get('workspace_id'), + resource_id=kwargs.get(resource_id_key) if resource_id_key else None) + + return permission_factory diff --git a/apps/common/auth/constants/group_constants.py b/apps/common/auth/constants/group_constants.py index cf859fdf436..beaeb226d9a 100644 --- a/apps/common/auth/constants/group_constants.py +++ b/apps/common/auth/constants/group_constants.py @@ -116,6 +116,7 @@ class Group(Enum): SYSTEM_OVERVIEW = ("SYSTEM_OVERVIEW", _("Overview")) SYSTEM_ACCESS = ("SYSTEM_ACCESS", _("Application Access")) SYSTEM_CHAT_LOG = ("SYSTEM_CHAT_LOG", _("Conversation log")) + CHAT = ("CHAT", _("Chat")) def __init__(self, value, label): self._value_ = value diff --git a/apps/common/auth/constants/operate_constants.py b/apps/common/auth/constants/operate_constants.py index 6f3c63c5b65..835c5e86935 100644 --- a/apps/common/auth/constants/operate_constants.py +++ b/apps/common/auth/constants/operate_constants.py @@ -77,6 +77,20 @@ class Operate(Enum): SYSTEM_API_KEY = ("READ+DELETE", _("System API Key")) PORTAL = ("READ+PORTAL", _("Portal")) + ANNOTATION_AUTH = ('ANNOTATION', _("Annotation")) + PASSWORD = ("PASSWORD", _("Password verification")) + LOCAL = ("LOCAL", _("Account login")) + CAS = ("CAS", _("CAS")) + DINGTALK = ("DINGTALK", _("dingtalk")) + WECOM = ("WECOM", _("WeCom")) + LARK = ("LARK", _("lark")) + OIDC = ("OIDC", _("OIDC")) + LDAP = ("LDAP", _("LDAP")) + OAUTH2 = ("OAUTH2", _("OAUTH2")) + def __init__(self, value, label): self._value_ = value self.label = label + + def __str__(self): + return self.value diff --git a/apps/common/auth/handle/impl/chat_anonymous_user_token.py b/apps/common/auth/handle/impl/chat_anonymous_user_token.py deleted file mode 100644 index 9810f9ebae4..00000000000 --- a/apps/common/auth/handle/impl/chat_anonymous_user_token.py +++ /dev/null @@ -1,49 +0,0 @@ -# coding=utf-8 -""" - @project: MaxKB - @Author:虎虎 - @file: chat_anonymous_user_token.py - @date:2025/6/6 15:08 - @desc: -""" -from django.db.models import QuerySet -from django.utils.translation import gettext_lazy as _ - -from application.models import ApplicationAccessToken -from common.auth.common import ChatUserToken -from common.auth.handle.auth_base_handle import AuthBaseHandle -from common.auth.struct.auth import Principal, Auth -from common.constants.authentication_type import AuthenticationType, ChatUserType -from common.exception.app_exception import AppAuthenticationFailed -from maxkb.settings import edition - - -class ChatAnonymousUserToken(AuthBaseHandle): - def support(self, request, token: str, get_token_details): - token_details = get_token_details() - if token_details is None: - return False - return ( - 'application_id' in token_details and - 'access_token' in token_details and - token_details.get('type') == AuthenticationType.CHAT_ANONYMOUS_USER.value) - - def handle(self, request, token: str, get_token_details): - auth_details = get_token_details() - chat_user_token = ChatUserToken.new_instance(auth_details) - application_id = chat_user_token.application_id - access_token = chat_user_token.access_token - application_access_token = QuerySet(ApplicationAccessToken).filter( - application_id=application_id).first() - if application_access_token is None: - raise AppAuthenticationFailed(1002, _('Authentication information is incorrect')) - if not application_access_token.is_active: - raise AppAuthenticationFailed(1002, _('Authentication information is incorrect')) - if not application_access_token.access_token == access_token: - raise AppAuthenticationFailed(1002, _('Authentication information is incorrect')) - if application_access_token.authentication and ['PE', 'EE'].__contains__(edition): - if chat_user_token.authentication.auth_type != application_access_token.authentication_value.get('type', - ''): - raise AppAuthenticationFailed(1002, _('Authentication information is incorrect')) - - return Principal(chat_user_token.chat_user_id, ChatUserType.ANONYMOUS_USER), Auth(set(), {}) diff --git a/apps/common/auth/handle/impl/chat_user_token.py b/apps/common/auth/handle/impl/chat_user_token.py new file mode 100644 index 00000000000..f48eef2e442 --- /dev/null +++ b/apps/common/auth/handle/impl/chat_user_token.py @@ -0,0 +1,77 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: chat_anonymous_user_token.py + @date:2025/6/6 15:08 + @desc: +""" +from functools import reduce + +from django.db.models import QuerySet, Q + +from application.models import ApplicationAccessToken, ChatUserType +from common.auth.constants.chat_permission_constants import ChatPermissionConstants +from common.auth.constants.group_constants import Group +from common.auth.constants.operate_constants import Operate +from common.auth.constants.permission_constants import PERMISSION_STR_MAP +from common.auth.handle.auth_base_handle import AuthBaseHandle +from common.auth.struct.auth import Principal, Auth +from common.constants.authentication_type import AuthenticationType +from system_manage.models import ResourceChatUserGroupAuthorize, ResourceType, ResourceChatUserAuthorize + + +class ChatUserToken(AuthBaseHandle): + def support(self, request, token: str, get_token_details): + token_details = get_token_details() + if token_details is None: + return False + return token_details.get('type') == AuthenticationType.CHAT_USER.value + + def handle(self, request, token: str, get_token_details): + auth_details = get_token_details() + application_access_token_list = QuerySet(ApplicationAccessToken).filter( + is_active=True + ) + _type = ChatUserType.ANONYMOUS_USER + login_type = auth_details.get('login_type') + if login_type.upper() == str(Operate.ANNOTATION_AUTH): + application_access_token_list = application_access_token_list.filter(authentication=False) + elif login_type.upper() == str(Operate.PASSWORD): + application_access_token_list = (application_access_token_list + .filter(authentication=True, authentication_value__type='password')) + else: + _type = ChatUserType.CHAT_USER + group_qs = QuerySet(ResourceChatUserGroupAuthorize).filter( + resource_type=ResourceType.APPLICATION, + is_auth=True, + ).values_list('resource_id', flat=True) + + user_qs = QuerySet(ResourceChatUserAuthorize).filter( + resource_type=ResourceType.APPLICATION, + is_auth=True, + ).values_list('resource_id', flat=True) + application_access_token_list = application_access_token_list.filter( + Q(authentication_value__type='login'), + Q(authentication_value__login_value__contains=login_type), + Q(application_id__in=group_qs) | Q(application_id__in=user_qs), + ) + permissions = {} + for application_access_token in application_access_token_list: + permission_list = [] + if application_access_token.authentication: + if application_access_token.get('type') == 'password': + permission_list.append(ChatPermissionConstants.CHAT_USER_PASSWORD.value) + elif application_access_token.get('type') == 'login': + login_value = application_access_token.get('login_value') or [] + for _value in login_value: + permission_str = f'{Group.CHAT_USER}_{_value.upper()}' + permission = PERMISSION_STR_MAP.get(permission_str) + if permission: + permission_list.append(permission) + + else: + permission_list.append(ChatPermissionConstants.CHAT_USER_ANONYMOUS.value) + k = f"{Group.CHAT_USER}:r:{application_access_token.application_id}" + permissions[k] = reduce(lambda x, y: x | y, [p.value.bit() for p in permission_list], 0) + return Principal(auth_details.get('user_id'), _type), Auth(set(), permissions) diff --git a/apps/common/auth/struct/auth.py b/apps/common/auth/struct/auth.py index 03c2f99c86d..b542a33f748 100644 --- a/apps/common/auth/struct/auth.py +++ b/apps/common/auth/struct/auth.py @@ -31,7 +31,11 @@ def __init__(self, class Principal: - def __init__(self, _id, _type: UserType | ChatUserType, profile=None): + def __init__(self, _id, + _type: ChatUserType | UserType, + profile=None, + **keywords): self.id = _id - self.type = _type + self._type = _type self.profile = profile + self.keywords = keywords diff --git a/apps/common/auth/struct/permission.py b/apps/common/auth/struct/permission.py index fde61a08cea..39a66a43670 100644 --- a/apps/common/auth/struct/permission.py +++ b/apps/common/auth/struct/permission.py @@ -21,8 +21,8 @@ class Permission: """ 权限信息 """ - group: Group - sub_group: Group + group: Group | str + sub_group: Group | str operate: Operate bit_index: int workspace_id: Optional[str] = None @@ -33,7 +33,9 @@ def bit(self): return 1 << self.bit_index def get_resource_permission_key(self, resource_id): - return f"{self.group}:{resource_id}" + workspace = f"w:{self.workspace_id}" if self.workspace_id else "" + resource = f"r:{self.resource_id}" if self.resource_id else "" + return f"{self.group}{workspace}{resource}" def __str__(self): sub = f"_{self.sub_group}" if self.sub_group != self.group else "" diff --git a/apps/maxkb/settings/auth/web.py b/apps/maxkb/settings/auth/web.py index e7936ef2378..497bd1bc979 100644 --- a/apps/maxkb/settings/auth/web.py +++ b/apps/maxkb/settings/auth/web.py @@ -7,7 +7,7 @@ @desc: """ USER_TOKEN_AUTH = 'common.auth.handle.impl.user_token.UserToken' -CHAT_ANONYMOUS_USER_AURH = 'common.auth.handle.impl.chat_anonymous_user_token.ChatAnonymousUserToken' +CHAT_ANONYMOUS_USER_AURH = 'common.auth.handle.impl.chat_user_token.ChatUserToken' APPLICATION_KEY_AUTH = 'common.auth.handle.impl.application_key.ApplicationKey' AUTH_HANDLES = [ USER_TOKEN_AUTH