Merge pull request #7214 from jumpserver/dev

v2.16.0 rc2
pull/7230/head
Jiangjie.Bai 2021-11-17 19:42:25 +08:00 committed by GitHub
commit 5a82174c54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 610 additions and 368 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -109,7 +109,7 @@
} }
.select-con { .select-con {
width: 22%; width: 30%;
} }
.mfa-div { .mfa-div {

View File

@ -3,7 +3,7 @@
{% load i18n %} {% load i18n %}
{% block title %} {% block title %}
{% trans 'MFA' %} {% trans 'MFA Auth' %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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