mirror of https://github.com/jumpserver/jumpserver
commit
5a82174c54
|
@ -16,6 +16,11 @@ def create_internal_platform(apps, schema_editor):
|
||||||
name=name, defaults=defaults
|
name=name, defaults=defaults
|
||||||
)
|
)
|
||||||
|
|
||||||
|
win2016 = model.objects.filter(name='Windows2016').first()
|
||||||
|
if win2016:
|
||||||
|
win2016.internal = False
|
||||||
|
win2016.save(update_fields=['internal'])
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
|
|
@ -103,6 +103,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
||||||
'port': {'write_only': True},
|
'port': {'write_only': True},
|
||||||
'hardware_info': {'label': _('Hardware info'), 'read_only': True},
|
'hardware_info': {'label': _('Hardware info'), 'read_only': True},
|
||||||
'admin_user_display': {'label': _('Admin user display'), 'read_only': True},
|
'admin_user_display': {'label': _('Admin user display'), 'read_only': True},
|
||||||
|
'cpu_info': {'label': _('CPU info')},
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_fields(self):
|
def get_fields(self):
|
||||||
|
|
|
@ -42,7 +42,7 @@ class UserLoginLogSerializer(serializers.ModelSerializer):
|
||||||
fields = fields_small
|
fields = fields_small
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"user_agent": {'label': _('User agent')},
|
"user_agent": {'label': _('User agent')},
|
||||||
"reason_display": {'label': _('Reason')}
|
"reason_display": {'label': _('Reason display')}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,6 @@ from orgs.mixins.api import RootOrgViewMixin
|
||||||
from common.http import is_true
|
from common.http import is_true
|
||||||
from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user
|
from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user
|
||||||
from perms.models.asset_permission import Action
|
from perms.models.asset_permission import Action
|
||||||
from authentication.errors import NotHaveUpDownLoadPerm
|
|
||||||
|
|
||||||
from ..serializers import (
|
from ..serializers import (
|
||||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
||||||
|
@ -96,22 +95,26 @@ class ClientProtocolMixin:
|
||||||
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
|
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
|
||||||
token = self.create_token(user, asset, application, system_user)
|
token = self.create_token(user, asset, application, system_user)
|
||||||
|
|
||||||
|
# 设置磁盘挂载
|
||||||
if drives_redirect and asset:
|
if drives_redirect and asset:
|
||||||
systemuser_actions_mapper = get_asset_system_user_ids_with_actions_by_user(user, asset)
|
systemuser_actions_mapper = get_asset_system_user_ids_with_actions_by_user(user, asset)
|
||||||
actions = systemuser_actions_mapper.get(system_user.id, [])
|
actions = systemuser_actions_mapper.get(system_user.id, 0)
|
||||||
if actions & Action.UPDOWNLOAD:
|
if actions & Action.UPDOWNLOAD:
|
||||||
options['drivestoredirect:s'] = '*'
|
options['drivestoredirect:s'] = '*'
|
||||||
else:
|
|
||||||
raise NotHaveUpDownLoadPerm
|
|
||||||
|
|
||||||
|
# 全屏
|
||||||
options['screen mode id:i'] = '2' if full_screen else '1'
|
options['screen mode id:i'] = '2' if full_screen else '1'
|
||||||
|
|
||||||
|
# RDP Server 地址
|
||||||
address = settings.TERMINAL_RDP_ADDR
|
address = settings.TERMINAL_RDP_ADDR
|
||||||
if not address or address == 'localhost:3389':
|
if not address or address == 'localhost:3389':
|
||||||
address = self.request.get_host().split(':')[0] + ':3389'
|
address = self.request.get_host().split(':')[0] + ':3389'
|
||||||
options['full address:s'] = address
|
options['full address:s'] = address
|
||||||
|
# 用户名
|
||||||
options['username:s'] = '{}|{}'.format(user.username, token)
|
options['username:s'] = '{}|{}'.format(user.username, token)
|
||||||
if system_user.ad_domain:
|
if system_user.ad_domain:
|
||||||
options['domain:s'] = system_user.ad_domain
|
options['domain:s'] = system_user.ad_domain
|
||||||
|
# 宽高
|
||||||
if width and height:
|
if width and height:
|
||||||
options['desktopwidth:i'] = width
|
options['desktopwidth:i'] = width
|
||||||
options['desktopheight:i'] = height
|
options['desktopheight:i'] = height
|
||||||
|
@ -160,13 +163,16 @@ class ClientProtocolMixin:
|
||||||
asset, application, system_user, user = self.get_request_resource(serializer)
|
asset, application, system_user, user = self.get_request_resource(serializer)
|
||||||
protocol = system_user.protocol
|
protocol = system_user.protocol
|
||||||
username = user.username
|
username = user.username
|
||||||
name = ''
|
|
||||||
if protocol == 'rdp':
|
if protocol == 'rdp':
|
||||||
name, config = self.get_rdp_file_content(serializer)
|
name, config = self.get_rdp_file_content(serializer)
|
||||||
elif protocol == 'vnc':
|
elif protocol == 'ssh':
|
||||||
raise HttpResponse(status=404, data={"error": "VNC not support"})
|
# Todo:
|
||||||
else:
|
name = ''
|
||||||
config = 'ssh://system_user@asset@user@jumpserver-ssh'
|
config = 'ssh://system_user@asset@user@jumpserver-ssh'
|
||||||
|
else:
|
||||||
|
raise ValueError('Protocol not support: {}'.format(protocol))
|
||||||
|
|
||||||
filename = "{}-{}-jumpserver".format(username, name)
|
filename = "{}-{}-jumpserver".format(username, name)
|
||||||
data = {
|
data = {
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
|
@ -179,8 +185,13 @@ class ClientProtocolMixin:
|
||||||
@action(methods=['POST', 'GET'], detail=False, url_path='client-url', permission_classes=[IsValidUser])
|
@action(methods=['POST', 'GET'], detail=False, url_path='client-url', permission_classes=[IsValidUser])
|
||||||
def get_client_protocol_url(self, request, *args, **kwargs):
|
def get_client_protocol_url(self, request, *args, **kwargs):
|
||||||
serializer = self.get_valid_serializer()
|
serializer = self.get_valid_serializer()
|
||||||
protocol_data = self.get_client_protocol_data(serializer)
|
try:
|
||||||
protocol_data = base64.b64encode(json.dumps(protocol_data).encode()).decode()
|
protocol_data = self.get_client_protocol_data(serializer)
|
||||||
|
except ValueError as e:
|
||||||
|
return Response({'error': str(e)}, status=401)
|
||||||
|
|
||||||
|
protocol_data = json.dumps(protocol_data).encode()
|
||||||
|
protocol_data = base64.b64encode(protocol_data).decode()
|
||||||
data = {
|
data = {
|
||||||
'url': 'jms://{}'.format(protocol_data),
|
'url': 'jms://{}'.format(protocol_data),
|
||||||
}
|
}
|
||||||
|
@ -348,14 +359,12 @@ class UserConnectionTokenViewSet(
|
||||||
raise serializers.ValidationError("User not valid, disabled or expired")
|
raise serializers.ValidationError("User not valid, disabled or expired")
|
||||||
|
|
||||||
system_user = get_object_or_404(SystemUser, id=value.get('system_user'))
|
system_user = get_object_or_404(SystemUser, id=value.get('system_user'))
|
||||||
|
|
||||||
asset = None
|
asset = None
|
||||||
app = None
|
app = None
|
||||||
if value.get('type') == 'asset':
|
if value.get('type') == 'asset':
|
||||||
asset = get_object_or_404(Asset, id=value.get('asset'))
|
asset = get_object_or_404(Asset, id=value.get('asset'))
|
||||||
if not asset.is_active:
|
if not asset.is_active:
|
||||||
raise serializers.ValidationError("Asset disabled")
|
raise serializers.ValidationError("Asset disabled")
|
||||||
|
|
||||||
has_perm, expired_at = asset_validate_permission(user, asset, system_user, 'connect')
|
has_perm, expired_at = asset_validate_permission(user, asset, system_user, 'connect')
|
||||||
else:
|
else:
|
||||||
app = get_object_or_404(Application, id=value.get('application'))
|
app = get_object_or_404(Application, id=value.get('application'))
|
||||||
|
|
|
@ -44,7 +44,7 @@ class MFASendCodeApi(AuthMixin, CreateAPIView):
|
||||||
else:
|
else:
|
||||||
user = get_object_or_404(User, username=username)
|
user = get_object_or_404(User, username=username)
|
||||||
|
|
||||||
mfa_backend = user.get_mfa_backend_by_type(mfa_type)
|
mfa_backend = user.get_active_mfa_backend_by_type(mfa_type)
|
||||||
if not mfa_backend or not mfa_backend.challenge_required:
|
if not mfa_backend or not mfa_backend.challenge_required:
|
||||||
raise ValidationError('MFA type not support: {} {}'.format(mfa_type, mfa_backend))
|
raise ValidationError('MFA type not support: {} {}'.format(mfa_type, mfa_backend))
|
||||||
mfa_backend.send_challenge()
|
mfa_backend.send_challenge()
|
||||||
|
@ -100,7 +100,7 @@ class UserOtpVerifyApi(CreateAPIView):
|
||||||
request.session["MFA_VERIFY_TIME"] = int(time.time())
|
request.session["MFA_VERIFY_TIME"] = int(time.time())
|
||||||
return Response({"ok": "1"})
|
return Response({"ok": "1"})
|
||||||
else:
|
else:
|
||||||
return Response({"error": _("Code is invalid") + ", " + error}, status=400)
|
return Response({"error": _("Code is invalid, {}").format(error)}, status=400)
|
||||||
|
|
||||||
def get_permissions(self):
|
def get_permissions(self):
|
||||||
if self.request.method.lower() == 'get' \
|
if self.request.method.lower() == 'get' \
|
||||||
|
|
|
@ -33,8 +33,8 @@ class TokenCreateApi(AuthMixin, CreateAPIView):
|
||||||
self.check_user_mfa_if_need(user)
|
self.check_user_mfa_if_need(user)
|
||||||
self.check_user_login_confirm_if_need(user)
|
self.check_user_login_confirm_if_need(user)
|
||||||
self.send_auth_signal(success=True, user=user)
|
self.send_auth_signal(success=True, user=user)
|
||||||
self.clear_auth_mark()
|
|
||||||
resp = super().create(request, *args, **kwargs)
|
resp = super().create(request, *args, **kwargs)
|
||||||
|
self.clear_auth_mark()
|
||||||
return resp
|
return resp
|
||||||
except errors.AuthFailedError as e:
|
except errors.AuthFailedError as e:
|
||||||
return Response(e.as_data(), status=400)
|
return Response(e.as_data(), status=400)
|
||||||
|
|
|
@ -60,13 +60,12 @@ block_mfa_msg = _(
|
||||||
"(please contact admin to unlock it or try again after {} minutes)"
|
"(please contact admin to unlock it or try again after {} minutes)"
|
||||||
)
|
)
|
||||||
mfa_error_msg = _(
|
mfa_error_msg = _(
|
||||||
"{error},"
|
"{error}, "
|
||||||
"You can also try {times_try} times "
|
"You can also try {times_try} times "
|
||||||
"(The account will be temporarily locked for {block_time} minutes)"
|
"(The account will be temporarily locked for {block_time} minutes)"
|
||||||
)
|
)
|
||||||
mfa_required_msg = _("MFA required")
|
mfa_required_msg = _("MFA required")
|
||||||
mfa_unset_msg = _("MFA not set, please set it first")
|
mfa_unset_msg = _("MFA not set, please set it first")
|
||||||
otp_unset_msg = _("OTP not set, please set it first")
|
|
||||||
login_confirm_required_msg = _("Login confirm required")
|
login_confirm_required_msg = _("Login confirm required")
|
||||||
login_confirm_wait_msg = _("Wait login confirm ticket for accept")
|
login_confirm_wait_msg = _("Wait login confirm ticket for accept")
|
||||||
login_confirm_error_msg = _("Login confirm ticket was {}")
|
login_confirm_error_msg = _("Login confirm ticket was {}")
|
||||||
|
@ -162,13 +161,11 @@ class BlockMFAError(AuthFailedNeedLogMixin, AuthFailedError):
|
||||||
super().__init__(username=username, request=request, ip=ip)
|
super().__init__(username=username, request=request, ip=ip)
|
||||||
|
|
||||||
|
|
||||||
class MFAUnsetError(AuthFailedNeedLogMixin, AuthFailedError):
|
class MFAUnsetError(Exception):
|
||||||
error = reason_mfa_unset
|
error = reason_mfa_unset
|
||||||
msg = mfa_unset_msg
|
msg = mfa_unset_msg
|
||||||
|
|
||||||
def __init__(self, user, request, url):
|
def __init__(self, user, request, url):
|
||||||
super().__init__(username=user.username, request=request)
|
|
||||||
self.user = user
|
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
|
|
||||||
|
@ -180,6 +177,14 @@ class BlockLoginError(AuthFailedNeedBlockMixin, AuthFailedError):
|
||||||
super().__init__(username=username, ip=ip)
|
super().__init__(username=username, ip=ip)
|
||||||
|
|
||||||
|
|
||||||
|
class BlockGlobalIpLoginError(AuthFailedError):
|
||||||
|
error = 'block_global_ip_login'
|
||||||
|
|
||||||
|
def __init__(self, username, ip):
|
||||||
|
self.msg = _("IP is not allowed")
|
||||||
|
super().__init__(username=username, ip=ip)
|
||||||
|
|
||||||
|
|
||||||
class SessionEmptyError(AuthFailedError):
|
class SessionEmptyError(AuthFailedError):
|
||||||
msg = session_empty_msg
|
msg = session_empty_msg
|
||||||
error = 'session_empty'
|
error = 'session_empty'
|
||||||
|
@ -340,27 +345,16 @@ class PasswordInvalid(JMSException):
|
||||||
default_detail = _('Your password is invalid')
|
default_detail = _('Your password is invalid')
|
||||||
|
|
||||||
|
|
||||||
class NotHaveUpDownLoadPerm(JMSException):
|
|
||||||
status_code = status.HTTP_403_FORBIDDEN
|
|
||||||
code = 'not_have_up_down_load_perm'
|
|
||||||
default_detail = _('No upload or download permission')
|
|
||||||
|
|
||||||
|
|
||||||
class OTPBindRequiredError(JMSException):
|
|
||||||
default_detail = otp_unset_msg
|
|
||||||
|
|
||||||
def __init__(self, url, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.url = url
|
|
||||||
|
|
||||||
|
|
||||||
class MFACodeRequiredError(AuthFailedError):
|
class MFACodeRequiredError(AuthFailedError):
|
||||||
|
error = 'mfa_code_required'
|
||||||
msg = _("Please enter MFA code")
|
msg = _("Please enter MFA code")
|
||||||
|
|
||||||
|
|
||||||
class SMSCodeRequiredError(AuthFailedError):
|
class SMSCodeRequiredError(AuthFailedError):
|
||||||
|
error = 'sms_code_required'
|
||||||
msg = _("Please enter SMS code")
|
msg = _("Please enter SMS code")
|
||||||
|
|
||||||
|
|
||||||
class UserPhoneNotSet(AuthFailedError):
|
class UserPhoneNotSet(AuthFailedError):
|
||||||
|
error = 'phone_not_set'
|
||||||
msg = _('Phone not set')
|
msg = _('Phone not set')
|
||||||
|
|
|
@ -10,6 +10,7 @@ otp_failed_msg = _("OTP code invalid, or server time error")
|
||||||
class MFAOtp(BaseMFA):
|
class MFAOtp(BaseMFA):
|
||||||
name = 'otp'
|
name = 'otp'
|
||||||
display_name = _('OTP')
|
display_name = _('OTP')
|
||||||
|
placeholder = _('OTP verification code')
|
||||||
|
|
||||||
def check_code(self, code):
|
def check_code(self, code):
|
||||||
from users.utils import check_otp_code
|
from users.utils import check_otp_code
|
||||||
|
|
|
@ -9,7 +9,8 @@ mfa_failed_msg = _("Radius verify code invalid")
|
||||||
|
|
||||||
class MFARadius(BaseMFA):
|
class MFARadius(BaseMFA):
|
||||||
name = 'otp_radius'
|
name = 'otp_radius'
|
||||||
display_name = _('Radius MFA')
|
display_name = 'Radius'
|
||||||
|
placeholder = _("Radius verification code")
|
||||||
|
|
||||||
def check_code(self, code):
|
def check_code(self, code):
|
||||||
assert self.is_authenticated()
|
assert self.is_authenticated()
|
||||||
|
|
|
@ -19,8 +19,12 @@ class MFASms(BaseMFA):
|
||||||
|
|
||||||
def check_code(self, code):
|
def check_code(self, code):
|
||||||
assert self.is_authenticated()
|
assert self.is_authenticated()
|
||||||
ok = self.sms.verify(code)
|
ok = False
|
||||||
msg = '' if ok else sms_failed_msg
|
msg = ''
|
||||||
|
try:
|
||||||
|
ok = self.sms.verify(code)
|
||||||
|
except Exception as e:
|
||||||
|
msg = str(e)
|
||||||
return ok, msg
|
return ok, msg
|
||||||
|
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
|
|
|
@ -8,7 +8,6 @@ from typing import Callable
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
@ -18,10 +17,10 @@ from django.contrib.auth import (
|
||||||
)
|
)
|
||||||
from django.shortcuts import reverse, redirect, get_object_or_404
|
from django.shortcuts import reverse, redirect, get_object_or_404
|
||||||
|
|
||||||
from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil
|
from common.utils import get_request_ip, get_logger, bulk_get, FlashMessageUtil
|
||||||
from acls.models import LoginACL
|
from acls.models import LoginACL
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from users.utils import LoginBlockUtil, MFABlockUtils
|
from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil
|
||||||
from . import errors
|
from . import errors
|
||||||
from .utils import rsa_decrypt, gen_key_pair
|
from .utils import rsa_decrypt, gen_key_pair
|
||||||
from .signals import post_auth_success, post_auth_failed
|
from .signals import post_auth_success, post_auth_failed
|
||||||
|
@ -76,7 +75,9 @@ def authenticate(request=None, **credentials):
|
||||||
return user
|
return user
|
||||||
|
|
||||||
# The credentials supplied are invalid to all backends, fire signal
|
# The credentials supplied are invalid to all backends, fire signal
|
||||||
user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request)
|
user_login_failed.send(
|
||||||
|
sender=__name__, credentials=_clean_credentials(credentials), request=request
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
auth.authenticate = authenticate
|
auth.authenticate = authenticate
|
||||||
|
@ -209,6 +210,10 @@ class AuthPreCheckMixin:
|
||||||
|
|
||||||
def _check_is_block(self, username, raise_exception=True):
|
def _check_is_block(self, username, raise_exception=True):
|
||||||
ip = self.get_request_ip()
|
ip = self.get_request_ip()
|
||||||
|
|
||||||
|
if LoginIpBlockUtil(ip).is_block():
|
||||||
|
raise errors.BlockGlobalIpLoginError(username=username, ip=ip)
|
||||||
|
|
||||||
is_block = LoginBlockUtil(username, ip).is_block()
|
is_block = LoginBlockUtil(username, ip).is_block()
|
||||||
if not is_block:
|
if not is_block:
|
||||||
return
|
return
|
||||||
|
@ -224,6 +229,7 @@ class AuthPreCheckMixin:
|
||||||
username = self.request.data.get("username")
|
username = self.request.data.get("username")
|
||||||
else:
|
else:
|
||||||
username = self.request.POST.get("username")
|
username = self.request.POST.get("username")
|
||||||
|
|
||||||
self._check_is_block(username, raise_exception)
|
self._check_is_block(username, raise_exception)
|
||||||
|
|
||||||
def _check_only_allow_exists_user_auth(self, username):
|
def _check_only_allow_exists_user_auth(self, username):
|
||||||
|
@ -242,16 +248,24 @@ class MFAMixin:
|
||||||
get_user_from_session: Callable
|
get_user_from_session: Callable
|
||||||
get_request_ip: Callable
|
get_request_ip: Callable
|
||||||
|
|
||||||
|
def _check_if_no_active_mfa(self, user):
|
||||||
|
active_mfa_mapper = user.active_mfa_backends_mapper
|
||||||
|
if not active_mfa_mapper:
|
||||||
|
url = reverse('authentication:user-otp-enable-start')
|
||||||
|
raise errors.MFAUnsetError(user, self.request, url)
|
||||||
|
|
||||||
def _check_login_page_mfa_if_need(self, user):
|
def _check_login_page_mfa_if_need(self, user):
|
||||||
if not settings.SECURITY_MFA_IN_LOGIN_PAGE:
|
if not settings.SECURITY_MFA_IN_LOGIN_PAGE:
|
||||||
return
|
return
|
||||||
|
self._check_if_no_active_mfa(user)
|
||||||
|
|
||||||
request = self.request
|
request = self.request
|
||||||
data = request.data if hasattr(request, 'data') else request.POST
|
data = request.data if hasattr(request, 'data') else request.POST
|
||||||
code = data.get('code')
|
code = data.get('code')
|
||||||
mfa_type = data.get('mfa_type', 'otp')
|
mfa_type = data.get('mfa_type', 'otp')
|
||||||
|
|
||||||
if not code:
|
if not code:
|
||||||
raise errors.MFACodeRequiredError
|
return
|
||||||
self._do_check_user_mfa(code, mfa_type, user=user)
|
self._do_check_user_mfa(code, mfa_type, user=user)
|
||||||
|
|
||||||
def check_user_mfa_if_need(self, user):
|
def check_user_mfa_if_need(self, user):
|
||||||
|
@ -260,10 +274,9 @@ class MFAMixin:
|
||||||
if not user.mfa_enabled:
|
if not user.mfa_enabled:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self._check_if_no_active_mfa(user)
|
||||||
|
|
||||||
active_mfa_mapper = user.active_mfa_backends_mapper
|
active_mfa_mapper = user.active_mfa_backends_mapper
|
||||||
if not active_mfa_mapper:
|
|
||||||
url = reverse('authentication:user-otp-enable-start')
|
|
||||||
raise errors.MFAUnsetError(user, self.request, url)
|
|
||||||
raise errors.MFARequiredError(mfa_types=tuple(active_mfa_mapper.keys()))
|
raise errors.MFARequiredError(mfa_types=tuple(active_mfa_mapper.keys()))
|
||||||
|
|
||||||
def mark_mfa_ok(self, mfa_type):
|
def mark_mfa_ok(self, mfa_type):
|
||||||
|
@ -299,10 +312,13 @@ class MFAMixin:
|
||||||
|
|
||||||
ok = False
|
ok = False
|
||||||
mfa_backend = user.get_mfa_backend_by_type(mfa_type)
|
mfa_backend = user.get_mfa_backend_by_type(mfa_type)
|
||||||
if mfa_backend:
|
backend_error = _('The MFA type ({}) is not enabled')
|
||||||
ok, msg = mfa_backend.check_code(code)
|
if not mfa_backend:
|
||||||
|
msg = backend_error.format(mfa_type)
|
||||||
|
elif not mfa_backend.is_active():
|
||||||
|
msg = backend_error.format(mfa_backend.display_name)
|
||||||
else:
|
else:
|
||||||
msg = _('The MFA type({}) is not supported'.format(mfa_type))
|
ok, msg = mfa_backend.check_code(code)
|
||||||
|
|
||||||
if ok:
|
if ok:
|
||||||
self.mark_mfa_ok(mfa_type)
|
self.mark_mfa_ok(mfa_type)
|
||||||
|
|
|
@ -54,9 +54,9 @@ class BearerTokenSerializer(serializers.Serializer):
|
||||||
user.last_login = timezone.now()
|
user.last_login = timezone.now()
|
||||||
user.save(update_fields=['last_login'])
|
user.save(update_fields=['last_login'])
|
||||||
|
|
||||||
def create(self, validated_data):
|
def get_request_user(self):
|
||||||
request = self.context.get('request')
|
request = self.context.get('request')
|
||||||
if request.user and not request.user.is_anonymous:
|
if request.user and request.user.is_authenticated:
|
||||||
user = request.user
|
user = request.user
|
||||||
else:
|
else:
|
||||||
user_id = request.session.get('user_id')
|
user_id = request.session.get('user_id')
|
||||||
|
@ -65,6 +65,12 @@ class BearerTokenSerializer(serializers.Serializer):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
"user id {} not exist".format(user_id)
|
"user id {} not exist".format(user_id)
|
||||||
)
|
)
|
||||||
|
return user
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
request = self.context.get('request')
|
||||||
|
user = self.get_request_user()
|
||||||
|
|
||||||
token, date_expired = user.create_bearer_token(request)
|
token, date_expired = user.create_bearer_token(request)
|
||||||
self.update_last_login(user)
|
self.update_last_login(user)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
{% load i18n %}
|
||||||
|
<p>{% trans 'Hello' %} {{ name }},</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% trans 'Your public key has just been successfully updated' %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br />
|
||||||
|
<b>{% trans 'Browser' %}:</b> {{ browser }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{% trans 'If the public key update was not initiated by you, your account may have security issues' %} <br />
|
||||||
|
{% trans 'If you have any questions, you can contact the administrator' %}
|
||||||
|
</p>
|
|
@ -109,7 +109,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-con {
|
.select-con {
|
||||||
width: 22%;
|
width: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mfa-div {
|
.mfa-div {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans 'MFA' %}
|
{% trans 'MFA Auth' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
|
@ -122,10 +122,10 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||||
self.request.session.set_test_cookie()
|
self.request.session.set_test_cookie()
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
except (
|
except (
|
||||||
|
errors.MFAUnsetError,
|
||||||
errors.PasswordTooSimple,
|
errors.PasswordTooSimple,
|
||||||
errors.PasswordRequireResetError,
|
errors.PasswordRequireResetError,
|
||||||
errors.PasswordNeedUpdate,
|
errors.PasswordNeedUpdate
|
||||||
errors.OTPBindRequiredError
|
|
||||||
) as e:
|
) as e:
|
||||||
return redirect(e.url)
|
return redirect(e.url)
|
||||||
except (
|
except (
|
||||||
|
@ -133,7 +133,8 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||||
errors.BlockMFAError,
|
errors.BlockMFAError,
|
||||||
errors.MFACodeRequiredError,
|
errors.MFACodeRequiredError,
|
||||||
errors.SMSCodeRequiredError,
|
errors.SMSCodeRequiredError,
|
||||||
errors.UserPhoneNotSet
|
errors.UserPhoneNotSet,
|
||||||
|
errors.BlockGlobalIpLoginError
|
||||||
) as e:
|
) as e:
|
||||||
form.add_error('code', e.msg)
|
form.add_error('code', e.msg)
|
||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
|
@ -292,6 +292,7 @@ class Config(dict):
|
||||||
'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True,
|
'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True,
|
||||||
'SECURITY_VIEW_AUTH_NEED_MFA': True,
|
'SECURITY_VIEW_AUTH_NEED_MFA': True,
|
||||||
'SECURITY_LOGIN_LIMIT_COUNT': 7,
|
'SECURITY_LOGIN_LIMIT_COUNT': 7,
|
||||||
|
'SECURITY_LOGIN_IP_BLACK_LIST': [],
|
||||||
'SECURITY_LOGIN_LIMIT_TIME': 30,
|
'SECURITY_LOGIN_LIMIT_TIME': 30,
|
||||||
'SECURITY_MAX_IDLE_TIME': 30,
|
'SECURITY_MAX_IDLE_TIME': 30,
|
||||||
'SECURITY_PASSWORD_EXPIRATION_TIME': 9999,
|
'SECURITY_PASSWORD_EXPIRATION_TIME': 9999,
|
||||||
|
|
|
@ -34,9 +34,10 @@ TERMINAL_REPLAY_STORAGE = CONFIG.TERMINAL_REPLAY_STORAGE
|
||||||
SECURITY_MFA_AUTH = CONFIG.SECURITY_MFA_AUTH
|
SECURITY_MFA_AUTH = CONFIG.SECURITY_MFA_AUTH
|
||||||
SECURITY_COMMAND_EXECUTION = CONFIG.SECURITY_COMMAND_EXECUTION
|
SECURITY_COMMAND_EXECUTION = CONFIG.SECURITY_COMMAND_EXECUTION
|
||||||
SECURITY_LOGIN_LIMIT_COUNT = CONFIG.SECURITY_LOGIN_LIMIT_COUNT
|
SECURITY_LOGIN_LIMIT_COUNT = CONFIG.SECURITY_LOGIN_LIMIT_COUNT
|
||||||
|
SECURITY_LOGIN_IP_BLACK_LIST = CONFIG.SECURITY_LOGIN_IP_BLACK_LIST
|
||||||
SECURITY_LOGIN_LIMIT_TIME = CONFIG.SECURITY_LOGIN_LIMIT_TIME # Unit: minute
|
SECURITY_LOGIN_LIMIT_TIME = CONFIG.SECURITY_LOGIN_LIMIT_TIME # Unit: minute
|
||||||
SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME # Unit: minute
|
SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME # Unit: minute
|
||||||
SECURITY_PASSWORD_EXPIRATION_TIME = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day
|
SECURITY_PASSWORD_EXPIRATION_TIME = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day
|
||||||
SECURITY_PASSWORD_MIN_LENGTH = CONFIG.SECURITY_PASSWORD_MIN_LENGTH # Unit: bit
|
SECURITY_PASSWORD_MIN_LENGTH = CONFIG.SECURITY_PASSWORD_MIN_LENGTH # Unit: bit
|
||||||
SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH = CONFIG.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH # Unit: bit
|
SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH = CONFIG.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH # Unit: bit
|
||||||
OLD_PASSWORD_HISTORY_LIMIT_COUNT = CONFIG.OLD_PASSWORD_HISTORY_LIMIT_COUNT
|
OLD_PASSWORD_HISTORY_LIMIT_COUNT = CONFIG.OLD_PASSWORD_HISTORY_LIMIT_COUNT
|
||||||
|
@ -118,7 +119,6 @@ REFERER_CHECK_ENABLED = CONFIG.REFERER_CHECK_ENABLED
|
||||||
CONNECTION_TOKEN_ENABLED = CONFIG.CONNECTION_TOKEN_ENABLED
|
CONNECTION_TOKEN_ENABLED = CONFIG.CONNECTION_TOKEN_ENABLED
|
||||||
FORGOT_PASSWORD_URL = CONFIG.FORGOT_PASSWORD_URL
|
FORGOT_PASSWORD_URL = CONFIG.FORGOT_PASSWORD_URL
|
||||||
|
|
||||||
|
|
||||||
# 自定义默认组织名
|
# 自定义默认组织名
|
||||||
GLOBAL_ORG_DISPLAY_NAME = CONFIG.GLOBAL_ORG_DISPLAY_NAME
|
GLOBAL_ORG_DISPLAY_NAME = CONFIG.GLOBAL_ORG_DISPLAY_NAME
|
||||||
HEALTH_CHECK_TOKEN = CONFIG.HEALTH_CHECK_TOKEN
|
HEALTH_CHECK_TOKEN = CONFIG.HEALTH_CHECK_TOKEN
|
||||||
|
@ -135,7 +135,6 @@ CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = CONFIG.CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS
|
||||||
|
|
||||||
XRDP_ENABLED = CONFIG.XRDP_ENABLED
|
XRDP_ENABLED = CONFIG.XRDP_ENABLED
|
||||||
|
|
||||||
|
|
||||||
# SMS enabled
|
# SMS enabled
|
||||||
SMS_ENABLED = CONFIG.SMS_ENABLED
|
SMS_ENABLED = CONFIG.SMS_ENABLED
|
||||||
SMS_BACKEND = CONFIG.SMS_BACKEND
|
SMS_BACKEND = CONFIG.SMS_BACKEND
|
||||||
|
@ -155,3 +154,7 @@ TENCENT_SMS_SIGN_AND_TEMPLATES = CONFIG.TENCENT_SMS_SIGN_AND_TEMPLATES
|
||||||
# 公告
|
# 公告
|
||||||
ANNOUNCEMENT_ENABLED = CONFIG.ANNOUNCEMENT_ENABLED
|
ANNOUNCEMENT_ENABLED = CONFIG.ANNOUNCEMENT_ENABLED
|
||||||
ANNOUNCEMENT = CONFIG.ANNOUNCEMENT
|
ANNOUNCEMENT = CONFIG.ANNOUNCEMENT
|
||||||
|
|
||||||
|
# help
|
||||||
|
HELP_DOCUMENT_URL = CONFIG.HELP_DOCUMENT_URL
|
||||||
|
HELP_SUPPORT_URL = CONFIG.HELP_SUPPORT_URL
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:925c5a219a4ee6835ad59e3b8e9f7ea5074ee3df6527c0f73ef1a50eaedaf59c
|
oid sha256:4fea2cdf5a5477757cb95ff36016ed754fd65f839c12adbac9247ebdcca138ef
|
||||||
size 91777
|
size 93440
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -28,7 +28,7 @@ class AssetSystemUserSerializer(serializers.ModelSerializer):
|
||||||
model = SystemUser
|
model = SystemUser
|
||||||
only_fields = (
|
only_fields = (
|
||||||
'id', 'name', 'username', 'priority', 'protocol', 'login_mode',
|
'id', 'name', 'username', 'priority', 'protocol', 'login_mode',
|
||||||
'sftp_root', 'username_same_with_user',
|
'sftp_root', 'username_same_with_user', 'su_enabled', 'su_from',
|
||||||
)
|
)
|
||||||
fields = list(only_fields) + ["actions"]
|
fields = list(only_fields) + ["actions"]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
|
@ -86,7 +86,7 @@ def get_asset_system_user_ids_with_actions_by_user(user: User, asset: Asset):
|
||||||
|
|
||||||
def has_asset_system_permission(user: User, asset: Asset, system_user: SystemUser):
|
def has_asset_system_permission(user: User, asset: Asset, system_user: SystemUser):
|
||||||
systemuser_actions_mapper = get_asset_system_user_ids_with_actions_by_user(user, asset)
|
systemuser_actions_mapper = get_asset_system_user_ids_with_actions_by_user(user, asset)
|
||||||
actions = systemuser_actions_mapper.get(system_user.id, [])
|
actions = systemuser_actions_mapper.get(system_user.id, 0)
|
||||||
if actions:
|
if actions:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -59,6 +59,8 @@ class PublicSettingApi(generics.RetrieveAPIView):
|
||||||
"XRDP_ENABLED": settings.XRDP_ENABLED,
|
"XRDP_ENABLED": settings.XRDP_ENABLED,
|
||||||
"ANNOUNCEMENT_ENABLED": settings.ANNOUNCEMENT_ENABLED,
|
"ANNOUNCEMENT_ENABLED": settings.ANNOUNCEMENT_ENABLED,
|
||||||
"ANNOUNCEMENT": settings.ANNOUNCEMENT,
|
"ANNOUNCEMENT": settings.ANNOUNCEMENT,
|
||||||
|
"HELP_DOCUMENT_URL": settings.HELP_DOCUMENT_URL,
|
||||||
|
"HELP_SUPPORT_URL": settings.HELP_SUPPORT_URL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return instance
|
return instance
|
||||||
|
|
|
@ -25,3 +25,8 @@ class CleaningSerializer(serializers.Serializer):
|
||||||
min_value=1, max_value=9999,
|
min_value=1, max_value=9999,
|
||||||
label=_("Cloud sync record keep days"), help_text=_("Unit: day")
|
label=_("Cloud sync record keep days"), help_text=_("Unit: day")
|
||||||
)
|
)
|
||||||
|
TERMINAL_SESSION_KEEP_DURATION = serializers.IntegerField(
|
||||||
|
min_value=1, max_value=99999, required=True, label=_('Session keep duration'),
|
||||||
|
help_text=_('Unit: days, Session, record, command will be delete if more than duration, only in database')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
|
||||||
|
|
||||||
|
|
||||||
class SecurityPasswordRuleSerializer(serializers.Serializer):
|
class SecurityPasswordRuleSerializer(serializers.Serializer):
|
||||||
SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField(
|
SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField(
|
||||||
|
@ -14,9 +16,24 @@ class SecurityPasswordRuleSerializer(serializers.Serializer):
|
||||||
SECURITY_PASSWORD_UPPER_CASE = serializers.BooleanField(
|
SECURITY_PASSWORD_UPPER_CASE = serializers.BooleanField(
|
||||||
required=False, label=_('Must contain capital')
|
required=False, label=_('Must contain capital')
|
||||||
)
|
)
|
||||||
SECURITY_PASSWORD_LOWER_CASE = serializers.BooleanField(required=False, label=_('Must contain lowercase'))
|
SECURITY_PASSWORD_LOWER_CASE = serializers.BooleanField(
|
||||||
SECURITY_PASSWORD_NUMBER = serializers.BooleanField(required=False, label=_('Must contain numeric'))
|
required=False, label=_('Must contain lowercase')
|
||||||
SECURITY_PASSWORD_SPECIAL_CHAR = serializers.BooleanField(required=False, label=_('Must contain special'))
|
)
|
||||||
|
SECURITY_PASSWORD_NUMBER = serializers.BooleanField(
|
||||||
|
required=False, label=_('Must contain numeric')
|
||||||
|
)
|
||||||
|
SECURITY_PASSWORD_SPECIAL_CHAR = serializers.BooleanField(
|
||||||
|
required=False, label=_('Must contain special')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ip_child_validator(ip_child):
|
||||||
|
is_valid = is_ip_address(ip_child) \
|
||||||
|
or is_ip_network(ip_child) \
|
||||||
|
or is_ip_segment(ip_child)
|
||||||
|
if not is_valid:
|
||||||
|
error = _('IP address invalid: `{}`').format(ip_child)
|
||||||
|
raise serializers.ValidationError(error)
|
||||||
|
|
||||||
|
|
||||||
class SecurityAuthSerializer(serializers.Serializer):
|
class SecurityAuthSerializer(serializers.Serializer):
|
||||||
|
@ -40,6 +57,14 @@ class SecurityAuthSerializer(serializers.Serializer):
|
||||||
'no login is allowed during this time interval.'
|
'no login is allowed during this time interval.'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
SECURITY_LOGIN_IP_BLACK_LIST = serializers.ListField(
|
||||||
|
default=[], label=_('IP Black List'), allow_empty=True,
|
||||||
|
child=serializers.CharField(max_length=1024, validators=[ip_child_validator]),
|
||||||
|
help_text=_(
|
||||||
|
'Format for comma-delimited string. Such as: '
|
||||||
|
'192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64'
|
||||||
|
)
|
||||||
|
)
|
||||||
SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField(
|
SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField(
|
||||||
min_value=1, max_value=99999, required=True,
|
min_value=1, max_value=99999, required=True,
|
||||||
label=_('User password expiration'),
|
label=_('User password expiration'),
|
||||||
|
@ -72,7 +97,9 @@ class SecurityAuthSerializer(serializers.Serializer):
|
||||||
SECURITY_MFA_VERIFY_TTL = serializers.IntegerField(
|
SECURITY_MFA_VERIFY_TTL = serializers.IntegerField(
|
||||||
min_value=5, max_value=60 * 60 * 10,
|
min_value=5, max_value=60 * 60 * 10,
|
||||||
label=_("MFA verify TTL"),
|
label=_("MFA verify TTL"),
|
||||||
help_text=_("Unit: second, The verification MFA takes effect only when you view the account password"),
|
help_text=_(
|
||||||
|
"Unit: second, The verification MFA takes effect only when you view the account password"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
SECURITY_LOGIN_CHALLENGE_ENABLED = serializers.BooleanField(
|
SECURITY_LOGIN_CHALLENGE_ENABLED = serializers.BooleanField(
|
||||||
required=False, default=False,
|
required=False, default=False,
|
||||||
|
@ -108,7 +135,9 @@ class SecurityAuthSerializer(serializers.Serializer):
|
||||||
class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSerializer):
|
class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSerializer):
|
||||||
SECURITY_SERVICE_ACCOUNT_REGISTRATION = serializers.BooleanField(
|
SECURITY_SERVICE_ACCOUNT_REGISTRATION = serializers.BooleanField(
|
||||||
required=True, label=_('Enable terminal register'),
|
required=True, label=_('Enable terminal register'),
|
||||||
help_text=_("Allow terminal register, after all terminal setup, you should disable this for security")
|
help_text=_(
|
||||||
|
"Allow terminal register, after all terminal setup, you should disable this for security"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
SECURITY_WATERMARK_ENABLED = serializers.BooleanField(
|
SECURITY_WATERMARK_ENABLED = serializers.BooleanField(
|
||||||
required=True, label=_('Enable watermark'),
|
required=True, label=_('Enable watermark'),
|
||||||
|
@ -142,6 +171,8 @@ class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSeri
|
||||||
)
|
)
|
||||||
SECURITY_CHECK_DIFFERENT_CITY_LOGIN = serializers.BooleanField(
|
SECURITY_CHECK_DIFFERENT_CITY_LOGIN = serializers.BooleanField(
|
||||||
required=False, label=_('Remote Login Protection'),
|
required=False, label=_('Remote Login Protection'),
|
||||||
help_text=_('The system determines whether the login IP address belongs to a common login city. '
|
help_text=_(
|
||||||
'If the account is logged in from a common login city, the system sends a remote login reminder')
|
'The system determines whether the login IP address belongs to a common login city. '
|
||||||
|
'If the account is logged in from a common login city, the system sends a remote login reminder'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -25,10 +25,6 @@ class TerminalSettingSerializer(serializers.Serializer):
|
||||||
TERMINAL_ASSET_LIST_SORT_BY = serializers.ChoiceField(SORT_BY_CHOICES, required=False, label=_('List sort by'))
|
TERMINAL_ASSET_LIST_SORT_BY = serializers.ChoiceField(SORT_BY_CHOICES, required=False, label=_('List sort by'))
|
||||||
TERMINAL_ASSET_LIST_PAGE_SIZE = serializers.ChoiceField(PAGE_SIZE_CHOICES, required=False,
|
TERMINAL_ASSET_LIST_PAGE_SIZE = serializers.ChoiceField(PAGE_SIZE_CHOICES, required=False,
|
||||||
label=_('List page size'))
|
label=_('List page size'))
|
||||||
TERMINAL_SESSION_KEEP_DURATION = serializers.IntegerField(
|
|
||||||
min_value=1, max_value=99999, required=True, label=_('Session keep duration'),
|
|
||||||
help_text=_('Unit: days, Session, record, command will be delete if more than duration, only in database')
|
|
||||||
)
|
|
||||||
TERMINAL_TELNET_REGEX = serializers.CharField(
|
TERMINAL_TELNET_REGEX = serializers.CharField(
|
||||||
allow_blank=True, max_length=1024, required=False, label=_('Telnet login regex'),
|
allow_blank=True, max_length=1024, required=False, label=_('Telnet login regex'),
|
||||||
help_text=_("The login success message varies with devices. "
|
help_text=_("The login success message varies with devices. "
|
||||||
|
|
|
@ -54,9 +54,13 @@
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
const mfaSelectRef = document.getElementById('mfa-select');
|
const mfaSelectRef = document.getElementById('mfa-select');
|
||||||
const preferMFA = localStorage.getItem(preferMFAKey);
|
const preferMFA = localStorage.getItem(preferMFAKey);
|
||||||
if (preferMFA) {
|
const valueSelector = "value=" + preferMFA
|
||||||
|
const preferMFADisabled = $(`#mfa-select option[${valueSelector}]`).attr('disabled')
|
||||||
|
|
||||||
|
if (preferMFA && !preferMFADisabled) {
|
||||||
mfaSelectRef.value = preferMFA;
|
mfaSelectRef.value = preferMFA;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mfaSelect = mfaSelectRef.value;
|
const mfaSelect = mfaSelectRef.value;
|
||||||
if (mfaSelect !== null) {
|
if (mfaSelect !== null) {
|
||||||
selectChange(mfaSelect, true);
|
selectChange(mfaSelect, true);
|
||||||
|
@ -71,9 +75,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$('.input-style').each(function (i, ele){
|
$('.input-style').each(function (i, ele){
|
||||||
$(ele).attr('name', '').attr('required', false)
|
$(ele).attr('name', '')
|
||||||
})
|
})
|
||||||
$('#mfa-' + name + ' .input-style').attr('name', 'code').attr('required', true)
|
|
||||||
|
const currentMFAInputRef = $('#mfa-' + name + ' .input-style')
|
||||||
|
currentMFAInputRef.attr('name', 'code').attr('required', true)
|
||||||
|
|
||||||
|
// 登录页时,不应该默认focus
|
||||||
|
const usernameRef = $('input[name="username"]')
|
||||||
|
if (!usernameRef || usernameRef.length === 0) {
|
||||||
|
currentMFAInputRef.focus()
|
||||||
|
}
|
||||||
|
currentMFAInputRef.attr('name', 'code')
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendChallengeCode(currentBtn) {
|
function sendChallengeCode(currentBtn) {
|
||||||
|
|
|
@ -55,8 +55,18 @@ class Status(models.Model):
|
||||||
stat = cls(**data)
|
stat = cls(**data)
|
||||||
stat.terminal = terminal
|
stat.terminal = terminal
|
||||||
stat.is_alive = terminal.is_alive
|
stat.is_alive = terminal.is_alive
|
||||||
|
stat.keep_one_decimal_place()
|
||||||
return stat
|
return stat
|
||||||
|
|
||||||
|
def keep_one_decimal_place(self):
|
||||||
|
keys = ['cpu_load', 'memory_used', 'disk_used']
|
||||||
|
for key in keys:
|
||||||
|
value = getattr(self, key, 0)
|
||||||
|
if not isinstance(value, (int, float)):
|
||||||
|
continue
|
||||||
|
value = '%.1f' % value
|
||||||
|
setattr(self, key, float(value))
|
||||||
|
|
||||||
def save(self, force_insert=False, force_update=False, using=None,
|
def save(self, force_insert=False, force_update=False, using=None,
|
||||||
update_fields=None):
|
update_fields=None):
|
||||||
self.terminal.set_alive(ttl=120)
|
self.terminal.set_alive(ttl=120)
|
||||||
|
|
|
@ -164,4 +164,4 @@ class ReplayStorage(CommonStorageModelMixin, CommonModelMixin):
|
||||||
return storage.is_valid(src, target)
|
return storage.is_valid(src, target)
|
||||||
|
|
||||||
def is_use(self):
|
def is_use(self):
|
||||||
return Terminal.objects.filter(replay_storage=self.name).exists()
|
return Terminal.objects.filter(replay_storage=self.name, is_deleted=False).exists()
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import copy
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
@ -9,6 +8,7 @@ from common.drf.serializers import MethodSerializer
|
||||||
from common.drf.fields import ReadableHiddenField
|
from common.drf.fields import ReadableHiddenField
|
||||||
from ..models import ReplayStorage, CommandStorage
|
from ..models import ReplayStorage, CommandStorage
|
||||||
from .. import const
|
from .. import const
|
||||||
|
from rest_framework.validators import UniqueValidator
|
||||||
|
|
||||||
|
|
||||||
# Replay storage serializers
|
# Replay storage serializers
|
||||||
|
@ -220,6 +220,9 @@ class CommandStorageSerializer(BaseStorageSerializer):
|
||||||
|
|
||||||
class Meta(BaseStorageSerializer.Meta):
|
class Meta(BaseStorageSerializer.Meta):
|
||||||
model = CommandStorage
|
model = CommandStorage
|
||||||
|
extra_kwargs = {
|
||||||
|
'name': {'validators': [UniqueValidator(queryset=CommandStorage.objects.all())]}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ReplayStorageSerializer
|
# ReplayStorageSerializer
|
||||||
|
@ -230,4 +233,6 @@ class ReplayStorageSerializer(BaseStorageSerializer):
|
||||||
|
|
||||||
class Meta(BaseStorageSerializer.Meta):
|
class Meta(BaseStorageSerializer.Meta):
|
||||||
model = ReplayStorage
|
model = ReplayStorage
|
||||||
|
extra_kwargs = {
|
||||||
|
'name': {'validators': [UniqueValidator(queryset=ReplayStorage.objects.all())]}
|
||||||
|
}
|
||||||
|
|
|
@ -77,14 +77,19 @@ class ApplySerializer(serializers.Serializer):
|
||||||
type = self.root.initial_data['meta'].get('apply_type')
|
type = self.root.initial_data['meta'].get('apply_type')
|
||||||
org_id = self.root.initial_data.get('org_id')
|
org_id = self.root.initial_data.get('org_id')
|
||||||
with tmp_to_org(org_id):
|
with tmp_to_org(org_id):
|
||||||
applications = Application.objects.filter(id__in=apply_applications, type=type).values_list('id', flat=True)
|
applications = Application.objects.filter(
|
||||||
|
id__in=apply_applications, type=type
|
||||||
|
).values_list('id', flat=True)
|
||||||
return list(applications)
|
return list(applications)
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
apply_date_start = attrs['apply_date_start']
|
apply_date_start = attrs['apply_date_start'].strftime('%Y-%m-%d %H:%M:%S')
|
||||||
apply_date_expired = attrs['apply_date_expired']
|
apply_date_expired = attrs['apply_date_expired'].strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
if apply_date_expired <= apply_date_start:
|
if apply_date_expired <= apply_date_start:
|
||||||
error = _('The expiration date should be greater than the start date')
|
error = _('The expiration date should be greater than the start date')
|
||||||
raise serializers.ValidationError({'apply_date_expired': error})
|
raise serializers.ValidationError({'apply_date_expired': error})
|
||||||
|
|
||||||
|
attrs['apply_date_start'] = apply_date_start
|
||||||
|
attrs['apply_date_expired'] = apply_date_expired
|
||||||
return attrs
|
return attrs
|
||||||
|
|
|
@ -64,10 +64,13 @@ class ApplySerializer(serializers.Serializer):
|
||||||
))
|
))
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
apply_date_start = attrs['apply_date_start']
|
apply_date_start = attrs['apply_date_start'].strftime('%Y-%m-%d %H:%M:%S')
|
||||||
apply_date_expired = attrs['apply_date_expired']
|
apply_date_expired = attrs['apply_date_expired'].strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
if apply_date_expired <= apply_date_start:
|
if apply_date_expired <= apply_date_start:
|
||||||
error = _('The expiration date should be greater than the start date')
|
error = _('The expiration date should be greater than the start date')
|
||||||
raise serializers.ValidationError({'apply_date_expired': error})
|
raise serializers.ValidationError({'apply_date_expired': error})
|
||||||
|
|
||||||
|
attrs['apply_date_start'] = apply_date_start
|
||||||
|
attrs['apply_date_expired'] = apply_date_expired
|
||||||
return attrs
|
return attrs
|
||||||
|
|
|
@ -5,7 +5,10 @@ from rest_framework import generics
|
||||||
from common.permissions import IsOrgAdmin
|
from common.permissions import IsOrgAdmin
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
from users.notifications import ResetPasswordMsg, ResetPasswordSuccessMsg, ResetSSHKeyMsg
|
from users.notifications import (
|
||||||
|
ResetPasswordMsg, ResetPasswordSuccessMsg, ResetSSHKeyMsg,
|
||||||
|
ResetPublicKeySuccessMsg,
|
||||||
|
)
|
||||||
from common.permissions import (
|
from common.permissions import (
|
||||||
IsCurrentUserOrReadOnly
|
IsCurrentUserOrReadOnly
|
||||||
)
|
)
|
||||||
|
@ -87,4 +90,4 @@ class UserPublicKeyApi(generics.RetrieveUpdateAPIView):
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
super().perform_update(serializer)
|
super().perform_update(serializer)
|
||||||
ResetPasswordSuccessMsg(self.get_object(), self.request).publish_async()
|
ResetPublicKeySuccessMsg(self.get_object(), self.request).publish_async()
|
||||||
|
|
|
@ -28,7 +28,7 @@ from ..filters import OrgRoleUserFilterBackend, UserFilter
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'UserViewSet', 'UserChangePasswordApi',
|
'UserViewSet', 'UserChangePasswordApi',
|
||||||
'UserUnblockPKApi', 'UserResetOTPApi',
|
'UserUnblockPKApi', 'UserResetMFAApi',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -199,7 +199,7 @@ class UserUnblockPKApi(UserQuerysetMixin, generics.UpdateAPIView):
|
||||||
MFABlockUtils.unblock_user(username)
|
MFABlockUtils.unblock_user(username)
|
||||||
|
|
||||||
|
|
||||||
class UserResetOTPApi(UserQuerysetMixin, generics.RetrieveAPIView):
|
class UserResetMFAApi(UserQuerysetMixin, generics.RetrieveAPIView):
|
||||||
permission_classes = (IsOrgAdmin,)
|
permission_classes = (IsOrgAdmin,)
|
||||||
serializer_class = serializers.ResetOTPSerializer
|
serializer_class = serializers.ResetOTPSerializer
|
||||||
|
|
||||||
|
@ -209,9 +209,10 @@ class UserResetOTPApi(UserQuerysetMixin, generics.RetrieveAPIView):
|
||||||
msg = _("Could not reset self otp, use profile reset instead")
|
msg = _("Could not reset self otp, use profile reset instead")
|
||||||
return Response({"error": msg}, status=401)
|
return Response({"error": msg}, status=401)
|
||||||
|
|
||||||
if user.mfa_enabled:
|
backends = user.active_mfa_backends_mapper
|
||||||
user.reset_mfa()
|
for backend in backends:
|
||||||
user.save()
|
if backend.can_disable():
|
||||||
|
backend.disable()
|
||||||
|
|
||||||
ResetMFAMsg(user).publish_async()
|
ResetMFAMsg(user).publish_async()
|
||||||
return Response({"msg": "success"})
|
return Response({"msg": "success"})
|
||||||
|
|
|
@ -507,8 +507,14 @@ class MFAMixin:
|
||||||
backends = [cls(user) for cls in MFA_BACKENDS if cls.global_enabled()]
|
backends = [cls(user) for cls in MFA_BACKENDS if cls.global_enabled()]
|
||||||
return backends
|
return backends
|
||||||
|
|
||||||
|
def get_active_mfa_backend_by_type(self, mfa_type):
|
||||||
|
backend = self.get_mfa_backend_by_type(mfa_type)
|
||||||
|
if not backend or not backend.is_active():
|
||||||
|
return None
|
||||||
|
return backend
|
||||||
|
|
||||||
def get_mfa_backend_by_type(self, mfa_type):
|
def get_mfa_backend_by_type(self, mfa_type):
|
||||||
mfa_mapper = self.active_mfa_backends_mapper
|
mfa_mapper = {b.name: b for b in self.get_user_mfa_backends(self)}
|
||||||
backend = mfa_mapper.get(mfa_type)
|
backend = mfa_mapper.get(mfa_type)
|
||||||
if not backend:
|
if not backend:
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -105,6 +105,38 @@ class ResetPasswordSuccessMsg(UserMessage):
|
||||||
return cls(user, request)
|
return cls(user, request)
|
||||||
|
|
||||||
|
|
||||||
|
class ResetPublicKeySuccessMsg(UserMessage):
|
||||||
|
def __init__(self, user, request):
|
||||||
|
super().__init__(user)
|
||||||
|
self.ip_address = get_request_ip_or_data(request)
|
||||||
|
self.browser = get_request_user_agent(request)
|
||||||
|
|
||||||
|
def get_html_msg(self) -> dict:
|
||||||
|
user = self.user
|
||||||
|
|
||||||
|
subject = _('Reset public key success')
|
||||||
|
context = {
|
||||||
|
'name': user.name,
|
||||||
|
'ip_address': self.ip_address,
|
||||||
|
'browser': self.browser,
|
||||||
|
}
|
||||||
|
message = render_to_string('authentication/_msg_rest_public_key_success.html', context)
|
||||||
|
return {
|
||||||
|
'subject': subject,
|
||||||
|
'message': message
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def gen_test_msg(cls):
|
||||||
|
from users.models import User
|
||||||
|
from rest_framework.test import APIRequestFactory
|
||||||
|
from rest_framework.request import Request
|
||||||
|
factory = APIRequestFactory()
|
||||||
|
request = Request(factory.get('/notes/'))
|
||||||
|
user = User.objects.first()
|
||||||
|
return cls(user, request)
|
||||||
|
|
||||||
|
|
||||||
class PasswordExpirationReminderMsg(UserMessage):
|
class PasswordExpirationReminderMsg(UserMessage):
|
||||||
def get_html_msg(self) -> dict:
|
def get_html_msg(self) -> dict:
|
||||||
user = self.user
|
user = self.user
|
||||||
|
|
|
@ -23,8 +23,8 @@ urlpatterns = [
|
||||||
path('profile/', api.UserProfileApi.as_view(), name='user-profile'),
|
path('profile/', api.UserProfileApi.as_view(), name='user-profile'),
|
||||||
path('profile/password/', api.UserPasswordApi.as_view(), name='user-password'),
|
path('profile/password/', api.UserPasswordApi.as_view(), name='user-password'),
|
||||||
path('profile/public-key/', api.UserPublicKeyApi.as_view(), name='user-public-key'),
|
path('profile/public-key/', api.UserPublicKeyApi.as_view(), name='user-public-key'),
|
||||||
path('otp/reset/', api.UserResetOTPApi.as_view(), name='my-otp-reset'),
|
path('profile/mfa/reset/', api.UserResetMFAApi.as_view(), name='my-mfa-reset'),
|
||||||
path('users/<uuid:pk>/otp/reset/', api.UserResetOTPApi.as_view(), name='user-reset-otp'),
|
path('users/<uuid:pk>/mfa/reset/', api.UserResetMFAApi.as_view(), name='user-reset-mfa'),
|
||||||
path('users/<uuid:pk>/password/', api.UserChangePasswordApi.as_view(), name='change-user-password'),
|
path('users/<uuid:pk>/password/', api.UserChangePasswordApi.as_view(), name='change-user-password'),
|
||||||
path('users/<uuid:pk>/password/reset/', api.UserResetPasswordApi.as_view(), name='user-reset-password'),
|
path('users/<uuid:pk>/password/reset/', api.UserResetPasswordApi.as_view(), name='user-reset-password'),
|
||||||
path('users/<uuid:pk>/pubkey/reset/', api.UserResetPKApi.as_view(), name='user-public-key-reset'),
|
path('users/<uuid:pk>/pubkey/reset/', api.UserResetPKApi.as_view(), name='user-public-key-reset'),
|
||||||
|
|
|
@ -14,7 +14,6 @@ from common.tasks import send_mail_async
|
||||||
from common.utils import reverse, get_object_or_none
|
from common.utils import reverse, get_object_or_none
|
||||||
from .models import User
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('jumpserver')
|
logger = logging.getLogger('jumpserver')
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,7 +100,7 @@ def check_password_rules(password, is_org_admin=False):
|
||||||
min_length = settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH
|
min_length = settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH
|
||||||
else:
|
else:
|
||||||
min_length = settings.SECURITY_PASSWORD_MIN_LENGTH
|
min_length = settings.SECURITY_PASSWORD_MIN_LENGTH
|
||||||
pattern += '.{' + str(min_length-1) + ',}$'
|
pattern += '.{' + str(min_length - 1) + ',}$'
|
||||||
match_obj = re.match(pattern, password)
|
match_obj = re.match(pattern, password)
|
||||||
return bool(match_obj)
|
return bool(match_obj)
|
||||||
|
|
||||||
|
@ -173,6 +172,33 @@ class BlockUtilBase:
|
||||||
return bool(cache.get(self.block_key))
|
return bool(cache.get(self.block_key))
|
||||||
|
|
||||||
|
|
||||||
|
class BlockGlobalIpUtilBase:
|
||||||
|
LIMIT_KEY_TMPL: str
|
||||||
|
BLOCK_KEY_TMPL: str
|
||||||
|
|
||||||
|
def __init__(self, ip):
|
||||||
|
self.ip = ip
|
||||||
|
self.limit_key = self.LIMIT_KEY_TMPL.format(ip)
|
||||||
|
self.block_key = self.BLOCK_KEY_TMPL.format(ip)
|
||||||
|
self.key_ttl = int(settings.SECURITY_LOGIN_LIMIT_TIME) * 60
|
||||||
|
|
||||||
|
def sign_limit_key_and_block_key(self):
|
||||||
|
count = cache.get(self.limit_key, 0)
|
||||||
|
count += 1
|
||||||
|
cache.set(self.limit_key, count, self.key_ttl)
|
||||||
|
|
||||||
|
limit_count = settings.SECURITY_LOGIN_LIMIT_COUNT
|
||||||
|
if count >= limit_count:
|
||||||
|
cache.set(self.block_key, True, self.key_ttl)
|
||||||
|
|
||||||
|
def is_block(self):
|
||||||
|
if self.ip in settings.SECURITY_LOGIN_IP_BLACK_LIST:
|
||||||
|
self.sign_limit_key_and_block_key()
|
||||||
|
return bool(cache.get(self.block_key))
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class LoginBlockUtil(BlockUtilBase):
|
class LoginBlockUtil(BlockUtilBase):
|
||||||
LIMIT_KEY_TMPL = "_LOGIN_LIMIT_{}_{}"
|
LIMIT_KEY_TMPL = "_LOGIN_LIMIT_{}_{}"
|
||||||
BLOCK_KEY_TMPL = "_LOGIN_BLOCK_{}"
|
BLOCK_KEY_TMPL = "_LOGIN_BLOCK_{}"
|
||||||
|
@ -183,6 +209,11 @@ class MFABlockUtils(BlockUtilBase):
|
||||||
BLOCK_KEY_TMPL = "_MFA_BLOCK_{}"
|
BLOCK_KEY_TMPL = "_MFA_BLOCK_{}"
|
||||||
|
|
||||||
|
|
||||||
|
class LoginIpBlockUtil(BlockGlobalIpUtilBase):
|
||||||
|
LIMIT_KEY_TMPL = "_LOGIN_LIMIT_{}"
|
||||||
|
BLOCK_KEY_TMPL = "_LOGIN_BLOCK_{}"
|
||||||
|
|
||||||
|
|
||||||
def construct_user_email(username, email):
|
def construct_user_email(username, email):
|
||||||
if '@' not in email:
|
if '@' not in email:
|
||||||
if '@' in username:
|
if '@' in username:
|
||||||
|
|
Loading…
Reference in New Issue