From e8886d5c657d59a384e23c87b882b5fa8af1338d Mon Sep 17 00:00:00 2001 From: shaohuzhang1 <80892890+shaohuzhang1@users.noreply.github.com> Date: Mon, 9 Jun 2025 20:25:58 +0800 Subject: [PATCH] feat: application api key (#3224) --- apps/application/api/application_api_key.py | 42 ++++++++++-- .../serializers/application_api_key.py | 43 ++++++++++-- apps/application/urls.py | 2 + .../views/application_access_token.py | 4 +- apps/application/views/application_api_key.py | 54 +++++++++++---- .../cache_data/application_api_key_cache.py | 28 ++++++++ apps/common/constants/cache_version.py | 2 + apps/common/utils/cache_util.py | 66 +++++++++++++++++++ 8 files changed, 214 insertions(+), 27 deletions(-) create mode 100644 apps/common/cache_data/application_api_key_cache.py create mode 100644 apps/common/utils/cache_util.py diff --git a/apps/application/api/application_api_key.py b/apps/application/api/application_api_key.py index 6c5c6fa2a..8e4ee71bc 100644 --- a/apps/application/api/application_api_key.py +++ b/apps/application/api/application_api_key.py @@ -1,10 +1,22 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter +from application.serializers.application_api_key import EditApplicationKeySerializer, ApplicationKeySerializerModel from common.mixins.api_mixin import APIMixin +from common.result import ResultSerializer -class ApplicationKeyCreateAPI(APIMixin): +class ApplicationKeyListResult(ResultSerializer): + def get_data(self): + return ApplicationKeySerializerModel(many=True) + + +class ApplicationKeyResult(ResultSerializer): + def get_data(self): + return ApplicationKeySerializerModel() + + +class ApplicationKeyAPI(APIMixin): @staticmethod def get_parameters(): return [ @@ -24,10 +36,26 @@ class ApplicationKeyCreateAPI(APIMixin): ) ] - # class Operate(APIMixin): - # @staticmethod - # def s(): - # pass + @staticmethod + def get_response(): + return ApplicationKeyResult - # def get_response(): - # return ApplicationKeyCreateResponse + class List(APIMixin): + @staticmethod + def get_response(): + return ApplicationKeyListResult + + class Operate(APIMixin): + @staticmethod + def get_parameters(): + return [*ApplicationKeyAPI.get_parameters(), OpenApiParameter( + name="api_key_id", + description="ApiKeyId", + type=OpenApiTypes.STR, + location='path', + required=True, + )] + + @staticmethod + def get_request(): + return EditApplicationKeySerializer diff --git a/apps/application/serializers/application_api_key.py b/apps/application/serializers/application_api_key.py index 430785867..a430f2537 100644 --- a/apps/application/serializers/application_api_key.py +++ b/apps/application/serializers/application_api_key.py @@ -7,6 +7,7 @@ from rest_framework import serializers from application.models import Application from application.models.application_api_key import ApplicationApiKey +from common.cache_data.application_api_key_cache import get_application_api_key, del_application_api_key from common.exception.app_exception import AppApiException @@ -16,12 +17,19 @@ class ApplicationKeySerializerModel(serializers.ModelSerializer): fields = "__all__" -class Edit(serializers.Serializer): - pass +class EditApplicationKeySerializer(serializers.Serializer): + is_active = serializers.BooleanField(required=False, label=_("Availability")) + + allow_cross_domain = serializers.BooleanField(required=False, + label=_("Is cross-domain allowed")) + + cross_domain_list = serializers.ListSerializer(required=False, + child=serializers.CharField(required=True, + label=_("Cross-domain address")), + label=_("Cross-domain list")) class ApplicationKeySerializer(serializers.Serializer): - user_id = serializers.UUIDField(required=True, label=_('user id')) workspace_id = serializers.CharField(required=True, label=_('workspace id')) application_id = serializers.UUIDField(required=True, label=_('application id')) @@ -53,10 +61,37 @@ class ApplicationKeySerializer(serializers.Serializer): QuerySet(ApplicationApiKey).filter(application_id=application_id)] class Operate(serializers.Serializer): - user_id = serializers.UUIDField(required=True, label=_('user id')) workspace_id = serializers.CharField(required=True, label=_('workspace id')) application_id = serializers.UUIDField(required=True, label=_('application id')) + api_key_id = serializers.UUIDField(required=True, label=_('ApiKeyId')) + + def delete(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + api_key_id = self.data.get("api_key_id") + application_id = self.data.get('application_id') + application_api_key = QuerySet(ApplicationApiKey).filter(id=api_key_id, + application_id=application_id).first() + del_application_api_key(application_api_key.secret_key) + application_api_key.delete() def edit(self, instance, with_valid=True): if with_valid: self.is_valid(raise_exception=True) + EditApplicationKeySerializer(data=instance).is_valid(raise_exception=True) + api_key_id = self.data.get("api_key_id") + application_id = self.data.get('application_id') + application_api_key = QuerySet(ApplicationApiKey).filter(id=api_key_id, + application_id=application_id).first() + if application_api_key is None: + raise AppApiException(500, _('APIKey does not exist')) + if 'is_active' in instance and instance.get('is_active') is not None: + application_api_key.is_active = instance.get('is_active') + if 'allow_cross_domain' in instance and instance.get('allow_cross_domain') is not None: + application_api_key.allow_cross_domain = instance.get('allow_cross_domain') + if 'cross_domain_list' in instance and instance.get('cross_domain_list') is not None: + application_api_key.cross_domain_list = instance.get('cross_domain_list') + application_api_key.save() + # 写入缓存 + get_application_api_key(application_api_key.secret_key, False) + return True diff --git a/apps/application/urls.py b/apps/application/urls.py index 23d36bad9..4dc769896 100644 --- a/apps/application/urls.py +++ b/apps/application/urls.py @@ -13,6 +13,8 @@ urlpatterns = [ path('workspace//application/', views.Application.Operate.as_view()), path('workspace//application//application_key', views.ApplicationKey.as_view()), + path('workspace//application//application_key/', + views.ApplicationKey.Operate.as_view()), path('workspace//application//export', views.Application.Export.as_view()), path('workspace//application//work_flow_version', diff --git a/apps/application/views/application_access_token.py b/apps/application/views/application_access_token.py index 7023c4a03..5314520e1 100644 --- a/apps/application/views/application_access_token.py +++ b/apps/application/views/application_access_token.py @@ -31,7 +31,7 @@ class AccessToken(APIView): request=ApplicationAccessTokenAPI.get_request(), tags=[_('Application')] # type: ignore ) - @has_permissions(PermissionConstants.APPLICATION_OVERVIEW_ACCESS.get_workspace_permission()) + @has_permissions(PermissionConstants.APPLICATION_OVERVIEW_ACCESS.get_workspace_application_permission()) def put(self, request: Request, workspace_id: str, application_id: str): return result.success( AccessTokenSerializer(data={'application_id': application_id}).edit( @@ -45,6 +45,6 @@ class AccessToken(APIView): parameters=ApplicationAccessTokenAPI.get_parameters(), tags=[_('Application')] # type: ignore ) - @has_permissions(PermissionConstants.APPLICATION_READ.get_workspace_permission()) + @has_permissions(PermissionConstants.APPLICATION_READ.get_workspace_application_permission()) def get(self, request: Request, workspace_id: str, application_id: str): return result.success(AccessTokenSerializer(data={'application_id': application_id}).one()) diff --git a/apps/application/views/application_api_key.py b/apps/application/views/application_api_key.py index 216d5c8af..84ca17503 100644 --- a/apps/application/views/application_api_key.py +++ b/apps/application/views/application_api_key.py @@ -1,17 +1,17 @@ from django.db.models import QuerySet +from django.utils.translation import gettext_lazy as _ from drf_spectacular.utils import extend_schema from rest_framework.request import Request from rest_framework.views import APIView -from django.utils.translation import gettext_lazy as _ -from application.api.application_api_key import ApplicationKeyCreateAPI +from application.api.application_api_key import ApplicationKeyAPI from application.models import ApplicationApiKey from application.serializers.application_api_key import ApplicationKeySerializer from common.auth import TokenAuth from common.auth.authentication import has_permissions from common.constants.permission_constants import PermissionConstants from common.log.log import log -from common.result import result, success +from common.result import result, success, DefaultResultSerializer def get_application_operation_object(application_api_key_id): @@ -31,7 +31,9 @@ class ApplicationKey(APIView): description=_('Create application ApiKey'), summary=_('Create application ApiKey'), operation_id=_('Create application ApiKey'), # type: ignore - parameters=ApplicationKeyCreateAPI.get_parameters(), + parameters=ApplicationKeyAPI.get_parameters(), + request=None, + responses=ApplicationKeyAPI.get_response(), tags=[_('Application Api Key')] # type: ignore ) @log(menu='Application', operate="Add ApiKey", @@ -47,26 +49,50 @@ class ApplicationKey(APIView): description=_('GET application ApiKey List'), summary=_('Create application ApiKey List'), operation_id=_('Create application ApiKey List'), # type: ignore - parameters=ApplicationKeyCreateAPI.get_parameters(), + parameters=ApplicationKeyAPI.get_parameters(), + responses=ApplicationKeyAPI.List.get_response(), tags=[_('Application Api Key')] # type: ignore ) @has_permissions(PermissionConstants.APPLICATION_OVERVIEW_API_KEY.get_workspace_application_permission()) def get(self, request: Request, workspace_id: str, application_id: str): - return result, success(ApplicationKeySerializer( - data={'application_id': application_id, 'user_id': request.user.id, + return result.success(ApplicationKeySerializer( + data={'application_id': application_id, 'workspace_id': workspace_id}).list()) class Operate(APIView): authentication_classes = [TokenAuth] @extend_schema( - methods=['GET'], - description=_('GET application ApiKey List'), - summary=_('Create application ApiKey List'), - operation_id=_('Create application ApiKey List'), # type: ignore - parameters=ApplicationKeyCreateAPI.get_parameters(), + methods=['PUT'], + description=_('Modify application API_KEY'), + summary=_('Modify application API_KEY'), + operation_id=_('Modify application API_KEY'), # type: ignore + parameters=ApplicationKeyAPI.Operate.get_parameters(), + request=ApplicationKeyAPI.Operate.get_request(), + responses=DefaultResultSerializer, tags=[_('Application Api Key')] # type: ignore ) @has_permissions(PermissionConstants.APPLICATION_OVERVIEW_API_KEY.get_workspace_application_permission()) - def put(self, request: Request, application_id: str, workspace_id: str): - return result.success(ApplicationKeySerializer.Operate()) + def put(self, request: Request, workspace_id: str, application_id: str, api_key_id: str): + return result.success( + ApplicationKeySerializer.Operate( + data={'workspace_id': workspace_id, 'application_id': application_id, + 'api_key_id': api_key_id}).edit( + request.data)) + + @extend_schema( + methods=['DELETE'], + description=_('Delete Application API_KEY'), + summary=_('Delete Application API_KEY'), + operation_id=_('Delete Application API_KEY'), # type: ignore + parameters=ApplicationKeyAPI.Operate.get_parameters(), + request=ApplicationKeyAPI.Operate.get_request(), + responses=DefaultResultSerializer, + tags=[_('Application Api Key')] # type: ignore + ) + @has_permissions(PermissionConstants.APPLICATION_OVERVIEW_API_KEY.get_workspace_application_permission()) + def delete(self, request: Request, workspace_id: str, application_id: str, api_key_id: str): + return result.success( + ApplicationKeySerializer.Operate( + data={'workspace_id': workspace_id, 'application_id': application_id, + 'api_key_id': api_key_id}).delete()) diff --git a/apps/common/cache_data/application_api_key_cache.py b/apps/common/cache_data/application_api_key_cache.py new file mode 100644 index 000000000..9f854de62 --- /dev/null +++ b/apps/common/cache_data/application_api_key_cache.py @@ -0,0 +1,28 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: application_api_key_cache.py + @date:2024/7/25 11:30 + @desc: +""" +from django.core.cache import cache +from django.db.models import QuerySet + +from application.models import ApplicationApiKey +from common.constants.cache_version import Cache_Version +from common.utils.cache_util import get_cache + + +@get_cache(cache_key=Cache_Version.APPLICATION_API_KEY.get_key_func(), + use_get_data=lambda secret_key, use_get_data: use_get_data, + version=Cache_Version.APPLICATION_API_KEY.get_version()) +def get_application_api_key(secret_key, use_get_data): + application_api_key = QuerySet(ApplicationApiKey).filter(secret_key=secret_key).first() + return {'allow_cross_domain': application_api_key.allow_cross_domain, + 'cross_domain_list': application_api_key.cross_domain_list} + + +def del_application_api_key(secret_key): + cache.delete(Cache_Version.APPLICATION_API_KEY.get_key(secret_key=secret_key, use_get_data=True), + version=Cache_Version.APPLICATION_API_KEY.get_version()) diff --git a/apps/common/constants/cache_version.py b/apps/common/constants/cache_version.py index b30c22414..861f8dd24 100644 --- a/apps/common/constants/cache_version.py +++ b/apps/common/constants/cache_version.py @@ -29,6 +29,8 @@ class Cache_Version(Enum): # 对话 CHAT = "CHAT", lambda key: key + # 应用API KEY + APPLICATION_API_KEY = "APPLICATION_API_KEY", lambda secret_key, use_get_data: secret_key def get_version(self): return self.value[0] diff --git a/apps/common/utils/cache_util.py b/apps/common/utils/cache_util.py new file mode 100644 index 000000000..5f94bc469 --- /dev/null +++ b/apps/common/utils/cache_util.py @@ -0,0 +1,66 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: cache_util.py + @date:2024/7/24 19:23 + @desc: +""" +from django.core.cache import cache + + +def get_data_by_default_cache(key: str, get_data, cache_instance=cache, version=None, kwargs=None): + """ + 获取数据, 先从缓存中获取,如果获取不到再调用get_data 获取数据 + @param kwargs: get_data所需参数 + @param key: key + @param get_data: 获取数据函数 + @param cache_instance: cache实例 + @param version: 版本用于隔离 + @return: + """ + if kwargs is None: + kwargs = {} + if cache_instance.has_key(key, version=version): + return cache_instance.get(key, version=version) + data = get_data(**kwargs) + cache_instance.add(key, data, version=version) + return data + + +def set_data_by_default_cache(key: str, get_data, cache_instance=cache, version=None): + data = get_data() + cache_instance.set(key, data, version=version) + return data + + +def get_cache(cache_key, use_get_data: any = True, cache_instance=cache, version=None): + def inner(get_data): + def run(*args, **kwargs): + key = cache_key(*args, **kwargs) if callable(cache_key) else cache_key + is_use_get_data = use_get_data(*args, **kwargs) if callable(use_get_data) else use_get_data + if is_use_get_data: + if cache_instance.has_key(key, version=version): + return cache_instance.get(key, version=version) + data = get_data(*args, **kwargs) + cache_instance.add(key, data, timeout=None, version=version) + return data + data = get_data(*args, **kwargs) + cache_instance.set(key, data, timeout=None, version=version) + return data + + return run + + return inner + + +def del_cache(cache_key, cache_instance=cache, version=None): + def inner(func): + def run(*args, **kwargs): + key = cache_key(*args, **kwargs) if callable(cache_key) else cache_key + func(*args, **kwargs) + cache_instance.delete(key, version=version) + + return run + + return inner