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 = [ list_params = [
{ {
"name": "search", "name": "search",
@@ -11,41 +13,99 @@ list_params = [
"in": "query", "in": "query",
"description": "Which field to use when ordering the results.", "description": "Which field to use when ordering the results.",
"required": False, "required": False,
"type": "string" "type": "string",
"enum": [
"name", "date_created", "date_updated", "created_by",
]
}, },
{ {
"name": "limit", "name": "limit",
"in": "query", "in": "query",
"description": "Number of results to return per page. Default is 10.", "description": "Number of results to return per page. Default is 10.",
"required": False, "required": False,
"type": "integer" "type": "integer",
"default": 10
}, },
{ {
"name": "offset", "name": "offset",
"in": "query", "in": "query",
"description": "The initial index from which to return the results.", "description": "The initial index from which to return the results.",
"required": False, "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 = [ common_params = [
{ {
"name": "resource", "name": "resource",
"in": "path", "in": "path",
"description": """Resource to query, e.g. users, assets, permissions, acls, user-groups, policies, nodes, hosts, "description": f"""Resource to query, {supported_resources}
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. 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, "required": True,
"type": "string" "type": "string",
"enum": supported_resources
}, },
{ {
"name": "X-JMS-ORG", "name": "X-JMS-ORG",

View File

@@ -69,13 +69,13 @@ class ResourceListApi(ProxyMixin, APIView):
return self._proxy(request, resource) return self._proxy(request, resource)
@extend_schema( @extend_schema(
operation_id="create_resource_by_type", operation_id="create_resource",
summary="Create resource", summary="Create resource",
parameters=create_params, parameters=create_params,
description=""" description="""
Create resource, Create resource,
OPTIONS /api/v1/resources/{resource}/?action=post to get every resource type field type and helptext, and OPTIONS /api/v1/resources/{resource}/?action=post to get every resource type field type and help text,
you will know how to create it. and you will know how to create it.
""", """,
) )
def post(self, request, resource, pk=None): 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.routers import DefaultRouter
from rest_framework.views import APIView from rest_framework.views import APIView
from .const import supported_resources
from .utils import get_full_resource_map from .utils import get_full_resource_map
router = DefaultRouter() router = DefaultRouter()
@@ -35,6 +36,8 @@ class ResourceTypeListApi(APIView):
result = [] result = []
resource_map = get_full_resource_map() resource_map = get_full_resource_map()
for name, desc in resource_map.items(): for name, desc in resource_map.items():
if name not in supported_resources:
continue
desc = resource_map.get(name, {}) desc = resource_map.get(name, {})
resource = { resource = {
"name": name, "name": name,

View File

@@ -4,8 +4,9 @@ import re
from functools import lru_cache from functools import lru_cache
from typing import Dict from typing import Dict
from django.urls import URLPattern from django.utils.functional import LazyObject
from django.urls import URLResolver from django.urls import URLPattern, URLResolver
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter from drf_spectacular.utils import OpenApiParameter
from rest_framework.routers import DefaultRouter 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): 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( return OpenApiParameter(
name=d['name'], location=d['in'], name=d['name'],
description=d['description'], type=d['type'], required=d.get('required', False) 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'), path('core/jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
] ]
DOC_TTL = 60 * 60 DOC_TTL = 1
DOC_VERSION = uuid.uuid4().hex DOC_VERSION = uuid.uuid4().hex
cache_kwargs = { cache_kwargs = {
'cache_timeout': DOC_TTL, 'cache_timeout': DOC_TTL,
@@ -98,7 +98,7 @@ cache_kwargs = {
} }
# docs 路由 # docs 路由
urlpatterns += [ 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'), 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/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'), 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): class CustomAutoSchema(AutoSchema):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.from_mcp = kwargs.get('from_mcp', False) self.from_mcp = True
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def map_parsers(self): def map_parsers(self):
@@ -30,19 +30,19 @@ class CustomAutoSchema(AutoSchema):
tags = ['_'.join(operation_keys[:2])] tags = ['_'.join(operation_keys[:2])]
return tags return tags
def get_operation(self, path, *args, **kwargs): # def get_operation(self, path, *args, **kwargs):
if path.endswith('render-to-json/'): # if path.endswith('render-to-json/'):
return None
# if not path.startswith('/api/v1/users'):
# return None # return None
operation = super().get_operation(path, *args, **kwargs) # # if not path.startswith('/api/v1/users'):
if not operation: # # return None
return operation # operation = super().get_operation(path, *args, **kwargs)
# if not operation:
# return operation
if not operation.get('summary', ''): # if not operation.get('summary', ''):
operation['summary'] = operation.get('operationId') # operation['summary'] = operation.get('operationId')
return operation # return operation
def get_operation_id(self): def get_operation_id(self):
tokenized_path = self._tokenize_path() tokenized_path = self._tokenize_path()
@@ -118,7 +118,8 @@ class CustomAutoSchema(AutoSchema):
'change-secret-dashboard', '/copy-to-assets/', 'change-secret-dashboard', '/copy-to-assets/',
'/move-to-assets/', 'dashboard', 'index', 'countries', '/move-to-assets/', 'dashboard', 'index', 'countries',
'/resources/cache/', 'profile/mfa', 'profile/password', '/resources/cache/', 'profile/mfa', 'profile/password',
'profile/permissions', 'prometheus', 'constraints' 'profile/permissions', 'prometheus', 'constraints',
'/api/swagger.json', '/api/swagger.yaml',
] ]
for p in excludes: for p in excludes:
if path.find(p) >= 0: if path.find(p) >= 0:
@@ -132,14 +133,15 @@ class CustomAutoSchema(AutoSchema):
apps = [] apps = []
if self.from_mcp: if self.from_mcp:
apps = [ # apps = [
'ops', 'tickets', 'authentication', # 'ops', 'tickets', 'authentication',
'settings', 'xpack', 'terminal', 'rbac', # 'settings', 'xpack', 'terminal', 'rbac',
'notifications', 'promethues', 'acls' # 'notifications', 'promethues', 'acls'
] # ]
apps = ['resources']
app_name = parts[3] app_name = parts[3]
if app_name in apps: if app_name not in apps:
return True return True
models = [] models = []
model = parts[4] model = parts[4]
@@ -191,6 +193,9 @@ class CustomAutoSchema(AutoSchema):
if not operation.get('summary', ''): if not operation.get('summary', ''):
operation['summary'] = operation.get('operationId') operation['summary'] = operation.get('operationId')
if self.is_excluded():
return None
exclude_operations = [ exclude_operations = [
'orgs_orgs_read', 'orgs_orgs_update', 'orgs_orgs_delete', 'orgs_orgs_read', 'orgs_orgs_update', 'orgs_orgs_delete',
'orgs_orgs_create', 'orgs_orgs_partial_update', 'orgs_orgs_create', 'orgs_orgs_partial_update',

View File

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

6
uv.lock generated
View File

@@ -2595,7 +2595,7 @@ requires-dist = [
{ name = "python-cas", specifier = "==1.6.0" }, { name = "python-cas", specifier = "==1.6.0" },
{ name = "python-daemon", specifier = "==3.0.1" }, { name = "python-daemon", specifier = "==3.0.1" },
{ name = "python-dateutil", specifier = "==2.8.2" }, { 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-nmap", specifier = "==0.7.1" },
{ name = "python-redis-lock", specifier = "==4.0.0" }, { name = "python-redis-lock", specifier = "==4.0.0" },
{ name = "python3-saml", specifier = "==1.16.0" }, { name = "python3-saml", specifier = "==1.16.0" },
@@ -4211,13 +4211,13 @@ wheels = [
[[package]] [[package]]
name = "python-ldap" name = "python-ldap"
version = "3.4.3" version = "3.4.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pyasn1" }, { name = "pyasn1" },
{ name = "pyasn1-modules" }, { 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]] [[package]]
name = "python-nmap" name = "python-nmap"