mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-04-27 11:12:54 +00:00
perf: change mcp integrate
This commit is contained in:
parent
dc6308b030
commit
471053e62a
@ -34,10 +34,14 @@ class SimpleMetadataWithFilters(SimpleMetadata):
|
||||
"""
|
||||
actions = {}
|
||||
view.raw_action = getattr(view, "action", None)
|
||||
query_action = request.query_params.get("action", None)
|
||||
for method in self.methods & set(view.allowed_methods):
|
||||
if hasattr(view, "action_map"):
|
||||
view.action = view.action_map.get(method.lower(), view.action)
|
||||
|
||||
if query_action and query_action.lower() != method.lower():
|
||||
continue
|
||||
|
||||
view.request = clone_request(request, method)
|
||||
try:
|
||||
# Test global permissions
|
||||
|
3
apps/jumpserver/api/__init__.py
Normal file
3
apps/jumpserver/api/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .aggregate import *
|
||||
from .dashboard import IndexApi
|
||||
from .health import PrometheusMetricsApi, HealthCheckView
|
9
apps/jumpserver/api/aggregate/__init__.py
Normal file
9
apps/jumpserver/api/aggregate/__init__.py
Normal file
@ -0,0 +1,9 @@
|
||||
from .detail import ResourceDetailApi
|
||||
from .list import ResourceListApi
|
||||
from .supported import ResourceTypeListApi
|
||||
|
||||
__all__ = [
|
||||
'ResourceListApi',
|
||||
'ResourceDetailApi',
|
||||
'ResourceTypeListApi',
|
||||
]
|
57
apps/jumpserver/api/aggregate/const.py
Normal file
57
apps/jumpserver/api/aggregate/const.py
Normal file
@ -0,0 +1,57 @@
|
||||
list_params = [
|
||||
{
|
||||
"name": "search",
|
||||
"in": "query",
|
||||
"description": "A search term.",
|
||||
"required": False,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "order",
|
||||
"in": "query",
|
||||
"description": "Which field to use when ordering the results.",
|
||||
"required": False,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"description": "Number of results to return per page. Default is 10.",
|
||||
"required": False,
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "offset",
|
||||
"in": "query",
|
||||
"description": "The initial index from which to return the results.",
|
||||
"required": False,
|
||||
"type": "integer"
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
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.
|
||||
""",
|
||||
"required": True,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "X-JMS-ORG",
|
||||
"in": "header",
|
||||
"description": "The organization ID to use for the request. Organization is the namespace for resources, if not set, use default org",
|
||||
"required": False,
|
||||
"type": "string"
|
||||
}
|
||||
]
|
73
apps/jumpserver/api/aggregate/detail.py
Normal file
73
apps/jumpserver/api/aggregate/detail.py
Normal file
@ -0,0 +1,73 @@
|
||||
# views.py
|
||||
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .const import common_params
|
||||
from .proxy import ProxyMixin
|
||||
from .utils import param_dic_to_param
|
||||
|
||||
one_param = [
|
||||
{
|
||||
'name': 'id',
|
||||
'in': 'path',
|
||||
'required': True,
|
||||
'description': 'Resource ID',
|
||||
'type': 'string',
|
||||
}
|
||||
]
|
||||
|
||||
object_params = [
|
||||
param_dic_to_param(d)
|
||||
for d in common_params + one_param
|
||||
]
|
||||
|
||||
|
||||
class ResourceDetailApi(ProxyMixin, APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_id="get_resource_detail",
|
||||
operation_summary="Get resource detail",
|
||||
manual_parameters=object_params,
|
||||
operation_description="""
|
||||
Get resource detail.
|
||||
{resource} is the resource name, GET /api/v1/resources/ to get full supported resource.
|
||||
|
||||
""", )
|
||||
def get(self, request, resource, pk=None):
|
||||
return self._proxy(request, resource, pk=pk, action='retrieve')
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_id="delete_resource",
|
||||
operation_summary="Delete the resource ",
|
||||
manual_parameters=object_params,
|
||||
operation_description="Delete the resource, and can not be restored",
|
||||
)
|
||||
def delete(self, request, resource, pk=None):
|
||||
return self._proxy(request, resource, pk, action='destroy')
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_id="update_resource",
|
||||
operation_summary="Update the resource property",
|
||||
manual_parameters=object_params,
|
||||
operation_description="""
|
||||
Update the resource property, all property will be update,
|
||||
{resource} is the resource name, GET /api/v1/resources/ to get full supported resource.
|
||||
|
||||
OPTION /api/v1/resources/{resource}/{id}/?action=put to get field type and helptext.
|
||||
""")
|
||||
def put(self, request, resource, pk=None):
|
||||
return self._proxy(request, resource, pk, action='update')
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_id="partial_update_resource",
|
||||
operation_summary="Update the resource property",
|
||||
manual_parameters=object_params,
|
||||
operation_description="""
|
||||
Partial update the resource property, only request property will be update,
|
||||
OPTION /api/v1/resources/{resource}/{id}/?action=patch to get field type and helptext.
|
||||
""")
|
||||
def patch(self, request, resource, pk=None):
|
||||
return self._proxy(request, resource, pk, action='partial_update')
|
86
apps/jumpserver/api/aggregate/list.py
Normal file
86
apps/jumpserver/api/aggregate/list.py
Normal file
@ -0,0 +1,86 @@
|
||||
# views.py
|
||||
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .const import list_params, common_params
|
||||
from .proxy import ProxyMixin
|
||||
from .utils import param_dic_to_param
|
||||
|
||||
router = DefaultRouter()
|
||||
|
||||
BASE_URL = "http://localhost:8080"
|
||||
|
||||
list_params = [
|
||||
param_dic_to_param(d)
|
||||
for d in list_params + common_params
|
||||
]
|
||||
|
||||
create_params = [
|
||||
param_dic_to_param(d)
|
||||
for d in common_params
|
||||
]
|
||||
|
||||
list_schema = {
|
||||
"required": [
|
||||
"count",
|
||||
"results"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"next": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"x-nullable": True
|
||||
},
|
||||
"previous": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"x-nullable": True
|
||||
},
|
||||
"results": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list_response = openapi.Response("Resource list response", schema=openapi.Schema(**list_schema))
|
||||
|
||||
|
||||
class ResourceListApi(ProxyMixin, APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_id="get_resource_list",
|
||||
operation_summary="Get resource list",
|
||||
manual_parameters=list_params,
|
||||
responses={200: list_response},
|
||||
operation_description="""
|
||||
Get resource list, you should set the resource name in the url.
|
||||
OPTIONS /api/v1/resources/{resource}/?action=get to get every type resource's field type and help text.
|
||||
""", )
|
||||
# ↓↓↓ Swagger 自动文档 ↓↓↓
|
||||
def get(self, request, resource):
|
||||
return self._proxy(request, resource)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_id="create_resource_by_type",
|
||||
operation_summary="Create resource",
|
||||
manual_parameters=create_params,
|
||||
operation_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.
|
||||
""")
|
||||
def post(self, request, resource, pk=None):
|
||||
if not resource:
|
||||
resource = request.data.pop('resource', '')
|
||||
return self._proxy(request, resource, pk, action='create')
|
||||
|
||||
def options(self, request, resource, pk=None):
|
||||
return self._proxy(request, resource, pk, action='metadata')
|
68
apps/jumpserver/api/aggregate/proxy.py
Normal file
68
apps/jumpserver/api/aggregate/proxy.py
Normal file
@ -0,0 +1,68 @@
|
||||
# views.py
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import requests
|
||||
from rest_framework.exceptions import NotFound, APIException
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .utils import get_full_resource_map
|
||||
|
||||
router = DefaultRouter()
|
||||
|
||||
BASE_URL = "http://localhost:8080"
|
||||
|
||||
|
||||
class ProxyMixin(APIView):
|
||||
"""
|
||||
通用资源代理 API,支持动态路径、自动文档生成
|
||||
"""
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def _build_url(self, resource_name: str, pk: str = None, query_params=None):
|
||||
resource_map = get_full_resource_map()
|
||||
resource = resource_map.get(resource_name)
|
||||
if not resource:
|
||||
raise NotFound(f"Unknown resource: {resource_name}")
|
||||
|
||||
base_path = resource['path']
|
||||
if pk:
|
||||
base_path += f"{pk}/"
|
||||
|
||||
if query_params:
|
||||
base_path += f"?{urlencode(query_params)}"
|
||||
|
||||
return f"{BASE_URL}{base_path}"
|
||||
|
||||
def _proxy(self, request, resource: str, pk: str = None, action='list'):
|
||||
method = request.method.lower()
|
||||
if method not in ['get', 'post', 'put', 'patch', 'delete', 'options']:
|
||||
raise APIException("Unsupported method")
|
||||
|
||||
if not resource or resource == '{resource}':
|
||||
if request.data:
|
||||
resource = request.data.get('resource')
|
||||
|
||||
query_params = request.query_params.dict()
|
||||
if action == 'list':
|
||||
query_params['limit'] = 10
|
||||
|
||||
url = self._build_url(resource, pk, query_params)
|
||||
headers = {k: v for k, v in request.headers.items() if k.lower() != 'host'}
|
||||
cookies = request.COOKIES
|
||||
body = request.body if method in ['post', 'put', 'patch'] else None
|
||||
|
||||
try:
|
||||
resp = requests.request(
|
||||
method=method,
|
||||
url=url,
|
||||
headers=headers,
|
||||
cookies=cookies,
|
||||
data=body,
|
||||
timeout=10,
|
||||
)
|
||||
return resp
|
||||
except requests.RequestException as e:
|
||||
raise APIException(f"Proxy request failed: {str(e)}")
|
43
apps/jumpserver/api/aggregate/supported.py
Normal file
43
apps/jumpserver/api/aggregate/supported.py
Normal file
@ -0,0 +1,43 @@
|
||||
# views.py
|
||||
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import serializers
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework.views import APIView
|
||||
|
||||
router = DefaultRouter()
|
||||
|
||||
BASE_URL = "http://localhost:8080"
|
||||
|
||||
|
||||
class ResourceTypeResourceSerializer(serializers.Serializer):
|
||||
name = serializers.CharField()
|
||||
path = serializers.CharField()
|
||||
app = serializers.CharField()
|
||||
verbose_name = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
|
||||
|
||||
class ResourceTypeListApi(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_id="get_supported_resources",
|
||||
operation_summary="Get-all-support-resources",
|
||||
operation_description="Get all support resources, name, path, verbose_name description",
|
||||
responses={200: ResourceTypeResourceSerializer(many=True)}, # Specify the response serializer
|
||||
)
|
||||
def get(self, request):
|
||||
result = []
|
||||
resource_map = get_full_resource_map()
|
||||
for name, desc in resource_map.items():
|
||||
desc = resource_map.get(name, {})
|
||||
resource = {
|
||||
"name": name,
|
||||
**desc,
|
||||
"path": f'/api/v1/resources/{name}/',
|
||||
}
|
||||
result.append(resource)
|
||||
return Response(result)
|
128
apps/jumpserver/api/aggregate/utils.py
Normal file
128
apps/jumpserver/api/aggregate/utils.py
Normal file
@ -0,0 +1,128 @@
|
||||
# views.py
|
||||
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from typing import Dict
|
||||
|
||||
from django.urls import URLPattern
|
||||
from django.urls import URLResolver
|
||||
from drf_yasg import openapi
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
router = DefaultRouter()
|
||||
|
||||
BASE_URL = "http://localhost:8080"
|
||||
|
||||
|
||||
def clean_path(path: str) -> str:
|
||||
"""
|
||||
清理掉 DRF 自动生成的正则格式内容,让其变成普通 RESTful URL path。
|
||||
"""
|
||||
|
||||
# 去掉格式后缀匹配: \.(?P<format>xxx)
|
||||
path = re.sub(r'\\\.\(\?P<format>[^)]+\)', '', path)
|
||||
|
||||
# 去掉括号式格式匹配
|
||||
path = re.sub(r'\(\?P<format>[^)]+\)', '', path)
|
||||
|
||||
# 移除 DRF 中正则参数的部分 (?P<param>pattern)
|
||||
path = re.sub(r'\(\?P<\w+>[^)]+\)', '{param}', path)
|
||||
|
||||
# 如果有多个括号包裹的正则(比如前缀路径),去掉可选部分包装
|
||||
path = re.sub(r'\(\(([^)]+)\)\?\)', r'\1', path) # ((...))? => ...
|
||||
|
||||
# 去掉中间和两边的 ^ 和 $
|
||||
path = path.replace('^', '').replace('$', '')
|
||||
|
||||
# 去掉尾部 ?/
|
||||
path = re.sub(r'\?/?$', '', path)
|
||||
|
||||
# 去掉反斜杠
|
||||
path = path.replace('\\', '')
|
||||
|
||||
# 替换多重斜杠
|
||||
path = re.sub(r'/+', '/', path)
|
||||
|
||||
# 添加开头斜杠,移除多余空格
|
||||
path = path.strip()
|
||||
if not path.startswith('/'):
|
||||
path = '/' + path
|
||||
if not path.endswith('/'):
|
||||
path += '/'
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def extract_resource_paths(urlpatterns, prefix='/api/v1/') -> Dict[str, Dict[str, str]]:
|
||||
resource_map = {}
|
||||
|
||||
for pattern in urlpatterns:
|
||||
if isinstance(pattern, URLResolver):
|
||||
nested_prefix = prefix + str(pattern.pattern)
|
||||
resource_map.update(extract_resource_paths(pattern.url_patterns, nested_prefix))
|
||||
|
||||
elif isinstance(pattern, URLPattern):
|
||||
callback = pattern.callback
|
||||
actions = getattr(callback, 'actions', {})
|
||||
if not actions:
|
||||
continue
|
||||
|
||||
if 'get' in actions and actions['get'] == 'list':
|
||||
path = clean_path(prefix + str(pattern.pattern))
|
||||
|
||||
# 尝试获取资源名称
|
||||
name = pattern.name
|
||||
if name and name.endswith('-list'):
|
||||
resource = name[:-5]
|
||||
else:
|
||||
resource = path.strip('/').split('/')[-1]
|
||||
|
||||
# 不强行加 s,资源名保持原状即可
|
||||
resource = resource if resource.endswith('s') else resource + 's'
|
||||
|
||||
# 获取 View 类和 model 的 verbose_name
|
||||
view_cls = getattr(callback, 'cls', None)
|
||||
model = None
|
||||
|
||||
if view_cls:
|
||||
queryset = getattr(view_cls, 'queryset', None)
|
||||
if queryset is not None:
|
||||
model = getattr(queryset, 'model', None)
|
||||
else:
|
||||
# 有些 View 用 get_queryset()
|
||||
try:
|
||||
instance = view_cls()
|
||||
qs = instance.get_queryset()
|
||||
model = getattr(qs, 'model', None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not model:
|
||||
continue
|
||||
|
||||
app = str(getattr(model._meta, 'app_label', ''))
|
||||
verbose_name = str(getattr(model._meta, 'verbose_name', ''))
|
||||
resource_map[resource] = {
|
||||
'path': path,
|
||||
'app': app,
|
||||
'verbose_name': verbose_name,
|
||||
'description': model.__doc__.__str__()
|
||||
}
|
||||
|
||||
print("Extracted resource paths:", list(resource_map.keys()))
|
||||
return resource_map
|
||||
|
||||
|
||||
def param_dic_to_param(d):
|
||||
return openapi.Parameter(
|
||||
d['name'], d['in'],
|
||||
description=d['description'], type=d['type'], required=d.get('required', False)
|
||||
)
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_full_resource_map():
|
||||
from apps.jumpserver.urls import resource_api
|
||||
resource_map = extract_resource_paths(resource_api)
|
||||
print("Building URL for resource:", resource_map)
|
||||
return resource_map
|
@ -1,15 +1,11 @@
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Count, Max, F, CharField
|
||||
from django.db.models.functions import Cast
|
||||
from django.http.response import JsonResponse, HttpResponse
|
||||
from django.http.response import JsonResponse
|
||||
from django.utils import timezone
|
||||
from django.utils.timesince import timesince
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from assets.const import AllTypes
|
||||
@ -25,8 +21,6 @@ from orgs.caches import OrgResourceStatisticsCache
|
||||
from orgs.utils import current_org
|
||||
from terminal.const import RiskLevelChoices
|
||||
from terminal.models import Session, Command
|
||||
from terminal.utils import ComponentsPrometheusMetricsUtil
|
||||
from users.models import User
|
||||
|
||||
__all__ = ['IndexApi']
|
||||
|
||||
@ -466,61 +460,3 @@ class IndexApi(DateTimeMixin, DatesLoginMetricMixin, APIView):
|
||||
})
|
||||
|
||||
return JsonResponse(data, status=200)
|
||||
|
||||
|
||||
class HealthApiMixin(APIView):
|
||||
pass
|
||||
|
||||
|
||||
class HealthCheckView(HealthApiMixin):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
@staticmethod
|
||||
def get_db_status():
|
||||
t1 = time.time()
|
||||
try:
|
||||
ok = User.objects.first() is not None
|
||||
t2 = time.time()
|
||||
return ok, t2 - t1
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
@staticmethod
|
||||
def get_redis_status():
|
||||
key = 'HEALTH_CHECK'
|
||||
|
||||
t1 = time.time()
|
||||
try:
|
||||
value = '1'
|
||||
cache.set(key, '1', 10)
|
||||
got = cache.get(key)
|
||||
t2 = time.time()
|
||||
|
||||
if value == got:
|
||||
return True, t2 - t1
|
||||
return False, 'Value not match'
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
def get(self, request):
|
||||
redis_status, redis_time = self.get_redis_status()
|
||||
db_status, db_time = self.get_db_status()
|
||||
status = all([redis_status, db_status])
|
||||
data = {
|
||||
'status': status,
|
||||
'db_status': db_status,
|
||||
'db_time': db_time,
|
||||
'redis_status': redis_status,
|
||||
'redis_time': redis_time,
|
||||
'time': int(time.time()),
|
||||
}
|
||||
return Response(data)
|
||||
|
||||
|
||||
class PrometheusMetricsApi(HealthApiMixin):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
util = ComponentsPrometheusMetricsUtil()
|
||||
metrics_text = util.get_prometheus_metrics_text()
|
||||
return HttpResponse(metrics_text, content_type='text/plain; version=0.0.4; charset=utf-8')
|
68
apps/jumpserver/api/health.py
Normal file
68
apps/jumpserver/api/health.py
Normal file
@ -0,0 +1,68 @@
|
||||
import time
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.http.response import HttpResponse
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from terminal.utils import ComponentsPrometheusMetricsUtil
|
||||
from users.models import User
|
||||
|
||||
|
||||
class HealthApiMixin(APIView):
|
||||
pass
|
||||
|
||||
|
||||
class HealthCheckView(HealthApiMixin):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
@staticmethod
|
||||
def get_db_status():
|
||||
t1 = time.time()
|
||||
try:
|
||||
ok = User.objects.first() is not None
|
||||
t2 = time.time()
|
||||
return ok, t2 - t1
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
@staticmethod
|
||||
def get_redis_status():
|
||||
key = 'HEALTH_CHECK'
|
||||
|
||||
t1 = time.time()
|
||||
try:
|
||||
value = '1'
|
||||
cache.set(key, '1', 10)
|
||||
got = cache.get(key)
|
||||
t2 = time.time()
|
||||
|
||||
if value == got:
|
||||
return True, t2 - t1
|
||||
return False, 'Value not match'
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
def get(self, request):
|
||||
redis_status, redis_time = self.get_redis_status()
|
||||
db_status, db_time = self.get_db_status()
|
||||
status = all([redis_status, db_status])
|
||||
data = {
|
||||
'status': status,
|
||||
'db_status': db_status,
|
||||
'db_time': db_time,
|
||||
'redis_status': redis_status,
|
||||
'redis_time': redis_time,
|
||||
'time': int(time.time()),
|
||||
}
|
||||
return Response(data)
|
||||
|
||||
|
||||
class PrometheusMetricsApi(HealthApiMixin):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
util = ComponentsPrometheusMetricsUtil()
|
||||
metrics_text = util.get_prometheus_metrics_text()
|
||||
return HttpResponse(metrics_text, content_type='text/plain; version=0.0.4; charset=utf-8')
|
@ -12,7 +12,7 @@ from django.views.i18n import JavaScriptCatalog
|
||||
|
||||
from . import views, api
|
||||
|
||||
api_v1 = [
|
||||
resource_api = [
|
||||
path('index/', api.IndexApi.as_view()),
|
||||
path('users/', include('users.urls.api_urls', namespace='api-users')),
|
||||
path('assets/', include('assets.urls.api_urls', namespace='api-assets')),
|
||||
@ -30,7 +30,13 @@ api_v1 = [
|
||||
path('notifications/', include('notifications.urls.api_urls', namespace='api-notifications')),
|
||||
path('rbac/', include('rbac.urls.api_urls', namespace='api-rbac')),
|
||||
path('labels/', include('labels.urls', namespace='api-label')),
|
||||
]
|
||||
|
||||
api_v1 = resource_api + [
|
||||
path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()),
|
||||
path('resources/', api.ResourceTypeListApi.as_view(), name='resource-list'),
|
||||
path('resources/<str:resource>/', api.ResourceListApi.as_view()),
|
||||
path('resources/<str:resource>/<str:pk>/', api.ResourceDetailApi.as_view()),
|
||||
]
|
||||
|
||||
app_view_patterns = [
|
||||
|
@ -19,41 +19,78 @@ class CustomSchemaGenerator(OpenAPISchemaGenerator):
|
||||
'/report/', '/render-to-json/', '/suggestions/',
|
||||
'executions', 'automations', 'change-secret-records',
|
||||
'change-secret-dashboard', '/copy-to-assets/',
|
||||
'/move-to-assets/', 'dashboard',
|
||||
|
||||
'/move-to-assets/', 'dashboard', 'index', 'countries',
|
||||
'/resources/cache/', 'profile/mfa', 'profile/password',
|
||||
'profile/permissions', 'prometheus', 'constraints'
|
||||
]
|
||||
for p in excludes:
|
||||
if path.find(p) >= 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def exclude_some_app(self, path):
|
||||
def exclude_some_app_model(self, path):
|
||||
parts = path.split('/')
|
||||
if len(parts) < 4:
|
||||
if len(parts) < 5:
|
||||
return False
|
||||
|
||||
apps = []
|
||||
if self.from_mcp:
|
||||
apps = [
|
||||
'ops', 'tickets', 'common', 'authentication',
|
||||
'settings', 'xpack', 'terminal', 'rbac'
|
||||
'ops', 'tickets', 'authentication',
|
||||
'settings', 'xpack', 'terminal', 'rbac',
|
||||
'notifications', 'promethues', 'acls'
|
||||
]
|
||||
|
||||
app_name = parts[3]
|
||||
if app_name in apps:
|
||||
return True
|
||||
models = []
|
||||
model = parts[4]
|
||||
if self.from_mcp:
|
||||
models = [
|
||||
'users', 'user-groups', 'users-groups-relations', 'assets', 'hosts', 'devices', 'databases',
|
||||
'webs', 'clouds', 'gpts', 'ds', 'customs', 'platforms', 'nodes', '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',
|
||||
'asset-permissions-users-relations', 'asset-permissions-user-groups-relations',
|
||||
'asset-permissions-assets-relations', 'asset-permissions-nodes-relations', 'terminal-status',
|
||||
'terminals', 'tasks', 'status', 'replay-storages', 'command-storages', 'session-sharing-records',
|
||||
'endpoints', 'endpoint-rules', 'applets', 'applet-hosts', 'applet-publications',
|
||||
'applet-host-deployments', 'virtual-apps', 'app-providers', 'virtual-app-publications',
|
||||
'celery-period-tasks', 'task-executions', 'adhocs', 'playbooks', 'variables', 'ftp-logs',
|
||||
'login-logs', 'operate-logs', 'password-change-logs', 'job-logs', 'jobs', 'user-sessions',
|
||||
'service-access-logs', 'chatai-prompts', 'super-connection-tokens', 'flows',
|
||||
'apply-assets', 'apply-nodes', 'login-acls', 'login-asset-acls', 'command-filter-acls',
|
||||
'command-groups', 'connect-method-acls', 'system-msg-subscriptions', 'roles', 'role-bindings',
|
||||
'system-roles', 'system-role-bindings', 'org-roles', 'org-role-bindings', 'content-types',
|
||||
'labeled-resources', 'account-backup-plans', 'account-check-engines', 'account-secrets',
|
||||
'change-secret', 'integration-applications', 'push-account', 'directories', 'connection-token',
|
||||
'groups', 'accounts', 'resource-types', 'favorite-assets', 'activities', 'platform-automation-methods',
|
||||
]
|
||||
if model in models:
|
||||
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):
|
||||
if self.exclude_some_app_model(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
|
||||
exclude_operations = [
|
||||
'orgs_orgs_read', 'orgs_orgs_update', 'orgs_orgs_delete', 'orgs_orgs_create',
|
||||
'orgs_orgs_partial_update',
|
||||
]
|
||||
if operation_id in exclude_operations:
|
||||
return None
|
||||
return operation
|
||||
|
||||
|
||||
@ -82,7 +119,8 @@ class CustomSwaggerAutoSchema(SwaggerAutoSchema):
|
||||
|
||||
def get_operation(self, operation_keys):
|
||||
operation = super().get_operation(operation_keys)
|
||||
operation.summary = operation.operation_id
|
||||
if not getattr(operation, 'summary', ''):
|
||||
operation.summary = operation.operation_id
|
||||
return operation
|
||||
|
||||
def get_filter_parameters(self):
|
||||
|
@ -10,6 +10,9 @@ __all__ = ['UserGroup']
|
||||
|
||||
|
||||
class UserGroup(LabeledMixin, JMSOrgBaseModel):
|
||||
"""
|
||||
User group, When a user is added to a group, they inherit its asset permissions for access control consistency.
|
||||
"""
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
|
||||
def __str__(self):
|
||||
|
@ -55,6 +55,11 @@ class User(
|
||||
JSONFilterMixin,
|
||||
AbstractUser,
|
||||
):
|
||||
"""
|
||||
User model, used for authentication and authorization. User can join multiple groups.
|
||||
User can have multiple roles, and each role can have multiple permissions.
|
||||
User can connect to multiple assets, If he has the permission. Permission was defined in Asset Permission.
|
||||
"""
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
username = models.CharField(max_length=128, unique=True, verbose_name=_("Username"))
|
||||
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
||||
|
Loading…
Reference in New Issue
Block a user