Compare commits

..

40 Commits

Author SHA1 Message Date
Jiangjie Bai
1d0db2ba8b Merge pull request #16316 from jumpserver/dev
v4.10.13-lts
2025-11-20 20:20:02 +08:00
老广
e617245b26 merge: to master 2025-10-16 17:30:34 +08:00
Bryan
9280884c1c Merge pull request #16056 from jumpserver/dev
v4.10.8-lts
2025-09-18 16:52:13 +08:00
Bryan
f31994fdcd Merge pull request #15899 from jumpserver/dev 2025-08-21 19:03:18 +08:00
Bryan
71766418bb Merge pull request #15742 from jumpserver/dev
merge: v4.10.4-lts
2025-07-17 15:12:58 +08:00
Bryan
a9399dd709 Merge pull request #15608 from jumpserver/dev
v4.10.2
2025-06-19 20:14:21 +08:00
Bryan
d0cb9e5432 Merge pull request #15412 from jumpserver/dev
v4.10.0
2025-05-15 17:11:43 +08:00
老广
558188da90 merge: dev to master
Ready to relase
2025-04-17 20:24:45 +08:00
Bryan
ad5460dab8 Merge pull request #15086 from jumpserver/dev
v4.8.0
2025-03-20 18:44:44 +08:00
Bryan
4d37dca0de Merge pull request #14901 from jumpserver/dev
v4.7.0
2025-02-20 10:21:16 +08:00
Bryan
2ca4002624 Merge pull request #14813 from jumpserver/dev
v4.6.0
2025-01-15 14:38:17 +08:00
Bryan
053d640e4c Merge pull request #14699 from jumpserver/dev
v4.5.0
2024-12-19 16:04:45 +08:00
Bryan
f3acc28ded Merge pull request #14697 from jumpserver/dev
v4.5.0
2024-12-19 15:57:11 +08:00
Bryan
25987545db Merge pull request #14511 from jumpserver/dev
v4.4.0
2024-11-21 19:00:35 +08:00
Bryan
6720ecc6e0 Merge pull request #14319 from jumpserver/dev
v4.3.0
2024-10-17 14:55:38 +08:00
老广
0b3a7bb020 Merge pull request #14203 from jumpserver/dev
merge: from dev to master
2024-09-19 19:37:19 +08:00
Bryan
56373e362b Merge pull request #13988 from jumpserver/dev
v4.1.0
2024-08-16 18:40:35 +08:00
Bryan
02fc045370 Merge pull request #13600 from jumpserver/dev
v4.0.0
2024-07-03 19:04:35 +08:00
Bryan
e4ac73896f Merge pull request #13452 from jumpserver/dev
v3.10.11-lts
2024-06-19 16:01:26 +08:00
Bryan
1518f792d6 Merge pull request #13236 from jumpserver/dev
v3.10.10-lts
2024-05-16 16:04:07 +08:00
Bai
67277dd622 fix: 修复仪表盘会话排序数量都是 1 的问题 2024-04-22 19:42:33 +08:00
Bryan
82e7f020ea Merge pull request #13094 from jumpserver/dev
v3.10.9 (dev to master)
2024-04-22 19:39:53 +08:00
Bryan
f20b9e01ab Merge pull request #13062 from jumpserver/dev
v3.10.8 dev to master
2024-04-18 18:01:20 +08:00
Bryan
8cf8a3701b Merge pull request #13059 from jumpserver/dev
v3.10.8
2024-04-18 17:16:37 +08:00
Bryan
7ba24293d1 Merge pull request #12736 from jumpserver/pr@dev@master_fix
fix: 解决冲突
2024-02-29 16:38:43 +08:00
Bai
f10114c9ed fix: 解决冲突 2024-02-29 16:37:10 +08:00
Bryan
cf31cbfb07 Merge pull request #12729 from jumpserver/dev
v3.10.4
2024-02-29 16:19:59 +08:00
wangruidong
0edad24d5d fix: 资产过期消息提示发送失败 2024-02-04 11:41:48 +08:00
ibuler
1f1c1a9157 fix: 修复定时检测用户是否活跃任务无法执行的问题 2024-01-23 09:28:38 +00:00
feng
6c9d271ae1 fix: redis 密码有特殊字符celery beat启动失败 2024-01-22 06:18:34 +00:00
Bai
6ff852e225 perf: 修复 Count 时没有去重的问题 2024-01-22 06:16:25 +00:00
Bryan
baa75dc735 Merge pull request #12566 from jumpserver/master
v3.10.2
2024-01-17 07:34:28 -04:00
Bryan
8a9f0436b8 Merge pull request #12565 from jumpserver/dev
v3.10.2
2024-01-17 07:23:30 -04:00
Bryan
a9620a3cbe Merge pull request #12461 from jumpserver/master
v3.10.1
2023-12-29 11:33:05 +05:00
Bryan
769e7dc8a0 Merge pull request #12460 from jumpserver/dev
v3.10.1
2023-12-29 11:20:36 +05:00
Bryan
2a70449411 Merge pull request #12458 from jumpserver/dev
v3.10.1
2023-12-29 11:01:13 +05:00
Bryan
8df720f19e Merge pull request #12401 from jumpserver/dev
v3.10
2023-12-21 15:14:19 +05:00
老广
dabbb45f6e Merge pull request #12144 from jumpserver/dev
v3.9.0
2023-11-16 18:23:05 +08:00
Bryan
ce24c1c3fd Merge pull request #11914 from jumpserver/dev
v3.8.0
2023-10-19 03:37:39 -05:00
Bryan
3c54c82ce9 Merge pull request #11636 from jumpserver/dev
v3.7.0
2023-09-21 17:02:48 +08:00
23 changed files with 52 additions and 160 deletions

View File

@@ -1,33 +1,10 @@
on: on: [push, pull_request, release]
push:
pull_request:
types: [opened, synchronize, closed]
release:
types: [created]
name: JumpServer repos generic handler name: JumpServer repos generic handler
jobs: jobs:
handle_pull_request: generic_handler:
if: github.event_name == 'pull_request' name: Run generic handler
runs-on: ubuntu-latest
steps:
- uses: jumpserver/action-generic-handler@master
env:
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
I18N_TOKEN: ${{ secrets.I18N_TOKEN }}
handle_push:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: jumpserver/action-generic-handler@master
env:
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
I18N_TOKEN: ${{ secrets.I18N_TOKEN }}
handle_release:
if: github.event_name == 'release'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: jumpserver/action-generic-handler@master - uses: jumpserver/action-generic-handler@master

View File

@@ -268,14 +268,6 @@ class Protocol(ChoicesMixin, models.TextChoices):
'port_from_addr': True, 'port_from_addr': True,
'required': True, 'required': True,
'secret_types': ['token'], 'secret_types': ['token'],
'setting': {
'namespace': {
'type': 'str',
'required': False,
'default': '',
'label': _('Namespace')
}
}
}, },
cls.http: { cls.http: {
'port': 80, 'port': 80,

View File

@@ -50,7 +50,7 @@ class UserLoginForm(forms.Form):
class UserCheckOtpCodeForm(forms.Form): class UserCheckOtpCodeForm(forms.Form):
code = forms.CharField(label=_('MFA Code'), max_length=128, required=False) code = forms.CharField(label=_('MFA Code'), max_length=128, required=False)
mfa_type = forms.CharField(label=_('MFA type'), max_length=128, required=False) mfa_type = forms.CharField(label=_('MFA type'), max_length=128)
class CustomCaptchaTextInput(CaptchaTextInput): class CustomCaptchaTextInput(CaptchaTextInput):

View File

@@ -72,9 +72,10 @@ class BaseMFA(abc.ABC):
def is_active(self): def is_active(self):
return False return False
@classmethod @staticmethod
def global_enabled(cls): @abc.abstractmethod
return cls.name in settings.SECURITY_MFA_ENABLED_BACKENDS def global_enabled():
return False
@abc.abstractmethod @abc.abstractmethod
def get_enable_url(self) -> str: def get_enable_url(self) -> str:

View File

@@ -39,9 +39,9 @@ class MFACustom(BaseMFA):
def is_active(self): def is_active(self):
return True return True
@classmethod @staticmethod
def global_enabled(cls): def global_enabled():
return super().global_enabled() and settings.MFA_CUSTOM and callable(mfa_custom_method) return settings.MFA_CUSTOM and callable(mfa_custom_method)
def get_enable_url(self) -> str: def get_enable_url(self) -> str:
return '' return ''

View File

@@ -50,9 +50,9 @@ class MFAEmail(BaseMFA):
) )
sender_util.gen_and_send_async() sender_util.gen_and_send_async()
@classmethod @staticmethod
def global_enabled(cls): def global_enabled():
return super().global_enabled and settings.SECURITY_MFA_BY_EMAIL return settings.SECURITY_MFA_BY_EMAIL
def disable(self): def disable(self):
return '/ui/#/profile/index' return '/ui/#/profile/index'

View File

@@ -29,10 +29,9 @@ class MFAFace(BaseMFA, AuthFaceMixin):
return True return True
return bool(self.user.face_vector) return bool(self.user.face_vector)
@classmethod @staticmethod
def global_enabled(cls): def global_enabled():
return ( return (
super().global_enabled() and
settings.XPACK_LICENSE_IS_VALID and settings.XPACK_LICENSE_IS_VALID and
settings.XPACK_LICENSE_EDITION_ULTIMATE and settings.XPACK_LICENSE_EDITION_ULTIMATE and
settings.FACE_RECOGNITION_ENABLED settings.FACE_RECOGNITION_ENABLED

View File

@@ -25,6 +25,10 @@ class MFAOtp(BaseMFA):
return True return True
return self.user.otp_secret_key return self.user.otp_secret_key
@staticmethod
def global_enabled():
return True
def get_enable_url(self) -> str: def get_enable_url(self) -> str:
return reverse('authentication:user-otp-enable-start') return reverse('authentication:user-otp-enable-start')

View File

@@ -23,9 +23,9 @@ class MFAPasskey(BaseMFA):
return False return False
return self.user.passkey_set.count() return self.user.passkey_set.count()
@classmethod @staticmethod
def global_enabled(cls): def global_enabled():
return super().global_enabled() and settings.AUTH_PASSKEY return settings.AUTH_PASSKEY
def get_enable_url(self) -> str: def get_enable_url(self) -> str:
return '/ui/#/profile/passkeys' return '/ui/#/profile/passkeys'

View File

@@ -27,9 +27,9 @@ class MFARadius(BaseMFA):
def is_active(self): def is_active(self):
return True return True
@classmethod @staticmethod
def global_enabled(cls): def global_enabled():
return super().global_enabled() and settings.OTP_IN_RADIUS return settings.OTP_IN_RADIUS
def get_enable_url(self) -> str: def get_enable_url(self) -> str:
return '' return ''

View File

@@ -46,9 +46,9 @@ class MFASms(BaseMFA):
def send_challenge(self): def send_challenge(self):
self.sms.gen_and_send_async() self.sms.gen_and_send_async()
@classmethod @staticmethod
def global_enabled(cls): def global_enabled():
return super().global_enabled() and settings.SMS_ENABLED return settings.SMS_ENABLED
def get_enable_url(self) -> str: def get_enable_url(self) -> str:
return '/ui/#/profile/index' return '/ui/#/profile/index'

View File

@@ -376,7 +376,7 @@
</div> </div>
{% if form.challenge %} {% if form.challenge %}
{% bootstrap_field form.challenge show_label=False %} {% bootstrap_field form.challenge show_label=False %}
{% elif form.mfa_type and mfa_backends %} {% elif form.mfa_type %}
<div class="form-group" style="display: flex"> <div class="form-group" style="display: flex">
{% include '_mfa_login_field.html' %} {% include '_mfa_login_field.html' %}
</div> </div>

View File

@@ -2,7 +2,6 @@
# #
import datetime import datetime
import inspect import inspect
import sys import sys
if sys.version_info.major == 3 and sys.version_info.minor >= 10: if sys.version_info.major == 3 and sys.version_info.minor >= 10:
@@ -335,10 +334,6 @@ class ES(object):
def is_keyword(props: dict, field: str) -> bool: def is_keyword(props: dict, field: str) -> bool:
return props.get(field, {}).get("type", "keyword") == "keyword" return props.get(field, {}).get("type", "keyword") == "keyword"
@staticmethod
def is_long(props: dict, field: str) -> bool:
return props.get(field, {}).get("type") == "long"
def get_query_body(self, **kwargs): def get_query_body(self, **kwargs):
new_kwargs = {} new_kwargs = {}
for k, v in kwargs.items(): for k, v in kwargs.items():
@@ -366,10 +361,10 @@ class ES(object):
if index_in_field in kwargs: if index_in_field in kwargs:
index['values'] = kwargs[index_in_field] index['values'] = kwargs[index_in_field]
mapping = self.es.indices.get_mapping(index=self.index) mapping = self.es.indices.get_mapping(index=self.query_index)
props = ( props = (
mapping mapping
.get(self.index, {}) .get(self.query_index, {})
.get('mappings', {}) .get('mappings', {})
.get('properties', {}) .get('properties', {})
) )
@@ -380,9 +375,6 @@ class ES(object):
if k in ("org_id", "session") and self.is_keyword(props, k): if k in ("org_id", "session") and self.is_keyword(props, k):
exact[k] = v exact[k] = v
elif self.is_long(props, k):
exact[k] = v
elif k in common_keyword_able: elif k in common_keyword_able:
exact[f"{k}.keyword"] = v exact[f"{k}.keyword"] = v

View File

@@ -101,7 +101,7 @@ def get_ip_city(ip):
info = get_ip_city_by_ipip(ip) info = get_ip_city_by_ipip(ip)
if info: if info:
city = info.get('city') or _("Unknown") city = info.get('city', _("Unknown"))
country = info.get('country') country = info.get('country')
# 国内城市 并且 语言是中文就使用国内 # 国内城市 并且 语言是中文就使用国内

View File

@@ -569,7 +569,7 @@ class Config(dict):
'SAFE_MODE': False, 'SAFE_MODE': False,
'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启 'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启
'SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY': True, 'SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY': True,
'SECURITY_MFA_ENABLED_BACKENDS': [], 'SECURITY_MFA_BY_EMAIL': False,
'SECURITY_COMMAND_EXECUTION': False, 'SECURITY_COMMAND_EXECUTION': False,
'SECURITY_COMMAND_BLACKLIST': [ 'SECURITY_COMMAND_BLACKLIST': [
'reboot', 'shutdown', 'poweroff', 'halt', 'dd', 'half', 'top' 'reboot', 'shutdown', 'poweroff', 'halt', 'dd', 'half', 'top'

View File

@@ -43,7 +43,6 @@ class ComponentI18nApi(RetrieveAPIView):
if not lang: if not lang:
return Response(data) return Response(data)
lang = lang.lower()
if lang not in dict(Language.get_code_mapper()).keys(): if lang not in dict(Language.get_code_mapper()).keys():
lang = 'en' lang = 'en'

View File

@@ -1,23 +0,0 @@
# Generated by Django 4.1.13 on 2025-11-27 02:54
from django.db import migrations, connections
def refresh_pg_collation(apps, schema_editor):
for alias, conn in connections.databases.items():
if connections[alias].vendor == "postgresql":
dbname = connections[alias].settings_dict["NAME"]
connections[alias].cursor().execute(
f'ALTER DATABASE "{dbname}" REFRESH COLLATION VERSION;'
)
print(f"Refreshed postgresql collation version for database: {dbname} successfully.")
class Migration(migrations.Migration):
dependencies = [
('settings', '0002_leakpasswords'),
]
operations = [
migrations.RunPython(refresh_pg_collation, migrations.RunPython.noop),
]

View File

@@ -1,7 +1,3 @@
import importlib
import os
from django.conf import settings
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
@@ -121,35 +117,6 @@ class SecurityLoginLimitSerializer(serializers.Serializer):
) )
class DynamicMFAChoiceField(serializers.MultipleChoiceField):
def __init__(self, **kwargs):
_choices = self._get_dynamic_choices()
super().__init__(choices=_choices, **kwargs)
@staticmethod
def _get_dynamic_choices():
choices = []
mfa_dir = os.path.join(settings.APPS_DIR, 'authentication', 'mfa')
for filename in os.listdir(mfa_dir):
if not filename.endswith('.py') or filename.startswith('__init__'):
continue
module_name = f'authentication.mfa.{filename[:-3]}'
try:
module = importlib.import_module(module_name)
except ImportError:
continue
for attr_name in dir(module):
item = getattr(module, attr_name)
if not isinstance(item, type) or not attr_name.startswith('MFA'):
continue
if 'BaseMFA' != item.__base__.__name__:
continue
choices.append((item.name, item.display_name))
return choices
class SecurityAuthSerializer(serializers.Serializer): class SecurityAuthSerializer(serializers.Serializer):
SECURITY_MFA_AUTH = serializers.ChoiceField( SECURITY_MFA_AUTH = serializers.ChoiceField(
choices=( choices=(
@@ -163,10 +130,10 @@ class SecurityAuthSerializer(serializers.Serializer):
required=False, default=True, required=False, default=True,
label=_('Third-party login MFA'), label=_('Third-party login MFA'),
) )
SECURITY_MFA_ENABLED_BACKENDS = DynamicMFAChoiceField( SECURITY_MFA_BY_EMAIL = serializers.BooleanField(
default=[], allow_empty=True, required=False, default=False,
label=_('MFA Backends'), label=_('MFA via Email'),
help_text=_('MFA methods supported for user login') help_text=_('Email as a method for multi-factor authentication')
) )
OTP_ISSUER_NAME = serializers.CharField( OTP_ISSUER_NAME = serializers.CharField(
required=False, max_length=16, label=_('OTP issuer name'), required=False, max_length=16, label=_('OTP issuer name'),

View File

@@ -22,13 +22,13 @@ p {
{% trans 'JumpServerClient, currently used to launch the client' %} {% trans 'JumpServerClient, currently used to launch the client' %}
</p> </p>
<ul> <ul>
<li> <a href="/download/public/JumpServerClient_{{ CLIENT_VERSION }}_x64_en-US.msi">JumpServerClient_{{ CLIENT_VERSION }}_x64_en-US.msi</a></li> <li> <a href="/download/public/JumpServerClient_{{ CLIENT_VERSION }}_x64_en-US.msi">JumpServerClient-x64_en-US.msi</a></li>
<li> <a href="/download/public/JumpServerClient_{{ CLIENT_VERSION }}_x64-setup.exe">JumpServerClient_{{ CLIENT_VERSION }}_x64-setup.exe</a></li> <li> <a href="/download/public/JumpServerClient_{{ CLIENT_VERSION }}_x64-setup.exe">JumpServerClient-x64-setup.exe</a></li>
<li> <a href="/download/public/JumpServerClient_{{ CLIENT_VERSION }}_aarch64.dmg">JumpServerClient_{{ CLIENT_VERSION }}_aarch64.dmg</a></li> <li> <a href="/download/public/JumpServerClient_{{ CLIENT_VERSION }}_aarch64.dmg">JumpServerClient-aarch64.dmg</a></li>
<li> <a href="/download/public/JumpServerClient_{{ CLIENT_VERSION }}_x64.dmg">JumpServerClient_{{ CLIENT_VERSION }}_x64.dmg</a></li> <li> <a href="/download/public/JumpServerClient_{{ CLIENT_VERSION }}_x64.dmg">JumpServerClient-x64.dmg</a></li>
<li> <a href="/download/public/JumpServerClient_{{ CLIENT_VERSION }}_amd64.AppImage">JumpServerClient_{{ CLIENT_VERSION }}_amd64.AppImage</a></li> <li> <a href="/download/public/JumpServerClient_{{ CLIENT_VERSION }}_amd64.AppImage">JumpServerClient-amd64.AppImage</a></li>
<li> <a href="/download/public/JumpServerClient_{{ CLIENT_VERSION }}_amd64.deb">JumpServerClient_{{ CLIENT_VERSION }}_amd64.deb</a></li> <li> <a href="/download/public/JumpServerClient_{{ CLIENT_VERSION }}_amd64.deb">JumpServerClient-amd64.deb</a></li>
<li> <a href="/download/public/JumpServerClient-{{ CLIENT_VERSION }}-1.x86_64.rpm">JumpServerClient-{{ CLIENT_VERSION }}-1.x86_64.rpm</a></li> <li> <a href="/download/public/JumpServerClient-{{ CLIENT_VERSION }}-1.x86_64.rpm">JumpServerClient-1.x86_64.rpm</a></li>
</ul> </ul>
</div> </div>

View File

@@ -1,11 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from datetime import datetime
import pytz import pytz
from common.plugins.es import ES from datetime import datetime
from common.utils import get_logger from common.utils import get_logger
from common.plugins.es import ES
logger = get_logger(__file__) logger = get_logger(__file__)
@@ -26,8 +27,8 @@ class CommandStore(ES):
"type": "long" "type": "long"
} }
} }
exact_fields = {'risk_level'} exact_fields = {}
fuzzy_fields = {'input', 'user', 'asset', 'account'} fuzzy_fields = {'input', 'risk_level', 'user', 'asset', 'account'}
match_fields = {'input'} match_fields = {'input'}
keyword_fields = {'session', 'org_id'} keyword_fields = {'session', 'org_id'}

View File

@@ -160,8 +160,7 @@ class SessionSerializer(BulkOrgResourceModelSerializer):
) )
validated_data['user'] = str(user) validated_data['user'] = str(user)
# web 资产 url 太长,超出限制 validated_data['asset'] = str(asset)
validated_data['asset'] = str(asset)[:128]
return super().create(validated_data) return super().create(validated_data)
def update(self, instance, validated_data): def update(self, instance, validated_data):

View File

@@ -6,7 +6,6 @@ import os
import re import re
import time import time
from contextlib import contextmanager from contextlib import contextmanager
from urllib.parse import unquote
import pyotp import pyotp
from django.conf import settings from django.conf import settings
@@ -61,8 +60,6 @@ def redirect_user_first_login_or_index(request, redirect_field_name):
# 防止 next 地址为 None # 防止 next 地址为 None
if not url or url.lower() in ['none']: if not url or url.lower() in ['none']:
url = reverse('index') url = reverse('index')
# 处理下载地址编码问题 '%2Fui%2F'
url = unquote(url)
return url return url

View File

@@ -37,25 +37,12 @@ logger = get_logger(__name__)
class UserOtpEnableStartView(AuthMixin, TemplateView): class UserOtpEnableStartView(AuthMixin, TemplateView):
template_name = 'users/user_otp_check_password.html' template_name = 'users/user_otp_check_password.html'
@staticmethod
def get_redirect_url():
message_data = {
'title': _('Redirecting'),
'message': _('No MFA services are available. Please contact the administrator'),
'redirect_url': reverse('authentication:login'),
'auto_redirect': True,
}
return FlashMessageUtil.gen_message_url(message_data)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
try: try:
self.get_user_from_session() self.get_user_from_session()
except SessionEmptyError: except SessionEmptyError:
url = reverse('authentication:login') + '?_=otp_enable_start' url = reverse('authentication:login') + '?_=otp_enable_start'
return redirect(url) return redirect(url)
if not MFAOtp.global_enabled():
return redirect(self.get_redirect_url())
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)