Compare commits

...

3 Commits

Author SHA1 Message Date
ibuler
777b655e90 perf: stash it 2025-11-03 18:04:12 +08:00
ibuler
6e1736c1a7 stash 2025-11-03 14:26:36 +08:00
ibuler
fe31eb0a44 feat: mcp internal 2025-10-31 14:14:23 +08:00
8 changed files with 146 additions and 48 deletions

View File

@@ -1,3 +1,5 @@
#!/usr/bin/env python3
list_params = [
{
"name": "search",
@@ -11,41 +13,99 @@ list_params = [
"in": "query",
"description": "Which field to use when ordering the results.",
"required": False,
"type": "string"
"type": "string",
"enum": [
"name", "date_created", "date_updated", "created_by",
]
},
{
"name": "limit",
"in": "query",
"description": "Number of results to return per page. Default is 10.",
"required": False,
"type": "integer"
"type": "integer",
"default": 10
},
{
"name": "offset",
"in": "query",
"description": "The initial index from which to return the results.",
"required": False,
"type": "integer"
"type": "integer",
"default": 0
},
]
unsupported_resources = [
"customs", "protocol-settings", "gathered-accounts",
"account-template-secrets", 'change-secret-records',
'account-backup-executions', 'change-secret-executions',
'change-secret-status', 'gather-account-executions',
'push-account-executions', 'check-account-executions',
'integration-apps', 'asset-permissions-users-relations',
'asset-permissions-user-groups-relations', 'asset-permissions-assets-relations',
'asset-permissions-nodes-relations', 'terminal-status', 'tasks', 'status',
'session-sharing-records', 'endpoints', 'endpoint-rules',
'chatai-prompts', 'leak-passwords', 'super-connection-tokens',
'system-role-bindings', 'org-role-bindings', 'content-types', 'system-role-permissions',
'org-role-permissions', 'system-msg-subscriptions',
'celery-period-tasks', 'task-executions', 'adhocs',
'user-sessions', 'service-access-logs',
'applet-publications', 'applet-host-deployments',
'virtual-app-publications', 'applet-host-accounts', 'applet-host-applets',
'flows'
]
supported_resources = [
# User
'users', 'user-groups', 'users-groups-relations',
# Asset
'assets', 'hosts', 'devices', 'databases', 'webs', 'clouds', 'gpts',
'ds', 'platforms', 'nodes', 'zones', 'gateways',
# Account
'virtual-accounts', 'account-templates', 'account-backups',
# Automation
'change-secret-automations', 'gather-account-automations',
'push-account-automations', 'check-account-automations',
'account-risks',
# Permission
'asset-permissions',
# Terminal
'terminals', 'replay-storages', 'command-storages',
# Applet
'applets', 'applet-hosts', 'virtual-apps',
# Ops
'playbooks', 'jobs',
# Audit
'ftp-logs', 'login-logs', 'operate-logs', 'password-change-logs', 'job-logs',
# Tickets
'tickets', 'comments', 'apply-assets', 'apply-nodes',
# Acls
'login-acls', 'login-asset-acls', 'command-filter-acls',
'command-groups', 'connect-method-acls', 'data-masking-rules',
# RBAC
'roles', 'role-bindings',
'system-roles', 'org-roles',
# Label
'labeled-resources',
'labels',
]
common_params = [
{
"name": "resource",
"in": "path",
"description": """Resource to query, e.g. users, assets, permissions, acls, user-groups, policies, nodes, hosts,
devices, clouds, webs, databases,
gpts, ds, customs, platforms, zones, gateways, protocol-settings, labels, virtual-accounts,
gathered-accounts, account-templates, account-template-secrets, account-backups, account-backup-executions,
change-secret-automations, change-secret-executions, change-secret-records, gather-account-automations,
gather-account-executions, push-account-automations, push-account-executions, push-account-records,
check-account-automations, check-account-executions, account-risks, integration-apps, asset-permissions,
zones, gateways, virtual-accounts, gathered-accounts, account-templates, account-template-secrets,,
GET /api/v1/resources/ to get full supported resource.
""",
"description": f"""Resource to query, {supported_resources}
GET /api/v1/resources/ to get full supported resource.
if you want to get the resource list, you can set the resource name in the url.
if you want to create a resource, you can set the resource name in the url.
if you want to get the resource detail, you can set the resource name and id in the url.
if you want to update the resource, you can set the resource name and id in the url.
if you want to delete the resource, you can set the resource name and id in the url.
""",
"required": True,
"type": "string"
"type": "string",
"enum": supported_resources
},
{
"name": "X-JMS-ORG",

View File

@@ -69,13 +69,13 @@ class ResourceListApi(ProxyMixin, APIView):
return self._proxy(request, resource)
@extend_schema(
operation_id="create_resource_by_type",
operation_id="create_resource",
summary="Create resource",
parameters=create_params,
description="""
Create resource,
OPTIONS /api/v1/resources/{resource}/?action=post to get every resource type field type and helptext, and
you will know how to create it.
OPTIONS /api/v1/resources/{resource}/?action=post to get every resource type field type and help text,
and you will know how to create it.
""",
)
def post(self, request, resource, pk=None):

View File

@@ -7,6 +7,7 @@ from rest_framework.response import Response
from rest_framework.routers import DefaultRouter
from rest_framework.views import APIView
from .const import supported_resources
from .utils import get_full_resource_map
router = DefaultRouter()
@@ -35,6 +36,8 @@ class ResourceTypeListApi(APIView):
result = []
resource_map = get_full_resource_map()
for name, desc in resource_map.items():
if name not in supported_resources:
continue
desc = resource_map.get(name, {})
resource = {
"name": name,

View File

@@ -4,8 +4,9 @@ import re
from functools import lru_cache
from typing import Dict
from django.urls import URLPattern
from django.urls import URLResolver
from django.utils.functional import LazyObject
from django.urls import URLPattern, URLResolver
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter
from rest_framework.routers import DefaultRouter
@@ -114,9 +115,37 @@ def extract_resource_paths(urlpatterns, prefix='/api/v1/') -> Dict[str, Dict[str
def param_dic_to_param(d):
# 将 'in' 字段映射到 OpenApiParameter 的 location 常量
location_map = {
'query': OpenApiParameter.QUERY,
'path': OpenApiParameter.PATH,
'header': OpenApiParameter.HEADER,
'cookie': OpenApiParameter.COOKIE,
}
location = location_map.get(d['in'], OpenApiParameter.QUERY)
# 将 type 字符串映射到 OpenApiTypes
type_map = {
'string': OpenApiTypes.STR,
'integer': OpenApiTypes.INT,
'number': OpenApiTypes.FLOAT,
'boolean': OpenApiTypes.BOOL,
'array': OpenApiTypes.OBJECT, # 对于 array 类型,需要特殊处理
'object': OpenApiTypes.OBJECT,
}
param_type = type_map.get(d['type'], OpenApiTypes.STR)
enum = None
if d.get('enum'):
enum = d['enum']
return OpenApiParameter(
name=d['name'], location=d['in'],
description=d['description'], type=d['type'], required=d.get('required', False)
name=d['name'],
location=location,
description=d['description'],
type=param_type,
required=d.get('required', False),
enum=enum
)

View File

@@ -88,7 +88,7 @@ urlpatterns += [
path('core/jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
]
DOC_TTL = 60 * 60
DOC_TTL = 1
DOC_VERSION = uuid.uuid4().hex
cache_kwargs = {
'cache_timeout': DOC_TTL,
@@ -98,7 +98,7 @@ cache_kwargs = {
}
# docs 路由
urlpatterns += [
path('api/swagger.json', views.get_swagger_view(ui='json', **cache_kwargs), name='schema-json'),
path('api/swagger.json', views.get_swagger_view(ui='json'), name='schema-json'),
path('api/swagger.yaml', views.get_swagger_view(ui='yaml', **cache_kwargs), name='schema'),
re_path('api/docs/?', views.get_swagger_view(ui='swagger', **cache_kwargs), name="docs"),
re_path('api/redoc/?', views.get_swagger_view(ui='redoc', **cache_kwargs), name='redoc'),

View File

@@ -14,7 +14,7 @@ class CustomSchemaGenerator(SchemaGenerator):
class CustomAutoSchema(AutoSchema):
def __init__(self, *args, **kwargs):
self.from_mcp = kwargs.get('from_mcp', False)
self.from_mcp = True
super().__init__(*args, **kwargs)
def map_parsers(self):
@@ -30,19 +30,19 @@ class CustomAutoSchema(AutoSchema):
tags = ['_'.join(operation_keys[:2])]
return tags
def get_operation(self, path, *args, **kwargs):
if path.endswith('render-to-json/'):
return None
# if not path.startswith('/api/v1/users'):
# return None
operation = super().get_operation(path, *args, **kwargs)
if not operation:
return operation
# def get_operation(self, path, *args, **kwargs):
# if path.endswith('render-to-json/'):
# return None
# # if not path.startswith('/api/v1/users'):
# # return None
# operation = super().get_operation(path, *args, **kwargs)
# if not operation:
# return operation
if not operation.get('summary', ''):
operation['summary'] = operation.get('operationId')
# if not operation.get('summary', ''):
# operation['summary'] = operation.get('operationId')
return operation
# return operation
def get_operation_id(self):
tokenized_path = self._tokenize_path()
@@ -118,7 +118,8 @@ class CustomAutoSchema(AutoSchema):
'change-secret-dashboard', '/copy-to-assets/',
'/move-to-assets/', 'dashboard', 'index', 'countries',
'/resources/cache/', 'profile/mfa', 'profile/password',
'profile/permissions', 'prometheus', 'constraints'
'profile/permissions', 'prometheus', 'constraints',
'/api/swagger.json', '/api/swagger.yaml',
]
for p in excludes:
if path.find(p) >= 0:
@@ -132,14 +133,15 @@ class CustomAutoSchema(AutoSchema):
apps = []
if self.from_mcp:
apps = [
'ops', 'tickets', 'authentication',
'settings', 'xpack', 'terminal', 'rbac',
'notifications', 'promethues', 'acls'
]
# apps = [
# 'ops', 'tickets', 'authentication',
# 'settings', 'xpack', 'terminal', 'rbac',
# 'notifications', 'promethues', 'acls'
# ]
apps = ['resources']
app_name = parts[3]
if app_name in apps:
if app_name not in apps:
return True
models = []
model = parts[4]
@@ -190,6 +192,9 @@ class CustomAutoSchema(AutoSchema):
if not operation.get('summary', ''):
operation['summary'] = operation.get('operationId')
if self.is_excluded():
return None
exclude_operations = [
'orgs_orgs_read', 'orgs_orgs_update', 'orgs_orgs_delete',

View File

@@ -9,15 +9,16 @@ from rest_framework.response import Response
from django.contrib.auth.mixins import LoginRequiredMixin
class SwaggerUI(LoginRequiredMixin, SpectacularSwaggerView):
class SwaggerUI( SpectacularSwaggerView):
pass
class Redoc(LoginRequiredMixin, SpectacularRedocView):
pass
class SchemeMixin:
permission_classes = []
def get(self, request, *args, **kwargs):
schema = super().get(request, *args, **kwargs).data
host = request.get_host()
@@ -37,7 +38,7 @@ class SchemeMixin:
}
return Response(schema)
@method_decorator(cache_page(60 * 5,), name="dispatch")
# @method_decorator(cache_page(60 * 5,), name="dispatch")
class JsonApi(SchemeMixin, SpectacularJSONAPIView):
pass

6
uv.lock generated
View File

@@ -2595,7 +2595,7 @@ requires-dist = [
{ name = "python-cas", specifier = "==1.6.0" },
{ name = "python-daemon", specifier = "==3.0.1" },
{ name = "python-dateutil", specifier = "==2.8.2" },
{ name = "python-ldap", specifier = "==3.4.3" },
{ name = "python-ldap", specifier = "==3.4.5" },
{ name = "python-nmap", specifier = "==0.7.1" },
{ name = "python-redis-lock", specifier = "==4.0.0" },
{ name = "python3-saml", specifier = "==1.16.0" },
@@ -4211,13 +4211,13 @@ wheels = [
[[package]]
name = "python-ldap"
version = "3.4.3"
version = "3.4.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
{ name = "pyasn1-modules" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3a/7d/de9ae3e5843de77eae3a60c1e70ef5cad9960db50521e8459f7d567a1d1d/python-ldap-3.4.3.tar.gz", hash = "sha256:ab26c519a0ef2a443a2a10391fa3c5cb52d7871323399db949ebfaa9f25ee2a0", size = 377438, upload-time = "2022-09-20T15:46:21.283Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0c/88/8d2797decc42e1c1cdd926df4f005e938b0643d0d1219c08c2b5ee8ae0c0/python_ldap-3.4.5.tar.gz", hash = "sha256:b2f6ef1c37fe2c6a5a85212efe71311ee21847766a7d45fcb711f3b270a5f79a", size = 388482, upload-time = "2025-10-10T20:00:39.06Z" }
[[package]]
name = "python-nmap"