Merge branch 'osm' of github.com:jumpserver/jumpserver into osm

This commit is contained in:
ibuler
2026-04-29 15:59:46 +08:00
18 changed files with 81 additions and 99 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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
)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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', '')

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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"
}

View File

@@ -1698,5 +1698,6 @@
"ReportRecipientsTip": "當前僅支持郵件發送",
"ReportSchedulePriorityTip": "如果同時設置了 interval 和 crontab則優先考慮 crontab",
"FooterContentTooLong200": "頁腳內容過長,請限制在 200 字以內",
"ImageFileCorruptedOrUnreadable": "影像檔案已損壞或無法讀取,請檢查檔案並重試。"
"ImageFileCorruptedOrUnreadable": "影像檔案已損壞或無法讀取,請檢查檔案並重試。",
"DeviceManager": "設備管理"
}

View File

@@ -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"]')

View File

@@ -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: