From 544b43fc8b71112190ad2dde8adbe41880a39c9f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 27 Apr 2026 14:30:46 +0000 Subject: [PATCH 1/9] perf: Update Dockerfile with new base image tag --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f49159375..b13986d0f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM jumpserver/core-base:20260424_143143 AS stage-build +FROM jumpserver/core-base:20260427_142026 AS stage-build ARG VERSION From 2c67e71cfba3b6c7a47ac0af0afdd661b2d99902 Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 28 Apr 2026 10:30:03 +0800 Subject: [PATCH 2/9] perf: update async local --- apps/common/local.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/apps/common/local.py b/apps/common/local.py index bec11e97d..3cdb7266e 100644 --- a/apps/common/local.py +++ b/apps/common/local.py @@ -10,10 +10,30 @@ class AsyncLocal: """ def __init__(self, context_var_name: str = "_async_local_storage"): - self._storage: contextvars.ContextVar[Dict[str, Any]] = contextvars.ContextVar( + object.__setattr__(self, "_storage", contextvars.ContextVar( context_var_name, default={} - ) + )) + + def __setattr__(self, key: str, value: Any) -> None: + if key.startswith("_"): + object.__setattr__(self, key, value) + return + self.set(key, value) + + def __getattr__(self, key: str) -> Any: + value = self.get(key, default=None) + if value is None: + raise AttributeError(f"{self.__class__.__name__!s} has no attribute {key!r}") + return value + + def __delattr__(self, key: str) -> None: + if key.startswith("_"): + object.__delattr__(self, key) + return + if key not in self._storage.get(): + raise AttributeError(f"{self.__class__.__name__!s} has no attribute {key!r}") + self.delete(key) def set(self, key: str, value: Any) -> None: current_data = self._storage.get().copy() From 48021bea50ff740a1343f9566a5ff21cadaa8e15 Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 28 Apr 2026 10:43:12 +0800 Subject: [PATCH 3/9] fix: many login logs are record when user login once --- apps/authentication/backends/base.py | 13 ++++++ apps/authentication/backends/cas/backends.py | 11 +++-- apps/authentication/backends/custom.py | 10 ----- .../backends/oauth2/backends.py | 18 +++----- apps/authentication/backends/oidc/backends.py | 41 ++++--------------- .../authentication/backends/saml2/backends.py | 18 +++----- apps/authentication/middleware.py | 5 ++- apps/authentication/mixins.py | 3 ++ apps/authentication/signal_handlers.py | 16 +------- apps/authentication/signals.py | 3 +- 10 files changed, 48 insertions(+), 90 deletions(-) diff --git a/apps/authentication/backends/base.py b/apps/authentication/backends/base.py index 31203c882..fd31f7ecb 100644 --- a/apps/authentication/backends/base.py +++ b/apps/authentication/backends/base.py @@ -3,6 +3,8 @@ from django.contrib.auth.backends import ModelBackend from common.utils import get_logger from users.models import User +from authentication.signals import backend_auth_failed +from authentication.errors import reason_choices, reason_user_invalid UserModel = get_user_model() logger = get_logger(__file__) @@ -65,3 +67,14 @@ class JMSBaseAuthBackend: class JMSModelBackend(JMSBaseAuthBackend, ModelBackend): def user_can_authenticate(self, user): return True + + +class RedirectAuthBackend(JMSBaseAuthBackend): + backend = None + + def send_backend_auth_failed_signal(self, request, username=None, reason=None): + default_reason = reason_choices.get(reason_user_invalid, reason) + backend_auth_failed.send( + sender=self.__class__, username=username, request=request, + reason=default_reason, backend=self.backend + ) diff --git a/apps/authentication/backends/cas/backends.py b/apps/authentication/backends/cas/backends.py index 65e38d4aa..9ce460e2d 100644 --- a/apps/authentication/backends/cas/backends.py +++ b/apps/authentication/backends/cas/backends.py @@ -5,13 +5,15 @@ from django.conf import settings from django_cas_ng.backends import CASBackend as _CASBackend from common.utils import get_logger -from ..base import JMSBaseAuthBackend +from ..base import RedirectAuthBackend __all__ = ['CASBackend'] logger = get_logger(__name__) -class CASBackend(JMSBaseAuthBackend, _CASBackend): +class CASBackend(RedirectAuthBackend, _CASBackend): + backend = settings.AUTH_BACKEND_CAS + @staticmethod def is_enabled(): return settings.AUTH_CAS @@ -19,4 +21,7 @@ class CASBackend(JMSBaseAuthBackend, _CASBackend): def authenticate(self, request, ticket, service): # 这里做个hack ,让父类始终走CAS_CREATE_USER=True的逻辑,然后调用 authentication/mixins.py 中的 custom_get_or_create 方法 settings.CAS_CREATE_USER = True - return super().authenticate(request, ticket, service) + user = super().authenticate(request, ticket, service) + if user is None: + self.send_backend_auth_failed_signal(request=request) + return user diff --git a/apps/authentication/backends/custom.py b/apps/authentication/backends/custom.py index 77109b1fd..9875249ce 100644 --- a/apps/authentication/backends/custom.py +++ b/apps/authentication/backends/custom.py @@ -3,7 +3,6 @@ from django.contrib.auth import get_user_model from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ -from authentication.signals import user_auth_failed, user_auth_success from common.utils import get_logger from .base import JMSBaseAuthBackend @@ -48,16 +47,7 @@ class CustomAuthBackend(JMSBaseAuthBackend): if self.user_can_authenticate(user): logger.info(f'Custom authenticate success: {user.username}') - user_auth_success.send( - sender=self.__class__, request=request, user=user, - backend=settings.AUTH_BACKEND_CUSTOM - ) return user else: logger.info(f'Custom authenticate failed: {user.username}') - user_auth_failed.send( - sender=self.__class__, request=request, username=user.username, - reason=_('User invalid, disabled or expired'), - backend=settings.AUTH_BACKEND_CUSTOM - ) return None diff --git a/apps/authentication/backends/oauth2/backends.py b/apps/authentication/backends/oauth2/backends.py index a6554c992..32e3e67bb 100644 --- a/apps/authentication/backends/oauth2/backends.py +++ b/apps/authentication/backends/oauth2/backends.py @@ -12,13 +12,12 @@ from django.urls import reverse from common.utils import get_logger from users.utils import construct_user_email from authentication.utils import build_absolute_uri -from authentication.signals import user_auth_failed, user_auth_success from common.exceptions import JMSException from .signals import ( oauth2_create_or_update_user ) -from ..base import JMSBaseAuthBackend +from ..base import RedirectAuthBackend __all__ = ['OAuth2Backend'] @@ -26,7 +25,9 @@ __all__ = ['OAuth2Backend'] logger = get_logger(__name__) -class OAuth2Backend(JMSBaseAuthBackend): +class OAuth2Backend(RedirectAuthBackend): + backend = settings.AUTH_BACKEND_OAUTH2 + @staticmethod def is_enabled(): return settings.AUTH_OAUTH2 @@ -144,18 +145,9 @@ class OAuth2Backend(JMSBaseAuthBackend): if self.user_can_authenticate(user): logger.debug(log_prompt.format('OAuth2 user login success')) - logger.debug(log_prompt.format('Send signal => oauth2 user login success')) - user_auth_success.send( - sender=self.__class__, request=request, user=user, - backend=settings.AUTH_BACKEND_OAUTH2 - ) return user else: logger.debug(log_prompt.format('OAuth2 user login failed')) logger.debug(log_prompt.format('Send signal => oauth2 user login failed')) - user_auth_failed.send( - sender=self.__class__, request=request, username=user.username, - reason=_('User invalid, disabled or expired'), - backend=settings.AUTH_BACKEND_OAUTH2 - ) + self.send_backend_auth_failed_signal(request=request, username=user.username) return None diff --git a/apps/authentication/backends/oidc/backends.py b/apps/authentication/backends/oidc/backends.py index 8a70b4027..c60dec1cd 100644 --- a/apps/authentication/backends/oidc/backends.py +++ b/apps/authentication/backends/oidc/backends.py @@ -16,7 +16,6 @@ from django.contrib.auth.backends import ModelBackend from django.db import transaction from django.urls import reverse -from authentication.signals import user_auth_success, user_auth_failed from authentication.utils import build_absolute_uri_for_oidc from common.utils import get_logger from users.utils import construct_user_email @@ -25,7 +24,7 @@ from .signals import ( openid_create_or_update_user ) from .utils import validate_and_return_id_token -from ..base import JMSBaseAuthBackend +from ..base import RedirectAuthBackend, JMSBaseAuthBackend logger = get_logger(__file__) @@ -66,14 +65,14 @@ class UserMixin: return user, created -class OIDCBaseBackend(UserMixin, JMSBaseAuthBackend, ModelBackend): +class OIDCBaseBackend(UserMixin, ModelBackend): @staticmethod def is_enabled(): return settings.AUTH_OPENID -class OIDCAuthCodeBackend(OIDCBaseBackend): +class OIDCAuthCodeBackend(RedirectAuthBackend, OIDCBaseBackend): """ Allows to authenticate users using an OpenID Connect Provider (OP). This authentication backend is able to authenticate users in the case of the OpenID Connect @@ -84,6 +83,8 @@ class OIDCAuthCodeBackend(OIDCBaseBackend): """ + backend = settings.AUTH_BACKEND_OIDC_CODE + @ssl_verification def authenticate(self, request, nonce=None, code_verifier=None): """ Authenticates users in case of the OpenID Connect Authorization code flow. """ @@ -212,23 +213,15 @@ class OIDCAuthCodeBackend(OIDCBaseBackend): if self.user_can_authenticate(user): logger.debug(log_prompt.format('OpenID user login success')) - logger.debug(log_prompt.format('Send signal => openid user login success')) - user_auth_success.send( - sender=self.__class__, request=request, user=user, - backend=settings.AUTH_BACKEND_OIDC_CODE - ) return user else: logger.debug(log_prompt.format('OpenID user login failed')) logger.debug(log_prompt.format('Send signal => openid user login failed')) - user_auth_failed.send( - sender=self.__class__, request=request, username=user.username, - reason="User is invalid", backend=settings.AUTH_BACKEND_OIDC_CODE - ) + self.send_backend_auth_failed_signal(request=request, username=user.username) return None -class OIDCAuthPasswordBackend(OIDCBaseBackend): +class OIDCAuthPasswordBackend(JMSBaseAuthBackend, OIDCBaseBackend): @ssl_verification def authenticate(self, request, username=None, password=None): @@ -274,11 +267,6 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend): error = "Json token response error, token response " \ "content is: {}, error is: {}".format(token_response.content, str(e)) logger.debug(log_prompt.format(error)) - logger.debug(log_prompt.format('Send signal => openid user login failed')) - user_auth_failed.send( - sender=self.__class__, request=request, username=username, reason=error, - backend=settings.AUTH_BACKEND_OIDC_PASSWORD - ) return # Retrieves the access token @@ -303,11 +291,6 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend): error = "Json claims response error, claims response " \ "content is: {}, error is: {}".format(claims_response.content, str(e)) logger.debug(log_prompt.format(error)) - logger.debug(log_prompt.format('Send signal => openid user login failed')) - user_auth_failed.send( - sender=self.__class__, request=request, username=username, reason=error, - backend=settings.AUTH_BACKEND_OIDC_PASSWORD - ) return logger.debug(log_prompt.format('Get or create user from claims')) @@ -317,17 +300,7 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend): if self.user_can_authenticate(user): logger.debug(log_prompt.format('OpenID user login success')) - logger.debug(log_prompt.format('Send signal => openid user login success')) - user_auth_success.send( - sender=self.__class__, request=request, user=user, - backend=settings.AUTH_BACKEND_OIDC_PASSWORD - ) return user else: logger.debug(log_prompt.format('OpenID user login failed')) - logger.debug(log_prompt.format('Send signal => openid user login failed')) - user_auth_failed.send( - sender=self.__class__, request=request, username=username, reason="User is invalid", - backend=settings.AUTH_BACKEND_OIDC_PASSWORD - ) return None diff --git a/apps/authentication/backends/saml2/backends.py b/apps/authentication/backends/saml2/backends.py index 52bc79501..907309079 100644 --- a/apps/authentication/backends/saml2/backends.py +++ b/apps/authentication/backends/saml2/backends.py @@ -5,19 +5,19 @@ from django.conf import settings from django.db import transaction from common.utils import get_logger -from authentication.errors import reason_choices, reason_user_invalid from .signals import ( saml2_create_or_update_user ) -from authentication.signals import user_auth_failed, user_auth_success -from ..base import JMSBaseAuthBackend +from ..base import RedirectAuthBackend __all__ = ['SAML2Backend'] logger = get_logger(__name__) -class SAML2Backend(JMSBaseAuthBackend): +class SAML2Backend(RedirectAuthBackend): + backend = settings.AUTH_BACKEND_SAML2 + @staticmethod def is_enabled(): return settings.AUTH_SAML2 @@ -59,16 +59,8 @@ class SAML2Backend(JMSBaseAuthBackend): if self.user_can_authenticate(user): logger.debug(log_prompt.format('SAML2 user login success')) - user_auth_success.send( - sender=self.__class__, request=request, user=user, created=created, - backend=settings.AUTH_BACKEND_SAML2 - ) return user else: logger.debug(log_prompt.format('SAML2 user login failed')) - user_auth_failed.send( - sender=self.__class__, request=request, username=username, - reason=reason_choices.get(reason_user_invalid), - backend=settings.AUTH_BACKEND_SAML2 - ) + self.send_backend_auth_failed_signal(request=request, username=user.username) return None diff --git a/apps/authentication/middleware.py b/apps/authentication/middleware.py index d87d41aec..6bb331fc3 100644 --- a/apps/authentication/middleware.py +++ b/apps/authentication/middleware.py @@ -10,7 +10,7 @@ from django.utils.translation import gettext as _ from apps.authentication import mixins from audits.signal_handlers import send_login_info_to_reviewers -from authentication.signals import post_auth_failed +from authentication.signals import post_auth_failed, post_auth_success from common.utils import gen_key_pair, gen_gm_key_pair from common.utils import get_request_ip @@ -101,6 +101,9 @@ class ThirdPartyLoginMiddleware(mixins.AuthMixin): response = render(request, 'authentication/auth_fail_flash_message_standalone.html', context) else: if not self.request.session.get('auth_confirm_required'): + post_auth_success.send( + sender=self.__class__, user=request.user, request=self.request + ) return response guard_url = reverse('authentication:login-guard') args = request.META.get('QUERY_STRING', '') diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 6a6b980fd..6c4bd03df 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -177,6 +177,9 @@ def authenticate(request=None, **credentials): if user is None: continue + if request: + request.session['auth_backend'] = backend_path + if not user.is_valid: temp_user = user temp_user.backend = backend_path diff --git a/apps/authentication/signal_handlers.py b/apps/authentication/signal_handlers.py index f8d5f591a..5b6f942ff 100644 --- a/apps/authentication/signal_handlers.py +++ b/apps/authentication/signal_handlers.py @@ -7,7 +7,7 @@ from django_cas_ng.signals import cas_user_authenticated from apps.jumpserver.settings.auth import AUTHENTICATION_BACKENDS_THIRD_PARTY from audits.models import UserSession from common.sessions.cache import user_session_manager -from .signals import post_auth_success, post_auth_failed, user_auth_failed, user_auth_success +from .signals import post_auth_failed, backend_auth_failed from .backends.oauth2_provider.signal_handlers import * @@ -43,19 +43,7 @@ def on_user_auth_login_success(sender, user, request, **kwargs): user.lang = lang -@receiver(cas_user_authenticated) -def on_cas_user_login_success(sender, request, user, **kwargs): - request.session['auth_backend'] = settings.AUTH_BACKEND_CAS - post_auth_success.send(sender, user=user, request=request) - - -@receiver(user_auth_success) -def on_user_login_success(sender, request, user, backend, create=False, **kwargs): - request.session['auth_backend'] = backend - post_auth_success.send(sender, user=user, request=request) - - -@receiver(user_auth_failed) +@receiver(backend_auth_failed) def on_user_login_failed(sender, username, request, reason, backend, **kwargs): request.session['auth_backend'] = backend post_auth_failed.send(sender, username=username, request=request, reason=reason) diff --git a/apps/authentication/signals.py b/apps/authentication/signals.py index 7c8b8e229..bd442de88 100644 --- a/apps/authentication/signals.py +++ b/apps/authentication/signals.py @@ -3,5 +3,4 @@ from django.dispatch import Signal post_auth_success = Signal() post_auth_failed = Signal() -user_auth_success = Signal() -user_auth_failed = Signal() +backend_auth_failed = Signal() From 3a65bd641957c2ca41cd6f27232a6fac65819b2b Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 28 Apr 2026 11:09:14 +0800 Subject: [PATCH 4/9] perf: update TINKER_VERSION to v0.2.3 --- apps/terminal/automations/deploy_applet_host/playbook.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/terminal/automations/deploy_applet_host/playbook.yml b/apps/terminal/automations/deploy_applet_host/playbook.yml index c443fa769..a451ba04e 100644 --- a/apps/terminal/automations/deploy_applet_host/playbook.yml +++ b/apps/terminal/automations/deploy_applet_host/playbook.yml @@ -17,7 +17,7 @@ PYTHON_VERSION: 3.11.11 CHROME_VERSION: 118.0.5993.118 CHROME_DRIVER_VERSION: 118.0.5993.70 - TINKER_VERSION: v0.2.2 + TINKER_VERSION: v0.2.3 tasks: - block: From efd0abb86752816237959c4999d3407688ecbf0c Mon Sep 17 00:00:00 2001 From: Bai Date: Tue, 28 Apr 2026 12:05:47 +0800 Subject: [PATCH 5/9] fix: remove mfa login page code-test payload --- apps/templates/_mfa_login_field.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/templates/_mfa_login_field.html b/apps/templates/_mfa_login_field.html index 12c7bba67..40b73ade4 100644 --- a/apps/templates/_mfa_login_field.html +++ b/apps/templates/_mfa_login_field.html @@ -78,11 +78,11 @@ } $('.input-style').each(function (i, ele) { - $(ele).attr('name', 'code-test') + $(ele).prop('disabled', true).removeAttr('name') }) const currentMFAInputRef = $('#mfa-' + name + ' .input-style') - currentMFAInputRef.attr('name', 'code') + currentMFAInputRef.prop('disabled', false).attr('name', 'code') // 登录页时,不应该默认focus const usernameRef = $('input[name="username"]') From 11ba01fce46786728436eb044271d585ec2c84b7 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Tue, 28 Apr 2026 12:11:20 +0800 Subject: [PATCH 6/9] perf: translate --- apps/i18n/lina/en.json | 3 ++- apps/i18n/lina/zh_hant.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/i18n/lina/en.json b/apps/i18n/lina/en.json index 271da0e79..d5cb7f75a 100644 --- a/apps/i18n/lina/en.json +++ b/apps/i18n/lina/en.json @@ -1690,5 +1690,6 @@ "ReportRecipientsTip": "Currently only supports email sending", "ReportSchedulePriorityTip": "If both interval and crontab are set, crontab takes priority", "FooterContentTooLong200": "Footer content is too long, please limit it to 200 characters", - "ImageFileCorruptedOrUnreadable": "The image file is corrupted or unreadable, please check the file and try again" + "ImageFileCorruptedOrUnreadable": "The image file is corrupted or unreadable, please check the file and try again", + "DeviceManager": "Device manager" } diff --git a/apps/i18n/lina/zh_hant.json b/apps/i18n/lina/zh_hant.json index 099a97994..c4140f86d 100644 --- a/apps/i18n/lina/zh_hant.json +++ b/apps/i18n/lina/zh_hant.json @@ -1698,5 +1698,6 @@ "ReportRecipientsTip": "當前僅支持郵件發送", "ReportSchedulePriorityTip": "如果同時設置了 interval 和 crontab,則優先考慮 crontab", "FooterContentTooLong200": "頁腳內容過長,請限制在 200 字以內", - "ImageFileCorruptedOrUnreadable": "影像檔案已損壞或無法讀取,請檢查檔案並重試。" + "ImageFileCorruptedOrUnreadable": "影像檔案已損壞或無法讀取,請檢查檔案並重試。", + "DeviceManager": "設備管理" } From 6d5f11c0f479f549cc68890f05b7c624c51d54c3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Apr 2026 06:23:24 +0000 Subject: [PATCH 7/9] perf: Update Dockerfile with new base image tag --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b13986d0f..cd5a56def 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM jumpserver/core-base:20260427_142026 AS stage-build +FROM jumpserver/core-base:20260428_061310 AS stage-build ARG VERSION From 338759901a38e779d47e18a18c3e62a6e1dbcda3 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 29 Apr 2026 10:50:50 +0800 Subject: [PATCH 8/9] perf: modify rdp filename --- apps/authentication/api/connection_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 34ae32e0e..54a326015 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -173,7 +173,7 @@ class RDPFileClientProtocolURLMixin: return name def get_connect_filename(self, prefix_name): - filename = f'{prefix_name}-jumpserver' + filename = prefix_name filename = self.escape_name(filename) return filename From bab5f56ec964e1f22a70d9b59b27f1b952cb29ff Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 29 Apr 2026 11:08:41 +0800 Subject: [PATCH 9/9] feat: support asset filter exclude_type/category --- apps/assets/api/asset/asset.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/assets/api/asset/asset.py b/apps/assets/api/asset/asset.py index 6d1312fe3..80c2ad41c 100644 --- a/apps/assets/api/asset/asset.py +++ b/apps/assets/api/asset/asset.py @@ -51,7 +51,9 @@ class AssetFilterSet(BaseFilterSet): exclude_platform = drf_filters.CharFilter(field_name="platform__name", lookup_expr='exact', exclude=True) zone = drf_filters.CharFilter(method='filter_zone') type = drf_filters.CharFilter(field_name="platform__type", lookup_expr="exact") + exclude_type = drf_filters.CharFilter(field_name="platform__type", lookup_expr="exact", exclude=True) category = drf_filters.CharFilter(field_name="platform__category", lookup_expr="exact") + exclude_category = drf_filters.CharFilter(field_name="platform__category", lookup_expr="exact", exclude=True) protocols = drf_filters.CharFilter(method='filter_protocols') gateway_enabled = drf_filters.BooleanFilter( field_name="platform__gateway_enabled", lookup_expr="exact"