perf: some swagger api (#15203)

* perf: some swagger api

* perf: update deps

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
fit2bot 2025-04-15 11:43:36 +08:00 committed by GitHub
parent 8b9fe3c72b
commit 5390fbacec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1287 additions and 1158 deletions

View File

@ -1,4 +1,4 @@
FROM jumpserver/core-base:20250224_065619 AS stage-build FROM jumpserver/core-base:20250415_032719 AS stage-build
ARG VERSION ARG VERSION

View File

@ -10,6 +10,7 @@ class VirtualAccountViewSet(OrgBulkModelViewSet):
serializer_class = VirtualAccountSerializer serializer_class = VirtualAccountSerializer
search_fields = ('alias',) search_fields = ('alias',)
filterset_fields = ('alias',) filterset_fields = ('alias',)
http_method_names = ['get']
def get_queryset(self): def get_queryset(self):
return VirtualAccount.get_or_init_queryset() return VirtualAccount.get_or_init_queryset()

View File

@ -39,9 +39,10 @@ class AutomationAssetsListApi(generics.ListAPIView):
return assets return assets
class AutomationRemoveAssetApi(generics.RetrieveUpdateAPIView): class AutomationRemoveAssetApi(generics.UpdateAPIView):
model = BaseAutomation model = BaseAutomation
serializer_class = serializers.UpdateAssetSerializer serializer_class = serializers.UpdateAssetSerializer
http_method_names = ['put']
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
@ -56,9 +57,10 @@ class AutomationRemoveAssetApi(generics.RetrieveUpdateAPIView):
return Response({'msg': 'ok'}) return Response({'msg': 'ok'})
class AutomationAddAssetApi(generics.RetrieveUpdateAPIView): class AutomationAddAssetApi(generics.UpdateAPIView):
model = BaseAutomation model = BaseAutomation
serializer_class = serializers.UpdateAssetSerializer serializer_class = serializers.UpdateAssetSerializer
http_method_names = ['put']
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
@ -72,9 +74,10 @@ class AutomationAddAssetApi(generics.RetrieveUpdateAPIView):
return Response({"error": serializer.errors}) return Response({"error": serializer.errors})
class AutomationNodeAddRemoveApi(generics.RetrieveUpdateAPIView): class AutomationNodeAddRemoveApi(generics.UpdateAPIView):
model = BaseAutomation model = BaseAutomation
serializer_class = serializers.UpdateNodeSerializer serializer_class = serializers.UpdateNodeSerializer
http_method_names = ['put']
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
action_params = ['add', 'remove'] action_params = ['add', 'remove']

View File

@ -147,6 +147,7 @@ class CheckAccountEngineViewSet(JMSModelViewSet):
serializer_class = serializers.CheckAccountEngineSerializer serializer_class = serializers.CheckAccountEngineSerializer
permission_classes = [RBACPermission, IsValidLicense] permission_classes = [RBACPermission, IsValidLicense]
perm_model = CheckAccountEngine perm_model = CheckAccountEngine
http_method_names = ['get']
def get_queryset(self): def get_queryset(self):
return CheckAccountEngine.get_default_engines() return CheckAccountEngine.get_default_engines()

View File

@ -97,10 +97,10 @@ class AssetFilterSet(BaseFilterSet):
return queryset.filter(protocols__name__in=value).distinct() return queryset.filter(protocols__name__in=value).distinct()
class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet): class BaseAssetViewSet(OrgBulkModelViewSet):
"""
API endpoint that allows Asset to be viewed or edited.
""" """
API endpoint that allows Asset to be viewed or edited.
"""
model = Asset model = Asset
filterset_class = AssetFilterSet filterset_class = AssetFilterSet
search_fields = ("name", "address", "comment") search_fields = ("name", "address", "comment")
@ -143,6 +143,19 @@ class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
return retrieve_cls return retrieve_cls
return cls return cls
def create(self, request, *args, **kwargs):
if request.path.find('/api/v1/assets/assets/') > -1:
error = _('Cannot create asset directly, you should create a host or other')
return Response({'error': error}, status=400)
if not settings.XPACK_LICENSE_IS_VALID and self.model.objects.order_by().count() >= 5000:
error = _('The number of assets exceeds the limit of 5000')
return Response({'error': error}, status=400)
return super().create(request, *args, **kwargs)
class AssetViewSet(SuggestionMixin, BaseAssetViewSet):
@action(methods=["GET"], detail=True, url_path="platform") @action(methods=["GET"], detail=True, url_path="platform")
def platform(self, *args, **kwargs): def platform(self, *args, **kwargs):
asset = super().get_object() asset = super().get_object()
@ -197,17 +210,6 @@ class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
Protocol.objects.bulk_create(objs) Protocol.objects.bulk_create(objs)
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_200_OK)
def create(self, request, *args, **kwargs):
if request.path.find('/api/v1/assets/assets/') > -1:
error = _('Cannot create asset directly, you should create a host or other')
return Response({'error': error}, status=400)
if not settings.XPACK_LICENSE_IS_VALID and self.model.objects.order_by().count() >= 5000:
error = _('The number of assets exceeds the limit of 5000')
return Response({'error': error}, status=400)
return super().create(request, *args, **kwargs)
def filter_bulk_update_data(self): def filter_bulk_update_data(self):
bulk_data = [] bulk_data = []
skip_assets = [] skip_assets = []

View File

@ -1,12 +1,12 @@
from assets.models import Cloud, Asset from assets.models import Cloud, Asset
from assets.serializers import CloudSerializer from assets.serializers import CloudSerializer
from .asset import AssetViewSet from .asset import BaseAssetViewSet
__all__ = ['CloudViewSet'] __all__ = ['CloudViewSet']
class CloudViewSet(AssetViewSet): class CloudViewSet(BaseAssetViewSet):
model = Cloud model = Cloud
perm_model = Asset perm_model = Asset

View File

@ -1,12 +1,12 @@
from assets.models import Custom, Asset from assets.models import Custom, Asset
from assets.serializers import CustomSerializer from assets.serializers import CustomSerializer
from .asset import AssetViewSet from .asset import BaseAssetViewSet
__all__ = ['CustomViewSet'] __all__ = ['CustomViewSet']
class CustomViewSet(AssetViewSet): class CustomViewSet(BaseAssetViewSet):
model = Custom model = Custom
perm_model = Asset perm_model = Asset

View File

@ -1,12 +1,12 @@
from assets.models import Database, Asset from assets.models import Database, Asset
from assets.serializers import DatabaseSerializer from assets.serializers import DatabaseSerializer
from .asset import AssetViewSet from .asset import BaseAssetViewSet
__all__ = ['DatabaseViewSet'] __all__ = ['DatabaseViewSet']
class DatabaseViewSet(AssetViewSet): class DatabaseViewSet(BaseAssetViewSet):
model = Database model = Database
perm_model = Asset perm_model = Asset

View File

@ -1,11 +1,11 @@
from assets.serializers import DeviceSerializer
from assets.models import Device, Asset from assets.models import Device, Asset
from .asset import AssetViewSet from assets.serializers import DeviceSerializer
from .asset import BaseAssetViewSet
__all__ = ['DeviceViewSet'] __all__ = ['DeviceViewSet']
class DeviceViewSet(AssetViewSet): class DeviceViewSet(BaseAssetViewSet):
model = Device model = Device
perm_model = Asset perm_model = Asset

View File

@ -1,12 +1,12 @@
from assets.models import DirectoryService, Asset from assets.models import DirectoryService, Asset
from assets.serializers import DSSerializer from assets.serializers import DSSerializer
from .asset import AssetViewSet from .asset import BaseAssetViewSet
__all__ = ['DSViewSet'] __all__ = ['DSViewSet']
class DSViewSet(AssetViewSet): class DSViewSet(BaseAssetViewSet):
model = DirectoryService model = DirectoryService
perm_model = Asset perm_model = Asset

View File

@ -1,12 +1,12 @@
from assets.models import GPT, Asset from assets.models import GPT, Asset
from assets.serializers import GPTSerializer from assets.serializers import GPTSerializer
from .asset import AssetViewSet from .asset import BaseAssetViewSet
__all__ = ['GPTViewSet'] __all__ = ['GPTViewSet']
class GPTViewSet(AssetViewSet): class GPTViewSet(BaseAssetViewSet):
model = GPT model = GPT
perm_model = Asset perm_model = Asset

View File

@ -1,11 +1,11 @@
from assets.models import Host, Asset from assets.models import Host, Asset
from assets.serializers import HostSerializer from assets.serializers import HostSerializer
from .asset import AssetViewSet from .asset import BaseAssetViewSet
__all__ = ['HostViewSet'] __all__ = ['HostViewSet']
class HostViewSet(AssetViewSet): class HostViewSet(BaseAssetViewSet):
model = Host model = Host
perm_model = Asset perm_model = Asset

View File

@ -1,12 +1,12 @@
from assets.models import Web, Asset from assets.models import Web, Asset
from assets.serializers import WebSerializer from assets.serializers import WebSerializer
from .asset import AssetViewSet from .asset import BaseAssetViewSet
__all__ = ['WebViewSet'] __all__ = ['WebViewSet']
class WebViewSet(AssetViewSet): class WebViewSet(BaseAssetViewSet):
model = Web model = Web
perm_model = Asset perm_model = Asset

View File

@ -188,6 +188,9 @@ class ActivityUnionLogSerializer(serializers.Serializer):
class FileSerializer(serializers.Serializer): class FileSerializer(serializers.Serializer):
file = serializers.FileField(allow_empty_file=True) file = serializers.FileField(allow_empty_file=True)
class Meta:
ref_name = 'AuditFileSerializer'
class UserSessionSerializer(serializers.ModelSerializer): class UserSessionSerializer(serializers.ModelSerializer):
type = LabeledChoiceField(choices=LoginTypeChoices.choices, label=_("Type")) type = LabeledChoiceField(choices=LoginTypeChoices.choices, label=_("Type"))

View File

@ -2,6 +2,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
import uuid
import private_storage.urls import private_storage.urls
from django.conf import settings from django.conf import settings
@ -74,12 +75,19 @@ urlpatterns += [
path('core/jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'), path('core/jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
] ]
DOC_TTL = 60 * 60
DOC_VERSION = uuid.uuid4().hex
cache_kwargs = {
'cache_timeout': DOC_TTL,
'cache_kwargs': {
'key_prefix': 'swagger-cache-' + DOC_VERSION,
},
}
# docs 路由 # docs 路由
urlpatterns += [ urlpatterns += [
re_path('^api/swagger(?P<format>\.json|\.yaml)$', path('api/swagger.<format>', views.get_swagger_view(False).without_ui(**cache_kwargs), name='schema-json'),
views.get_swagger_view().without_ui(cache_timeout=1), name='schema-json'), re_path('api/docs/?', views.get_swagger_view().with_ui('swagger', **cache_kwargs), name="docs"),
re_path('api/docs/?', views.get_swagger_view().with_ui('swagger', cache_timeout=1), name="docs"), re_path('api/redoc/?', views.get_swagger_view().with_ui('redoc', **cache_kwargs), name='redoc'),
re_path('api/redoc/?', views.get_swagger_view().with_ui('redoc', cache_timeout=1), name='redoc'),
] ]
if os.environ.get('DEBUG_TOOLBAR', False): if os.environ.get('DEBUG_TOOLBAR', False):

View File

@ -1,8 +1,54 @@
from drf_yasg.inspectors import SwaggerAutoSchema
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi from drf_yasg import openapi
from drf_yasg.generators import OpenAPISchemaGenerator
from drf_yasg.inspectors import SwaggerAutoSchema
from drf_yasg.views import get_schema_view
from rest_framework import permissions
class CustomSchemaGenerator(OpenAPISchemaGenerator):
from_mcp = False
def get_schema(self, request=None, public=False):
self.from_mcp = request.query_params.get('mcp') or request.path.endswith('swagger.json')
return super().get_schema(request, public)
@staticmethod
def exclude_some_paths(path):
# 这里可以对 paths 进行处理
excludes = ['/report/', '/render-to-json/', '/suggestions/', 'executions', 'automations']
for p in excludes:
if path.find(p) >= 0:
return True
return False
def exclude_some_app(self, path):
parts = path.split('/')
if len(parts) < 4:
return False
apps = []
if self.from_mcp:
apps = [
'ops', 'tickets', 'common', 'authentication',
'settings', 'xpack', 'terminal', 'rbac'
]
app_name = parts[3]
if app_name in apps:
return True
return False
def get_operation(self, view, path, prefix, method, components, request):
# 这里可以对 path 进行处理
if self.exclude_some_paths(path):
return None
if self.exclude_some_app(path):
return None
operation = super().get_operation(view, path, prefix, method, components, request)
operation_id = operation.get('operationId')
if 'bulk' in operation_id:
return None
return operation
class CustomSwaggerAutoSchema(SwaggerAutoSchema): class CustomSwaggerAutoSchema(SwaggerAutoSchema):
@ -59,17 +105,25 @@ api_info = openapi.Info(
) )
def get_swagger_view(version='v1'): def get_swagger_view(with_auth=True):
from ..urls import api_v1 from ..urls import api_v1
from django.urls import path, include from django.urls import path, include
api_v1_patterns = [ patterns = [
path('api/v1/', include(api_v1)) path('api/v1/', include(api_v1))
] ]
patterns = api_v1_patterns
if with_auth:
permission_classes = (permissions.IsAuthenticated,)
public = False
else:
permission_classes = []
public = True
schema_view = get_schema_view( schema_view = get_schema_view(
api_info, api_info,
public=public,
patterns=patterns, patterns=patterns,
permission_classes=(permissions.IsAuthenticated,), generator_class=CustomSchemaGenerator,
permission_classes=permission_classes
) )
return schema_view return schema_view

View File

@ -2,7 +2,6 @@
# #
import logging import logging
from django.conf import settings
from django.db.models import Q from django.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_filters import rest_framework as filters from django_filters import rest_framework as filters
@ -10,7 +9,7 @@ from rest_framework import generics
from rest_framework import status from rest_framework import status
from rest_framework.views import APIView, Response from rest_framework.views import APIView, Response
from common.api import JMSBulkModelViewSet from common.api import JMSModelViewSet
from common.drf.filters import BaseFilterSet from common.drf.filters import BaseFilterSet
from common.exceptions import JMSException from common.exceptions import JMSException
from common.permissions import WithBootstrapToken, IsServiceAccount from common.permissions import WithBootstrapToken, IsServiceAccount
@ -43,7 +42,7 @@ class TerminalFilterSet(BaseFilterSet):
return queryset return queryset
class TerminalViewSet(JMSBulkModelViewSet): class TerminalViewSet(JMSModelViewSet):
queryset = Terminal.objects.filter(is_deleted=False) queryset = Terminal.objects.filter(is_deleted=False)
serializer_class = serializers.TerminalSerializer serializer_class = serializers.TerminalSerializer
filterset_class = TerminalFilterSet filterset_class = TerminalFilterSet

View File

@ -12,7 +12,6 @@ app_name = 'terminal'
router = BulkRouter() router = BulkRouter()
router.register(r'sessions', api.SessionViewSet, 'session') router.register(r'sessions', api.SessionViewSet, 'session')
router.register(r'terminals/((?P<terminal>[^/.]{36})/)?status', api.StatusViewSet, 'terminal-status') router.register(r'terminals/((?P<terminal>[^/.]{36})/)?status', api.StatusViewSet, 'terminal-status')
router.register(r'terminals/((?P<terminal>[^/.]{36})/)?sessions', api.SessionViewSet, 'terminal-sessions')
router.register(r'terminals', api.TerminalViewSet, 'terminal') router.register(r'terminals', api.TerminalViewSet, 'terminal')
router.register(r'tasks', api.TaskViewSet, 'tasks') router.register(r'tasks', api.TaskViewSet, 'tasks')
router.register(r'commands', api.CommandViewSet, 'command') router.register(r'commands', api.CommandViewSet, 'command')

2267
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -99,7 +99,7 @@ django-private-storage = "3.1"
drf-nested-routers = "0.93.4" drf-nested-routers = "0.93.4"
drf-writable-nested = "0.7.0" drf-writable-nested = "0.7.0"
rest-condition = "1.0.3" rest-condition = "1.0.3"
drf-yasg = "1.21.7" drf-yasg = "1.21.10"
coreapi = "2.3.3" coreapi = "2.3.3"
coreschema = "0.0.4" coreschema = "0.0.4"
openapi-codec = "1.3.2" openapi-codec = "1.3.2"
@ -148,7 +148,7 @@ mistune = "2.0.3"
openai = "^1.29.0" openai = "^1.29.0"
xlsxwriter = "^3.1.9" xlsxwriter = "^3.1.9"
exchangelib = "^5.1.0" exchangelib = "^5.1.0"
xmlsec = "^1.3.13" xmlsec = "1.3.14"
lxml = "5.2.1" lxml = "5.2.1"
pydantic = "^2.7.4" pydantic = "^2.7.4"
annotated-types = "^0.6.0" annotated-types = "^0.6.0"