mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-22 12:02:34 +00:00
Compare commits
11 Commits
v4.10.13
...
pr@dev@per
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
396144e3a8 | ||
|
|
c191d86f43 | ||
|
|
7911137ffb | ||
|
|
1053933cae | ||
|
|
96fdc025cd | ||
|
|
fde19764e0 | ||
|
|
978fbc70e6 | ||
|
|
636ffd786d | ||
|
|
3b756aa26f | ||
|
|
817c0099d1 | ||
|
|
a0d7871130 |
29
.github/workflows/jms-generic-action-handler.yml
vendored
29
.github/workflows/jms-generic-action-handler.yml
vendored
@@ -1,10 +1,33 @@
|
|||||||
on: [push, pull_request, release]
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, closed]
|
||||||
|
release:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
name: JumpServer repos generic handler
|
name: JumpServer repos generic handler
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generic_handler:
|
handle_pull_request:
|
||||||
name: Run generic handler
|
if: github.event_name == 'pull_request'
|
||||||
|
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
|
||||||
|
|||||||
@@ -268,6 +268,14 @@ 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,
|
||||||
|
|||||||
@@ -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)
|
mfa_type = forms.CharField(label=_('MFA type'), max_length=128, required=False)
|
||||||
|
|
||||||
|
|
||||||
class CustomCaptchaTextInput(CaptchaTextInput):
|
class CustomCaptchaTextInput(CaptchaTextInput):
|
||||||
|
|||||||
@@ -72,10 +72,9 @@ class BaseMFA(abc.ABC):
|
|||||||
def is_active(self):
|
def is_active(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
@abc.abstractmethod
|
def global_enabled(cls):
|
||||||
def global_enabled():
|
return cls.name in settings.SECURITY_MFA_ENABLED_BACKENDS
|
||||||
return False
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def get_enable_url(self) -> str:
|
def get_enable_url(self) -> str:
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ class MFACustom(BaseMFA):
|
|||||||
def is_active(self):
|
def is_active(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def global_enabled():
|
def global_enabled(cls):
|
||||||
return settings.MFA_CUSTOM and callable(mfa_custom_method)
|
return super().global_enabled() and settings.MFA_CUSTOM and callable(mfa_custom_method)
|
||||||
|
|
||||||
def get_enable_url(self) -> str:
|
def get_enable_url(self) -> str:
|
||||||
return ''
|
return ''
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ class MFAEmail(BaseMFA):
|
|||||||
)
|
)
|
||||||
sender_util.gen_and_send_async()
|
sender_util.gen_and_send_async()
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def global_enabled():
|
def global_enabled(cls):
|
||||||
return settings.SECURITY_MFA_BY_EMAIL
|
return super().global_enabled and settings.SECURITY_MFA_BY_EMAIL
|
||||||
|
|
||||||
def disable(self):
|
def disable(self):
|
||||||
return '/ui/#/profile/index'
|
return '/ui/#/profile/index'
|
||||||
|
|||||||
@@ -29,9 +29,10 @@ class MFAFace(BaseMFA, AuthFaceMixin):
|
|||||||
return True
|
return True
|
||||||
return bool(self.user.face_vector)
|
return bool(self.user.face_vector)
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def global_enabled():
|
def global_enabled(cls):
|
||||||
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
|
||||||
|
|||||||
@@ -25,10 +25,6 @@ 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')
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ class MFAPasskey(BaseMFA):
|
|||||||
return False
|
return False
|
||||||
return self.user.passkey_set.count()
|
return self.user.passkey_set.count()
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def global_enabled():
|
def global_enabled(cls):
|
||||||
return settings.AUTH_PASSKEY
|
return super().global_enabled() and settings.AUTH_PASSKEY
|
||||||
|
|
||||||
def get_enable_url(self) -> str:
|
def get_enable_url(self) -> str:
|
||||||
return '/ui/#/profile/passkeys'
|
return '/ui/#/profile/passkeys'
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ class MFARadius(BaseMFA):
|
|||||||
def is_active(self):
|
def is_active(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def global_enabled():
|
def global_enabled(cls):
|
||||||
return settings.OTP_IN_RADIUS
|
return super().global_enabled() and settings.OTP_IN_RADIUS
|
||||||
|
|
||||||
def get_enable_url(self) -> str:
|
def get_enable_url(self) -> str:
|
||||||
return ''
|
return ''
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def global_enabled():
|
def global_enabled(cls):
|
||||||
return settings.SMS_ENABLED
|
return super().global_enabled() and settings.SMS_ENABLED
|
||||||
|
|
||||||
def get_enable_url(self) -> str:
|
def get_enable_url(self) -> str:
|
||||||
return '/ui/#/profile/index'
|
return '/ui/#/profile/index'
|
||||||
|
|||||||
@@ -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 %}
|
{% elif form.mfa_type and mfa_backends %}
|
||||||
<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>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#
|
#
|
||||||
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:
|
||||||
@@ -334,6 +335,10 @@ 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():
|
||||||
@@ -361,10 +366,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.query_index)
|
mapping = self.es.indices.get_mapping(index=self.index)
|
||||||
props = (
|
props = (
|
||||||
mapping
|
mapping
|
||||||
.get(self.query_index, {})
|
.get(self.index, {})
|
||||||
.get('mappings', {})
|
.get('mappings', {})
|
||||||
.get('properties', {})
|
.get('properties', {})
|
||||||
)
|
)
|
||||||
@@ -375,6 +380,9 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -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', _("Unknown"))
|
city = info.get('city') or _("Unknown")
|
||||||
country = info.get('country')
|
country = info.get('country')
|
||||||
|
|
||||||
# 国内城市 并且 语言是中文就使用国内
|
# 国内城市 并且 语言是中文就使用国内
|
||||||
|
|||||||
@@ -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_BY_EMAIL': False,
|
'SECURITY_MFA_ENABLED_BACKENDS': [],
|
||||||
'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'
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ 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'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# 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),
|
||||||
|
]
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
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
|
||||||
|
|
||||||
@@ -117,6 +121,35 @@ 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=(
|
||||||
@@ -130,10 +163,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_BY_EMAIL = serializers.BooleanField(
|
SECURITY_MFA_ENABLED_BACKENDS = DynamicMFAChoiceField(
|
||||||
required=False, default=False,
|
default=[], allow_empty=True,
|
||||||
label=_('MFA via Email'),
|
label=_('MFA Backends'),
|
||||||
help_text=_('Email as a method for multi-factor authentication')
|
help_text=_('MFA methods supported for user login')
|
||||||
)
|
)
|
||||||
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'),
|
||||||
|
|||||||
@@ -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-x64_en-US.msi</a></li>
|
<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-setup.exe">JumpServerClient-x64-setup.exe</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 }}_aarch64.dmg">JumpServerClient-aarch64.dmg</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 }}_x64.dmg">JumpServerClient-x64.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 }}_amd64.AppImage">JumpServerClient-amd64.AppImage</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.deb">JumpServerClient-amd64.deb</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 }}-1.x86_64.rpm">JumpServerClient-1.x86_64.rpm</a></li>
|
<li> <a href="/download/public/JumpServerClient-{{ CLIENT_VERSION }}-1.x86_64.rpm">JumpServerClient-{{ CLIENT_VERSION }}-1.x86_64.rpm</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import pytz
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from common.utils import get_logger
|
import pytz
|
||||||
from common.plugins.es import ES
|
|
||||||
|
|
||||||
|
from common.plugins.es import ES
|
||||||
|
from common.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
@@ -27,8 +26,8 @@ class CommandStore(ES):
|
|||||||
"type": "long"
|
"type": "long"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exact_fields = {}
|
exact_fields = {'risk_level'}
|
||||||
fuzzy_fields = {'input', 'risk_level', 'user', 'asset', 'account'}
|
fuzzy_fields = {'input', 'user', 'asset', 'account'}
|
||||||
match_fields = {'input'}
|
match_fields = {'input'}
|
||||||
keyword_fields = {'session', 'org_id'}
|
keyword_fields = {'session', 'org_id'}
|
||||||
|
|
||||||
|
|||||||
@@ -160,7 +160,8 @@ class SessionSerializer(BulkOrgResourceModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
validated_data['user'] = str(user)
|
validated_data['user'] = str(user)
|
||||||
validated_data['asset'] = str(asset)
|
# web 资产 url 太长,超出限制
|
||||||
|
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):
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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
|
||||||
@@ -60,6 +61,8 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -37,12 +37,25 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user