mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-17 09:32:35 +00:00
Compare commits
15 Commits
pr@dev@per
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce107a0f39 | ||
|
|
045ca8807a | ||
|
|
19a68d8930 | ||
|
|
75ed02a2d2 | ||
|
|
f420dac49c | ||
|
|
1ee68134f2 | ||
|
|
937265db5d | ||
|
|
c611d5e88b | ||
|
|
883b6b6383 | ||
|
|
ac4c72064f | ||
|
|
dbf8360e27 | ||
|
|
150d7a09bc | ||
|
|
a7ed20e059 | ||
|
|
1b7b8e6f2e | ||
|
|
cd22fbce19 |
@@ -1,4 +1,4 @@
|
|||||||
FROM jumpserver/core-base:20251113_092612 AS stage-build
|
FROM jumpserver/core-base:20251128_025056 AS stage-build
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
|
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ from .sso import *
|
|||||||
from .temp_token import *
|
from .temp_token import *
|
||||||
from .token import *
|
from .token import *
|
||||||
from .face import *
|
from .face import *
|
||||||
|
from .access_token import *
|
||||||
|
|||||||
32
apps/authentication/api/access_token.py
Normal file
32
apps/authentication/api/access_token.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from django.utils import timezone
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.status import HTTP_200_OK
|
||||||
|
from rbac.permissions import RBACPermission
|
||||||
|
from common.api import JMSModelViewSet
|
||||||
|
from ..serializers import AccessTokenSerializer
|
||||||
|
|
||||||
|
from oauth2_provider.models import get_access_token_model
|
||||||
|
|
||||||
|
AccessToken = get_access_token_model()
|
||||||
|
|
||||||
|
class AccessTokenViewSet(JMSModelViewSet):
|
||||||
|
serializer_class = AccessTokenSerializer
|
||||||
|
permission_classes = [RBACPermission]
|
||||||
|
http_method_names = ['get', 'options', 'delete']
|
||||||
|
rbac_perms = {
|
||||||
|
'revoke': 'oauth2_provider.delete_accesstoken',
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return AccessToken.objects.filter(user=self.request.user).order_by('-created')
|
||||||
|
|
||||||
|
@action(methods=['DELETE'], detail=True, url_path='revoke')
|
||||||
|
def revoke(self, *args, **kwargs):
|
||||||
|
token = AccessToken.objects.filter(id=kwargs['pk']).first()
|
||||||
|
if not token or token.user != self.request.user:
|
||||||
|
return Response({ "detail": _("Access token not found.") }, status=HTTP_200_OK)
|
||||||
|
token = token.refresh_token or token
|
||||||
|
token.revoke()
|
||||||
|
return Response( {"detail": _("Token revoked successfully.")}, status=HTTP_200_OK)
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 ''
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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 ''
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ from common.utils import get_object_or_none, random_string
|
|||||||
from users.models import User
|
from users.models import User
|
||||||
from users.serializers import UserProfileSerializer
|
from users.serializers import UserProfileSerializer
|
||||||
from ..models import AccessKey, TempToken
|
from ..models import AccessKey, TempToken
|
||||||
|
from oauth2_provider.models import get_access_token_model
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'AccessKeySerializer', 'BearerTokenSerializer',
|
'AccessKeySerializer', 'BearerTokenSerializer',
|
||||||
'SSOTokenSerializer', 'TempTokenSerializer',
|
'SSOTokenSerializer', 'TempTokenSerializer',
|
||||||
'AccessKeyCreateSerializer'
|
'AccessKeyCreateSerializer', 'AccessTokenSerializer',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -114,3 +115,28 @@ class TempTokenSerializer(serializers.ModelSerializer):
|
|||||||
token = TempToken(**kwargs)
|
token = TempToken(**kwargs)
|
||||||
token.save()
|
token.save()
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
class AccessTokenSerializer(serializers.ModelSerializer):
|
||||||
|
token_preview = serializers.SerializerMethodField(label=_("Token"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = get_access_token_model()
|
||||||
|
fields = [
|
||||||
|
'id', 'user', 'token_preview', 'is_valid',
|
||||||
|
'is_expired', 'expires', 'scope', 'created', 'updated',
|
||||||
|
]
|
||||||
|
read_only_fields = fields
|
||||||
|
extra_kwargs = {
|
||||||
|
'scope': { 'label': _('Scope') },
|
||||||
|
'expires': { 'label': _('Date expired') },
|
||||||
|
'updated': { 'label': _('Date updated') },
|
||||||
|
'created': { 'label': _('Date created') },
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_token_preview(self, obj):
|
||||||
|
token_string = obj.token
|
||||||
|
if len(token_string) > 16:
|
||||||
|
return f"{token_string[:6]}...{token_string[-4:]}"
|
||||||
|
return "****"
|
||||||
@@ -47,3 +47,9 @@ def clean_expire_token():
|
|||||||
count = TempToken.objects.filter(date_expired__lt=expired_time).delete()
|
count = TempToken.objects.filter(date_expired__lt=expired_time).delete()
|
||||||
logging.info('Deleted %d temporary tokens.', count[0])
|
logging.info('Deleted %d temporary tokens.', count[0])
|
||||||
logging.info('Cleaned expired temporary and connection tokens.')
|
logging.info('Cleaned expired temporary and connection tokens.')
|
||||||
|
|
||||||
|
|
||||||
|
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
|
||||||
|
def clear_oauth2_provider_expired_tokens():
|
||||||
|
from oauth2_provider.models import clear_expired
|
||||||
|
clear_expired()
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ router.register('super-connection-token', api.SuperConnectionTokenViewSet, 'supe
|
|||||||
router.register('admin-connection-token', api.AdminConnectionTokenViewSet, 'admin-connection-token')
|
router.register('admin-connection-token', api.AdminConnectionTokenViewSet, 'admin-connection-token')
|
||||||
router.register('confirm', api.UserConfirmationViewSet, 'confirm')
|
router.register('confirm', api.UserConfirmationViewSet, 'confirm')
|
||||||
router.register('ssh-key', api.SSHkeyViewSet, 'ssh-key')
|
router.register('ssh-key', api.SSHkeyViewSet, 'ssh-key')
|
||||||
|
# oauth2-provider
|
||||||
|
router.register('access-tokens', api.AccessTokenViewSet, 'access-token')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('<str:backend>/qr/unbind/', api.QRUnBindForUserApi.as_view(), name='qr-unbind'),
|
path('<str:backend>/qr/unbind/', api.QRUnBindForUserApi.as_view(), name='qr-unbind'),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import sys
|
|||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.db import close_old_connections
|
from django.db import close_old_connections
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
class CommonConfig(AppConfig):
|
class CommonConfig(AppConfig):
|
||||||
@@ -23,3 +24,21 @@ class CommonConfig(AppConfig):
|
|||||||
if not os.environ.get('DJANGO_DEBUG_SHELL'):
|
if not os.environ.get('DJANGO_DEBUG_SHELL'):
|
||||||
django_ready.send(CommonConfig)
|
django_ready.send(CommonConfig)
|
||||||
close_old_connections()
|
close_old_connections()
|
||||||
|
|
||||||
|
self._auto_register_jumpserver_client_if_not_exists()
|
||||||
|
|
||||||
|
def _auto_register_jumpserver_client_if_not_exists(self):
|
||||||
|
""" Auto register JumpServer Client application if not exists. """
|
||||||
|
from oauth2_provider.models import get_application_model
|
||||||
|
Application = get_application_model()
|
||||||
|
client_id = settings.OAUTH2_PROVIDER_CLIENT_ID
|
||||||
|
if Application.objects.filter(client_id=client_id).exists():
|
||||||
|
return
|
||||||
|
Application.objects.create(
|
||||||
|
name='JumpServer Client',
|
||||||
|
client_id=client_id,
|
||||||
|
client_type=Application.CLIENT_PUBLIC,
|
||||||
|
authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
|
||||||
|
redirect_uris=settings.OAUTH2_PROVIDER_CLIENT_REDIRECT_URI,
|
||||||
|
skip_authorization=True,
|
||||||
|
)
|
||||||
|
|||||||
@@ -280,7 +280,8 @@
|
|||||||
"CACertificate": "Ca certificate",
|
"CACertificate": "Ca certificate",
|
||||||
"CAS": "CAS",
|
"CAS": "CAS",
|
||||||
"CMPP2": "Cmpp v2.0",
|
"CMPP2": "Cmpp v2.0",
|
||||||
"CTYunPrivate": "eCloud Private Cloud",
|
"CTYun": "State Cloud",
|
||||||
|
"CTYunPrivate": "State Cloud(Private)",
|
||||||
"CalculationResults": "Error in cron expression",
|
"CalculationResults": "Error in cron expression",
|
||||||
"CallRecords": "Call Records",
|
"CallRecords": "Call Records",
|
||||||
"CanDragSelect": "Select by dragging; Empty means all selected",
|
"CanDragSelect": "Select by dragging; Empty means all selected",
|
||||||
@@ -1634,5 +1635,8 @@
|
|||||||
"selectedAssets": "Selected assets",
|
"selectedAssets": "Selected assets",
|
||||||
"setVariable": "Set variable",
|
"setVariable": "Set variable",
|
||||||
"userId": "User ID",
|
"userId": "User ID",
|
||||||
"userName": "User name"
|
"userName": "User name",
|
||||||
|
"AccessToken": "Access tokens",
|
||||||
|
"AccessTokenTip": "Access Token is a temporary credential generated through the OAuth2 (Authorization Code Grant) flow using the JumpServer client, which is used to access protected resources.",
|
||||||
|
"Revoke": "Revoke"
|
||||||
}
|
}
|
||||||
@@ -279,6 +279,7 @@
|
|||||||
"CACertificate": "CA 证书",
|
"CACertificate": "CA 证书",
|
||||||
"CAS": "CAS",
|
"CAS": "CAS",
|
||||||
"CMPP2": "CMPP v2.0",
|
"CMPP2": "CMPP v2.0",
|
||||||
|
"CTYun": "天翼云",
|
||||||
"CTYunPrivate": "天翼私有云",
|
"CTYunPrivate": "天翼私有云",
|
||||||
"CalculationResults": "cron 表达式错误",
|
"CalculationResults": "cron 表达式错误",
|
||||||
"CallRecords": "调用记录",
|
"CallRecords": "调用记录",
|
||||||
@@ -1644,5 +1645,8 @@
|
|||||||
"userId": "用户ID",
|
"userId": "用户ID",
|
||||||
"userName": "用户名",
|
"userName": "用户名",
|
||||||
"Risk": "风险",
|
"Risk": "风险",
|
||||||
"selectFiles": "已选择选择{number}文件"
|
"selectFiles": "已选择选择{number}文件",
|
||||||
|
"AccessToken": "访问令牌",
|
||||||
|
"AccessTokenTip": "访问令牌是通过 JumpServer 客户端使用 OAuth2(授权码授权)流程生成的临时凭证,用于访问受保护的资源。",
|
||||||
|
"Revoke": "撤销"
|
||||||
}
|
}
|
||||||
@@ -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'
|
||||||
@@ -735,6 +735,10 @@ class Config(dict):
|
|||||||
|
|
||||||
# MCP
|
# MCP
|
||||||
'MCP_ENABLED': False,
|
'MCP_ENABLED': False,
|
||||||
|
|
||||||
|
# oauth2_provider settings
|
||||||
|
'OAUTH2_PROVIDER_ACCESS_TOKEN_EXPIRE_SECONDS': 36000,
|
||||||
|
'OAUTH2_PROVIDER_REFRESH_TOKEN_EXPIRE_SECONDS': 2592000,
|
||||||
}
|
}
|
||||||
|
|
||||||
old_config_map = {
|
old_config_map = {
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ INSTALLED_APPS = [
|
|||||||
'drf_spectacular',
|
'drf_spectacular',
|
||||||
'drf_spectacular_sidecar',
|
'drf_spectacular_sidecar',
|
||||||
'django_cas_ng',
|
'django_cas_ng',
|
||||||
|
'oauth2_provider',
|
||||||
'channels',
|
'channels',
|
||||||
'django_filters',
|
'django_filters',
|
||||||
'bootstrap3',
|
'bootstrap3',
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ REST_FRAMEWORK = {
|
|||||||
),
|
),
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
# 'rest_framework.authentication.BasicAuthentication',
|
# 'rest_framework.authentication.BasicAuthentication',
|
||||||
|
"oauth2_provider.contrib.rest_framework.OAuth2Authentication",
|
||||||
'authentication.backends.drf.AccessTokenAuthentication',
|
'authentication.backends.drf.AccessTokenAuthentication',
|
||||||
'authentication.backends.drf.PrivateTokenAuthentication',
|
'authentication.backends.drf.PrivateTokenAuthentication',
|
||||||
'authentication.backends.drf.ServiceAuthentication',
|
'authentication.backends.drf.ServiceAuthentication',
|
||||||
@@ -222,3 +223,13 @@ PIICO_DRIVER_PATH = CONFIG.PIICO_DRIVER_PATH
|
|||||||
LEAK_PASSWORD_DB_PATH = CONFIG.LEAK_PASSWORD_DB_PATH
|
LEAK_PASSWORD_DB_PATH = CONFIG.LEAK_PASSWORD_DB_PATH
|
||||||
|
|
||||||
JUMPSERVER_UPTIME = int(time.time())
|
JUMPSERVER_UPTIME = int(time.time())
|
||||||
|
|
||||||
|
# OAuth2 Provider settings
|
||||||
|
OAUTH2_PROVIDER = {
|
||||||
|
'ALLOWED_REDIRECT_URI_SCHEMES': ['https', 'jms'],
|
||||||
|
'PKCE_REQUIRED': True,
|
||||||
|
'ACCESS_TOKEN_EXPIRE_SECONDS': CONFIG.OAUTH2_PROVIDER_ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||||
|
'REFRESH_TOKEN_EXPIRE_SECONDS': CONFIG.OAUTH2_PROVIDER_REFRESH_TOKEN_EXPIRE_SECONDS,
|
||||||
|
}
|
||||||
|
OAUTH2_PROVIDER_CLIENT_ID = 'FkkXFf0wPelYPIbvf0VElkZtyrw8TWIcyqakDgni'
|
||||||
|
OAUTH2_PROVIDER_CLIENT_REDIRECT_URI = 'jms://auth/callback'
|
||||||
@@ -54,6 +54,7 @@ app_view_patterns = [
|
|||||||
path('download/', views.ResourceDownload.as_view(), name='download'),
|
path('download/', views.ResourceDownload.as_view(), name='download'),
|
||||||
path('redirect/confirm/', views.RedirectConfirm.as_view(), name='redirect-confirm'),
|
path('redirect/confirm/', views.RedirectConfirm.as_view(), name='redirect-confirm'),
|
||||||
path('i18n/<str:lang>/', views.I18NView.as_view(), name='i18n-switch'),
|
path('i18n/<str:lang>/', views.I18NView.as_view(), name='i18n-switch'),
|
||||||
|
path('oauth2-provider/', include('oauth2_provider.urls', namespace='oauth2-provider')),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.XPACK_ENABLED:
|
if settings.XPACK_ENABLED:
|
||||||
|
|||||||
@@ -148,6 +148,6 @@ class RedirectConfirm(TemplateView):
|
|||||||
parsed = urlparse(url)
|
parsed = urlparse(url)
|
||||||
if not parsed.scheme or not parsed.netloc:
|
if not parsed.scheme or not parsed.netloc:
|
||||||
return False
|
return False
|
||||||
if parsed.scheme not in ['http', 'https']:
|
if parsed.scheme not in ['http', 'https', 'jms']:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -133,6 +133,11 @@ exclude_permissions = (
|
|||||||
('terminal', 'session', 'delete,change', 'command'),
|
('terminal', 'session', 'delete,change', 'command'),
|
||||||
('applications', '*', '*', '*'),
|
('applications', '*', '*', '*'),
|
||||||
('settings', 'chatprompt', 'add,delete,change', 'chatprompt'),
|
('settings', 'chatprompt', 'add,delete,change', 'chatprompt'),
|
||||||
|
('oauth2_provider', 'grant', '*', '*'),
|
||||||
|
('oauth2_provider', 'refreshtoken', '*', '*'),
|
||||||
|
('oauth2_provider', 'idtoken', '*', '*'),
|
||||||
|
('oauth2_provider', 'application', '*', '*'),
|
||||||
|
('oauth2_provider', 'accesstoken', 'add,change', 'accesstoken')
|
||||||
)
|
)
|
||||||
|
|
||||||
only_system_permissions = (
|
only_system_permissions = (
|
||||||
@@ -160,6 +165,7 @@ only_system_permissions = (
|
|||||||
('authentication', 'temptoken', '*', '*'),
|
('authentication', 'temptoken', '*', '*'),
|
||||||
('authentication', 'passkey', '*', '*'),
|
('authentication', 'passkey', '*', '*'),
|
||||||
('authentication', 'ssotoken', '*', '*'),
|
('authentication', 'ssotoken', '*', '*'),
|
||||||
|
('oauth2_provider', 'accesstoken', '*', '*'),
|
||||||
('tickets', '*', '*', '*'),
|
('tickets', '*', '*', '*'),
|
||||||
('orgs', 'organization', 'view', 'rootorg'),
|
('orgs', 'organization', 'view', 'rootorg'),
|
||||||
('terminal', 'applet', '*', '*'),
|
('terminal', 'applet', '*', '*'),
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ special_pid_mapper = {
|
|||||||
"rbac.view_systemtools": "view_workbench",
|
"rbac.view_systemtools": "view_workbench",
|
||||||
'tickets.view_ticket': 'tickets',
|
'tickets.view_ticket': 'tickets',
|
||||||
"audits.joblog": "job_audit",
|
"audits.joblog": "job_audit",
|
||||||
|
'oauth2_provider.accesstoken': 'authentication',
|
||||||
}
|
}
|
||||||
|
|
||||||
special_setting_pid_mapper = {
|
special_setting_pid_mapper = {
|
||||||
@@ -184,6 +185,8 @@ verbose_name_mapper = {
|
|||||||
'tickets.view_ticket': _("Ticket"),
|
'tickets.view_ticket': _("Ticket"),
|
||||||
'settings.setting': _("Common setting"),
|
'settings.setting': _("Common setting"),
|
||||||
'rbac.view_permission': _('View permission tree'),
|
'rbac.view_permission': _('View permission tree'),
|
||||||
|
'authentication.passkey': _("Passkey"),
|
||||||
|
'oauth2_provider.accesstoken': _("Access token"),
|
||||||
}
|
}
|
||||||
|
|
||||||
xpack_nodes = [
|
xpack_nodes = [
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ dependencies = [
|
|||||||
'sshpubkeys==3.3.1',
|
'sshpubkeys==3.3.1',
|
||||||
'uritemplate==4.1.1',
|
'uritemplate==4.1.1',
|
||||||
'vine==5.0.0',
|
'vine==5.0.0',
|
||||||
'werkzeug==3.0.6',
|
'werkzeug==3.1.4',
|
||||||
'unicodecsv==0.14.1',
|
'unicodecsv==0.14.1',
|
||||||
'httpsig==1.3.0',
|
'httpsig==1.3.0',
|
||||||
'treelib==1.6.4',
|
'treelib==1.6.4',
|
||||||
@@ -84,7 +84,7 @@ dependencies = [
|
|||||||
'rest-condition==1.0.3',
|
'rest-condition==1.0.3',
|
||||||
'drf-spectacular==0.28.0',
|
'drf-spectacular==0.28.0',
|
||||||
'pillow==10.2.0',
|
'pillow==10.2.0',
|
||||||
'pytz==2023.3',
|
'pytz==2025.2',
|
||||||
'django-proxy==1.2.2',
|
'django-proxy==1.2.2',
|
||||||
'python-daemon==3.0.1',
|
'python-daemon==3.0.1',
|
||||||
'eventlet==0.40.3',
|
'eventlet==0.40.3',
|
||||||
@@ -152,6 +152,7 @@ dependencies = [
|
|||||||
'playwright==1.55.0',
|
'playwright==1.55.0',
|
||||||
'pdf2image==1.17.0',
|
'pdf2image==1.17.0',
|
||||||
'drf-spectacular-sidecar==2025.8.1',
|
'drf-spectacular-sidecar==2025.8.1',
|
||||||
|
"django-oauth-toolkit==2.4.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|||||||
13
utils/delete_oauth2_provider_application.sh
Normal file
13
utils/delete_oauth2_provider_application.sh
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
|
||||||
|
function delete_oauth2_provider_applications() {
|
||||||
|
python3 ../apps/manage.py shell << EOF
|
||||||
|
from oauth2_provider.models import *
|
||||||
|
apps = Application.objects.all()
|
||||||
|
apps.delete()
|
||||||
|
print("OAuth2 Provider Applications deleted successfully!")
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_oauth2_provider_applications
|
||||||
Reference in New Issue
Block a user