Compare commits

..

12 Commits

Author SHA1 Message Date
github-actions[bot]
40369b5df3 perf: Update Dockerfile with new base image tag 2025-10-29 03:23:29 +00:00
wrd
f42a08152d Revert "perf: update fields serialization and bump django and djangorestframe…"
This reverts commit dd0cacb4bc.
2025-10-29 11:18:54 +08:00
ibuler
fab6219cea perf: branches auto cleanup 2025-10-29 10:10:21 +08:00
fit2bot
dd0cacb4bc perf: update fields serialization and bump django and djangorestframework versions (#16209)
Co-authored-by: wangruidong <940853815@qq.com>
2025-10-28 16:42:06 +08:00
ibuler
b8639601a1 perf: branches auto cleanup 2025-10-27 15:33:06 +08:00
老广
ab9882c9c1 perf: check api summary 2025-10-27 15:28:21 +08:00
ibuler
77a7b74b15 perf: print summary in the end 2025-10-27 15:26:04 +08:00
dependabot[bot]
4bc05865f1 chore(deps): bump python-ldap from 3.4.3 to 3.4.5
Bumps [python-ldap](https://github.com/python-ldap/python-ldap) from 3.4.3 to 3.4.5.
- [Release notes](https://github.com/python-ldap/python-ldap/releases)
- [Changelog](https://github.com/python-ldap/python-ldap/blob/python-ldap-3.4.5/CHANGES)
- [Commits](https://github.com/python-ldap/python-ldap/compare/python-ldap-3.4.3...python-ldap-3.4.5)

---
updated-dependencies:
- dependency-name: python-ldap
  dependency-version: 3.4.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 15:24:06 +08:00
fit2bot
bec9e4f3a7 perf: update deps kombu (#16133)
* perf: update deps kombu

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: Ewall555 <a03216@foxmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: wrd <940853815@qq.com>
2025-10-27 15:18:16 +08:00
fit2bot
359adf3dbb perf: add check api for common user 2025-10-27 14:54:02 +08:00
feng
ac54bb672c fix: Bulk account invalid secret_reset 2025-10-24 18:18:16 +08:00
ibuler
9e3ba00bc4 perf: search support keyword q=str to search 2025-10-24 10:22:49 +08:00
15 changed files with 275 additions and 41 deletions

View File

@@ -34,7 +34,7 @@ jobs:
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "dry_run=${{ github.event.inputs.dry_run }}" >> $GITHUB_OUTPUT echo "dry_run=${{ github.event.inputs.dry_run }}" >> $GITHUB_OUTPUT
else else
echo "dry_run=true" >> $GITHUB_OUTPUT echo "dry_run=false" >> $GITHUB_OUTPUT
fi fi
- name: Cleanup branches - name: Cleanup branches

View File

@@ -1,4 +1,4 @@
FROM jumpserver/core-base:20251014_095903 AS stage-build FROM jumpserver/core-base:20251029_031929 AS stage-build
ARG VERSION ARG VERSION

View File

@@ -25,7 +25,8 @@ class IntegrationApplicationViewSet(OrgBulkModelViewSet):
} }
rbac_perms = { rbac_perms = {
'get_once_secret': 'accounts.change_integrationapplication', 'get_once_secret': 'accounts.change_integrationapplication',
'get_account_secret': 'accounts.view_integrationapplication' 'get_account_secret': 'accounts.view_integrationapplication',
'get_sdks_info': 'accounts.view_integrationapplication'
} }
def read_file(self, path): def read_file(self, path):
@@ -36,7 +37,6 @@ class IntegrationApplicationViewSet(OrgBulkModelViewSet):
@action( @action(
['GET'], detail=False, url_path='sdks', ['GET'], detail=False, url_path='sdks',
permission_classes=[IsValidUser]
) )
def get_sdks_info(self, request, *args, **kwargs): def get_sdks_info(self, request, *args, **kwargs):
code_suffix_mapper = { code_suffix_mapper = {

View File

@@ -309,10 +309,10 @@ class AssetAccountBulkSerializer(
class Meta: class Meta:
model = Account model = Account
fields = [ fields = [
'name', 'username', 'secret', 'secret_type', 'passphrase', 'name', 'username', 'secret', 'secret_type', 'secret_reset',
'privileged', 'is_active', 'comment', 'template', 'passphrase', 'privileged', 'is_active', 'comment', 'template',
'on_invalid', 'push_now', 'params', 'assets', 'on_invalid', 'push_now', 'params', 'assets', 'su_from_username',
'su_from_username', 'source', 'source_id', 'source', 'source_id',
] ]
extra_kwargs = { extra_kwargs = {
'name': {'required': False}, 'name': {'required': False},

View File

@@ -113,7 +113,7 @@ class BaseAssetViewSet(OrgBulkModelViewSet):
("accounts", AccountSerializer), ("accounts", AccountSerializer),
) )
rbac_perms = ( rbac_perms = (
("match", "assets.match_asset"), ("match", "assets.view_asset"),
("platform", "assets.view_platform"), ("platform", "assets.view_platform"),
("gateways", "assets.view_gateway"), ("gateways", "assets.view_gateway"),
("accounts", "assets.view_account"), ("accounts", "assets.view_account"),

View File

@@ -43,7 +43,7 @@ class NodeViewSet(SuggestionMixin, OrgBulkModelViewSet):
search_fields = ('full_value',) search_fields = ('full_value',)
serializer_class = serializers.NodeSerializer serializer_class = serializers.NodeSerializer
rbac_perms = { rbac_perms = {
'match': 'assets.match_node', 'match': 'assets.view_node',
'check_assets_amount_task': 'assets.change_node' 'check_assets_amount_task': 'assets.change_node'
} }

View File

@@ -112,8 +112,10 @@ class PlatformProtocolViewSet(JMSModelViewSet):
class PlatformAutomationMethodsApi(generics.ListAPIView): class PlatformAutomationMethodsApi(generics.ListAPIView):
permission_classes = (IsValidUser,)
queryset = PlatformAutomation.objects.none() queryset = PlatformAutomation.objects.none()
rbac_perms = {
'list': 'assets.view_platform'
}
@staticmethod @staticmethod
def automation_methods(): def automation_methods():

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework.filters import SearchFilter as SearchFilterBase
import base64 import base64
import json import json
import logging import logging
@@ -35,6 +36,14 @@ __all__ = [
] ]
class SearchFilter(SearchFilterBase):
def get_search_terms(self, request):
params = request.query_params.get(self.search_param, '') or request.query_params.get('search', '')
params = params.replace('\x00', '') # strip null characters
params = params.replace(',', ' ')
return params.split()
class BaseFilterSet(drf_filters.FilterSet): class BaseFilterSet(drf_filters.FilterSet):
days = drf_filters.NumberFilter(method="filter_days") days = drf_filters.NumberFilter(method="filter_days")
days__lt = drf_filters.NumberFilter(method="filter_days") days__lt = drf_filters.NumberFilter(method="filter_days")

View File

@@ -1,9 +1,13 @@
import re import re
import uuid
import time
from django.conf import settings from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.test import Client from django.test import Client
from django.urls import URLPattern, URLResolver from django.urls import URLPattern, URLResolver
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from jumpserver.urls import api_v1 from jumpserver.urls import api_v1
@@ -85,50 +89,262 @@ known_error_urls = [
'/api/v1/terminal/sessions/00000000-0000-0000-0000-000000000000/replay/download/', '/api/v1/terminal/sessions/00000000-0000-0000-0000-000000000000/replay/download/',
] ]
# API 白名单 - 普通用户可以访问的 API
user_accessible_urls = known_unauth_urls + [
# 添加更多普通用户可以访问的 API
"/api/v1/settings/public/",
"/api/v1/users/profile/",
"/api/v1/users/change-password/",
"/api/v1/users/logout/",
"/api/v1/settings/chatai-prompts/",
"/api/v1/authentication/confirm/",
"/api/v1/users/connection-token/",
"/api/v1/authentication/temp-tokens/",
"/api/v1/notifications/backends/",
"/api/v1/authentication/passkeys/",
"/api/v1/orgs/orgs/current/",
"/api/v1/tickets/apply-asset-tickets/",
"/api/v1/ops/celery/task/00000000-0000-0000-0000-000000000000/task-execution/00000000-0000-0000-0000-000000000000/log/",
"/api/v1/assets/favorite-assets/",
"/api/v1/authentication/connection-token/",
"/api/v1/ops/jobs/",
"/api/v1/assets/categories/",
"/api/v1/tickets/tickets/",
"/api/v1/authentication/ssh-key/",
"/api/v1/terminal/my-sessions/",
"/api/v1/authentication/access-keys/",
"/api/v1/users/profile/permissions/",
"/api/v1/tickets/apply-login-asset-tickets/",
"/api/v1/resources/",
"/api/v1/ops/celery/task/00000000-0000-0000-0000-000000000000/task-execution/00000000-0000-0000-0000-000000000000/result/",
"/api/v1/notifications/site-messages/",
"/api/v1/notifications/site-messages/unread-total/",
"/api/v1/assets/assets/suggestions/",
"/api/v1/search/",
"/api/v1/notifications/user-msg-subscription/",
"/api/v1/ops/ansible/job-execution/00000000-0000-0000-0000-000000000000/log/",
"/api/v1/tickets/apply-login-tickets/",
"/api/v1/ops/variables/form-data/",
"/api/v1/ops/variables/help/",
"/api/v1/users/profile/password/",
"/api/v1/tickets/apply-command-tickets/",
"/api/v1/ops/job-executions/",
"/api/v1/audits/my-login-logs/",
"/api/v1/terminal/components/connect-methods/"
"/api/v1/ops/task-executions/",
"/api/v1/terminal/sessions/online-info/",
"/api/v1/ops/adhocs/",
"/api/v1/tickets/apply-nodes/suggestions/",
"/api/v1/tickets/apply-assets/suggestions/",
"/api/v1/settings/server-info/",
"/api/v1/ops/playbooks/",
"/api/v1/assets/categories/types/",
"/api/v1/assets/protocols/",
"/api/v1/common/countries/",
"/api/v1/audits/jobs/",
"/api/v1/terminal/components/connect-methods/",
"/api/v1/ops/task-executions/",
]
errors = {} errors = {}
class Command(BaseCommand): class Command(BaseCommand):
help = 'Check api if unauthorized' """
Check API authorization and user access permissions.
def handle(self, *args, **options): This command performs two types of checks:
settings.LOG_LEVEL = 'ERROR' 1. Anonymous access check - finds APIs that can be accessed without authentication
urls = get_api_urls() 2. User access check - finds APIs that can be accessed by a normal user
client = Client()
client.defaults['HTTP_HOST'] = 'localhost' The functionality is split into two methods:
- check_anonymous_access(): Checks for APIs accessible without authentication
- check_user_access(): Checks for APIs accessible by a normal user
Usage examples:
# Check both anonymous and user access (default behavior)
python manage.py check_api
# Check only anonymous access
python manage.py check_api --skip-user-check
# Check only user access
python manage.py check_api --skip-anonymous-check
# Check user access and update whitelist
python manage.py check_api --update-whitelist
"""
help = 'Check API authorization and user access permissions'
password = uuid.uuid4().hex
unauth_urls = [] unauth_urls = []
error_urls = [] error_urls = []
unformat_urls = [] unformat_urls = []
# 用户可以访问的 API但不在白名单中的 API
unexpected_access = []
def add_arguments(self, parser):
parser.add_argument(
'--skip-anonymous-check',
action='store_true',
help='Skip anonymous access check (only check user access)',
)
parser.add_argument(
'--skip-user-check',
action='store_true',
help='Skip user access check (only check anonymous access)',
)
parser.add_argument(
'--update-whitelist',
action='store_true',
help='Update the user accessible URLs whitelist based on current scan results',
)
def create_test_user(self):
"""创建测试用户"""
User = get_user_model()
username = 'test_user_api_check'
email = 'test@example.com'
# 删除可能存在的测试用户
User.objects.filter(username=username).delete()
# 创建新的测试用户
user = User.objects.create_user(
username=username,
email=email,
password=self.password,
is_active=True
)
return user
def check_user_api_access(self, urls):
"""检查普通用户可以访问的 API"""
user = self.create_test_user()
client = Client()
client.defaults['HTTP_HOST'] = 'localhost'
# 登录用户
login_success = client.login(username=user.username, password=self.password)
if not login_success:
self.stdout.write(
self.style.ERROR('Failed to login test user')
)
return [], []
accessible_urls = []
error_urls = []
self.stdout.write('Checking user API access...')
for url, ourl in urls: for url, ourl in urls:
if '(' in url or '<' in url: if '(' in url or '<' in url:
unformat_urls.append([url, ourl]) continue
try:
response = client.get(url, follow=True)
time.sleep(0.1)
# 如果状态码是 200 或 201说明用户可以访问
if response.status_code in [200, 201]:
accessible_urls.append((url, ourl, response.status_code))
elif response.status_code == 403:
# 403 表示权限不足,这是正常的
pass
else:
# 其他状态码可能是错误
error_urls.append((url, ourl, response.status_code))
except Exception as e:
error_urls.append((url, ourl, str(e)))
# 清理测试用户
user.delete()
return accessible_urls, error_urls
def check_anonymous_access(self, urls):
"""检查匿名访问权限"""
client = Client()
client.defaults['HTTP_HOST'] = 'localhost'
for url, ourl in urls:
if '(' in url or '<' in url:
self.unformat_urls.append([url, ourl])
continue continue
try: try:
response = client.get(url, follow=True) response = client.get(url, follow=True)
if response.status_code != 401: if response.status_code != 401:
errors[url] = str(response.status_code) + ' ' + str(ourl) errors[url] = str(response.status_code) + ' ' + str(ourl)
unauth_urls.append(url) self.unauth_urls.append(url)
except Exception as e: except Exception as e:
errors[url] = str(e) errors[url] = str(e)
error_urls.append(url) self.error_urls.append(url)
unauth_urls = set(unauth_urls) - set(known_unauth_urls) self.unauth_urls = set(self.unauth_urls) - set(known_unauth_urls)
print("\nUnauthorized urls:") self.error_urls = set(self.error_urls)
if not unauth_urls: self.unformat_urls = set(self.unformat_urls)
def print_anonymous_access_result(self):
print("\n=== Anonymous Access Check ===")
print("Unauthorized urls:")
if not self.unauth_urls:
print(" Empty, very good!") print(" Empty, very good!")
for url in unauth_urls: for url in self.unauth_urls:
print('"{}", {}'.format(url, errors.get(url, ''))) print('"{}", {}'.format(url, errors.get(url, '')))
print("\nError urls:") print("\nError urls:")
if not error_urls: if not self.error_urls:
print(" Empty, very good!") print(" Empty, very good!")
for url in set(error_urls): for url in set(self.error_urls):
print(url, ': ' + errors.get(url)) print(url, ': ' + errors.get(url))
print("\nUnformat urls:") print("\nUnformat urls:")
if not unformat_urls: if not self.unformat_urls:
print(" Empty, very good!") print(" Empty, very good!")
for url in unformat_urls: for url in self.unformat_urls:
print(url) print(url)
def check_user_access(self, urls, update_whitelist=False):
"""检查用户访问权限"""
print("\n=== User Access Check ===")
accessible_urls, user_error_urls = self.check_user_api_access(urls)
# 检查是否有不在白名单中的可访问 API
accessible_url_list = [url for url, _, _ in accessible_urls]
unexpected_access = set(accessible_url_list) - set(user_accessible_urls)
self.unexpected_access = unexpected_access
# 如果启用了更新白名单选项
if update_whitelist:
print("\n=== Updating Whitelist ===")
new_whitelist = sorted(set(user_accessible_urls + accessible_url_list))
print("Updated whitelist would include:")
for url in new_whitelist:
print(f' "{url}",')
print(f"\nTotal URLs in whitelist: {len(new_whitelist)}")
def print_user_access_result(self):
print("\n=== User Access Check ===")
print("User unexpected urls:")
if self.unexpected_access:
print(f" Error: Found {len(self.unexpected_access)} URLs accessible by user but not in whitelist:")
for url in self.unexpected_access:
print(f' "{url}"')
else:
print(" Empty, very good!")
def handle(self, *args, **options):
settings.LOG_LEVEL = 'ERROR'
urls = get_api_urls()
# 检查匿名访问权限(默认执行)
if not options['skip_anonymous_check']:
self.check_anonymous_access(urls)
# 检查用户访问权限(默认执行)
if not options['skip_user_check']:
self.check_user_access(urls, options['update_whitelist'])
print("\nCheck total urls: ", len(urls))
self.print_anonymous_access_result()
self.print_user_access_result()

View File

@@ -732,6 +732,9 @@ class Config(dict):
# Suggestion api # Suggestion api
'SUGGESTION_LIMIT': 10, 'SUGGESTION_LIMIT': 10,
# MCP
'MCP_ENABLED': False,
} }
old_config_map = { old_config_map = {

View File

@@ -268,3 +268,4 @@ LOKI_BASE_URL = CONFIG.LOKI_BASE_URL
TOOL_USER_ENABLED = CONFIG.TOOL_USER_ENABLED TOOL_USER_ENABLED = CONFIG.TOOL_USER_ENABLED
SUGGESTION_LIMIT = CONFIG.SUGGESTION_LIMIT SUGGESTION_LIMIT = CONFIG.SUGGESTION_LIMIT
MCP_ENABLED = CONFIG.MCP_ENABLED

View File

@@ -38,12 +38,12 @@ REST_FRAMEWORK = {
), ),
'DEFAULT_FILTER_BACKENDS': ( 'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend', 'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter', 'common.drf.filters.SearchFilter',
'common.drf.filters.RewriteOrderingFilter', 'common.drf.filters.RewriteOrderingFilter',
), ),
'DEFAULT_METADATA_CLASS': 'common.drf.metadata.SimpleMetadataWithFilters', 'DEFAULT_METADATA_CLASS': 'common.drf.metadata.SimpleMetadataWithFilters',
'ORDERING_PARAM': "order", 'ORDERING_PARAM': "order",
'SEARCH_PARAM': "search", 'SEARCH_PARAM': "q",
'DATETIME_FORMAT': '%Y/%m/%d %H:%M:%S %z', 'DATETIME_FORMAT': '%Y/%m/%d %H:%M:%S %z',
'DATETIME_INPUT_FORMATS': ['%Y/%m/%d %H:%M:%S %z', 'iso-8601', '%Y-%m-%d %H:%M:%S %z'], 'DATETIME_INPUT_FORMATS': ['%Y/%m/%d %H:%M:%S %z', 'iso-8601', '%Y-%m-%d %H:%M:%S %z'],
'DEFAULT_PAGINATION_CLASS': 'jumpserver.rewriting.pagination.MaxLimitOffsetPagination', 'DEFAULT_PAGINATION_CLASS': 'jumpserver.rewriting.pagination.MaxLimitOffsetPagination',

View File

@@ -35,11 +35,14 @@ resource_api = [
api_v1 = resource_api + [ api_v1 = resource_api + [
path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()), path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()),
path('search/', api.GlobalSearchView.as_view()),
]
if settings.MCP_ENABLED:
api_v1.extend([
path('resources/', api.ResourceTypeListApi.as_view(), name='resource-list'), path('resources/', api.ResourceTypeListApi.as_view(), name='resource-list'),
path('resources/<str:resource>/', api.ResourceListApi.as_view()), path('resources/<str:resource>/', api.ResourceListApi.as_view()),
path('resources/<str:resource>/<str:pk>/', api.ResourceDetailApi.as_view()), path('resources/<str:resource>/<str:pk>/', api.ResourceDetailApi.as_view()),
path('search/', api.GlobalSearchView.as_view()), ])
]
app_view_patterns = [ app_view_patterns = [
path('auth/', include('authentication.urls.view_urls'), name='auth'), path('auth/', include('authentication.urls.view_urls'), name='auth'),

View File

@@ -93,10 +93,10 @@ dependencies = [
'celery==5.3.1', 'celery==5.3.1',
'flower==2.0.1', 'flower==2.0.1',
'django-celery-beat==2.6.0', 'django-celery-beat==2.6.0',
'kombu==5.3.1', 'kombu==5.3.5',
'uvicorn==0.22.0', 'uvicorn==0.22.0',
'websockets==11.0.3', 'websockets==11.0.3',
'python-ldap==3.4.3', 'python-ldap==3.4.5',
'ldap3==2.9.1', 'ldap3==2.9.1',
'django-radius', 'django-radius',
'django-cas-ng', 'django-cas-ng',

10
uv.lock generated
View File

@@ -1,5 +1,5 @@
version = 1 version = 1
revision = 2 revision = 3
requires-python = ">=3.11" requires-python = ">=3.11"
resolution-markers = [ resolution-markers = [
"python_full_version >= '3.14'", "python_full_version >= '3.14'",
@@ -2553,7 +2553,7 @@ requires-dist = [
{ name = "itypes", specifier = "==1.2.0" }, { name = "itypes", specifier = "==1.2.0" },
{ name = "jinja2", specifier = "==3.1.6" }, { name = "jinja2", specifier = "==3.1.6" },
{ name = "jsonfield2", specifier = "==4.0.0.post0" }, { name = "jsonfield2", specifier = "==4.0.0.post0" },
{ name = "kombu", specifier = "==5.3.1" }, { name = "kombu", specifier = "==5.3.5" },
{ name = "ldap3", specifier = "==2.9.1" }, { name = "ldap3", specifier = "==2.9.1" },
{ name = "lxml", specifier = "==5.2.1" }, { name = "lxml", specifier = "==5.2.1" },
{ name = "markupsafe", specifier = "==2.1.3" }, { name = "markupsafe", specifier = "==2.1.3" },
@@ -2671,15 +2671,15 @@ wheels = [
[[package]] [[package]]
name = "kombu" name = "kombu"
version = "5.3.1" version = "5.3.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "amqp" }, { name = "amqp" },
{ name = "vine" }, { name = "vine" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c8/69/b703f8ec8d0406be22534dad885cac847fe092b793c4893034e3308feb9b/kombu-5.3.1.tar.gz", hash = "sha256:fbd7572d92c0bf71c112a6b45163153dea5a7b6a701ec16b568c27d0fd2370f2", size = 434284, upload-time = "2023-06-15T13:16:22.683Z" } sdist = { url = "https://files.pythonhosted.org/packages/55/61/0b91085837d446570ea12f63f79463e5a74b449956b1ca9d1946a6f584c2/kombu-5.3.5.tar.gz", hash = "sha256:30e470f1a6b49c70dc6f6d13c3e4cc4e178aa6c469ceb6bcd55645385fc84b93", size = 438460, upload-time = "2024-01-12T19:55:54.982Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/63/58/b23b9c1ffb30d8b5cdfc7bdecb17bfd7ea20c619e86e515297b496177144/kombu-5.3.1-py3-none-any.whl", hash = "sha256:48ee589e8833126fd01ceaa08f8a2041334e9f5894e5763c8486a550454551e9", size = 198498, upload-time = "2023-06-15T13:16:14.57Z" }, { url = "https://files.pythonhosted.org/packages/f7/88/daca086d72832c74a7e239558ad484644c8cda0b9ae8a690f247bf13c268/kombu-5.3.5-py3-none-any.whl", hash = "sha256:0eac1bbb464afe6fb0924b21bf79460416d25d8abc52546d4f16cad94f789488", size = 200001, upload-time = "2024-01-12T19:55:51.59Z" },
] ]
[[package]] [[package]]