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
)
win2016 = model.objects.filter(name='Windows2016').first()
if win2016:
win2016.internal = False
win2016.save(update_fields=['internal'])
class Migration(migrations.Migration):

View File

@ -103,6 +103,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
'port': {'write_only': True},
'hardware_info': {'label': _('Hardware info'), 'read_only': True},
'admin_user_display': {'label': _('Admin user display'), 'read_only': True},
'cpu_info': {'label': _('CPU info')},
}
def get_fields(self):

View File

@ -42,7 +42,7 @@ class UserLoginLogSerializer(serializers.ModelSerializer):
fields = fields_small
extra_kwargs = {
"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 perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user
from perms.models.asset_permission import Action
from authentication.errors import NotHaveUpDownLoadPerm
from ..serializers import (
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
@ -96,22 +95,26 @@ class ClientProtocolMixin:
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
token = self.create_token(user, asset, application, system_user)
# 设置磁盘挂载
if drives_redirect and 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:
options['drivestoredirect:s'] = '*'
else:
raise NotHaveUpDownLoadPerm
# 全屏
options['screen mode id:i'] = '2' if full_screen else '1'
# RDP Server 地址
address = settings.TERMINAL_RDP_ADDR
if not address or address == 'localhost:3389':
address = self.request.get_host().split(':')[0] + ':3389'
options['full address:s'] = address
# 用户名
options['username:s'] = '{}|{}'.format(user.username, token)
if system_user.ad_domain:
options['domain:s'] = system_user.ad_domain
# 宽高
if width and height:
options['desktopwidth:i'] = width
options['desktopheight:i'] = height
@ -160,13 +163,16 @@ class ClientProtocolMixin:
asset, application, system_user, user = self.get_request_resource(serializer)
protocol = system_user.protocol
username = user.username
name = ''
if protocol == 'rdp':
name, config = self.get_rdp_file_content(serializer)
elif protocol == 'vnc':
raise HttpResponse(status=404, data={"error": "VNC not support"})
else:
elif protocol == 'ssh':
# Todo:
name = ''
config = 'ssh://system_user@asset@user@jumpserver-ssh'
else:
raise ValueError('Protocol not support: {}'.format(protocol))
filename = "{}-{}-jumpserver".format(username, name)
data = {
"filename": filename,
@ -179,8 +185,13 @@ class ClientProtocolMixin:
@action(methods=['POST', 'GET'], detail=False, url_path='client-url', permission_classes=[IsValidUser])
def get_client_protocol_url(self, request, *args, **kwargs):
serializer = self.get_valid_serializer()
protocol_data = self.get_client_protocol_data(serializer)
protocol_data = base64.b64encode(json.dumps(protocol_data).encode()).decode()
try:
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 = {
'url': 'jms://{}'.format(protocol_data),
}
@ -348,14 +359,12 @@ class UserConnectionTokenViewSet(
raise serializers.ValidationError("User not valid, disabled or expired")
system_user = get_object_or_404(SystemUser, id=value.get('system_user'))
asset = None
app = None
if value.get('type') == 'asset':
asset = get_object_or_404(Asset, id=value.get('asset'))
if not asset.is_active:
raise serializers.ValidationError("Asset disabled")
has_perm, expired_at = asset_validate_permission(user, asset, system_user, 'connect')
else:
app = get_object_or_404(Application, id=value.get('application'))

View File

@ -44,7 +44,7 @@ class MFASendCodeApi(AuthMixin, CreateAPIView):
else:
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:
raise ValidationError('MFA type not support: {} {}'.format(mfa_type, mfa_backend))
mfa_backend.send_challenge()
@ -100,7 +100,7 @@ class UserOtpVerifyApi(CreateAPIView):
request.session["MFA_VERIFY_TIME"] = int(time.time())
return Response({"ok": "1"})
else:
return Response({"error": _("Code is invalid") + ", " + error}, status=400)
return Response({"error": _("Code is invalid, {}").format(error)}, status=400)
def get_permissions(self):
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_login_confirm_if_need(user)
self.send_auth_signal(success=True, user=user)
self.clear_auth_mark()
resp = super().create(request, *args, **kwargs)
self.clear_auth_mark()
return resp
except errors.AuthFailedError as e:
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)"
)
mfa_error_msg = _(
"{error},"
"{error}, "
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
)
mfa_required_msg = _("MFA required")
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_wait_msg = _("Wait login confirm ticket for accept")
login_confirm_error_msg = _("Login confirm ticket was {}")
@ -162,13 +161,11 @@ class BlockMFAError(AuthFailedNeedLogMixin, AuthFailedError):
super().__init__(username=username, request=request, ip=ip)
class MFAUnsetError(AuthFailedNeedLogMixin, AuthFailedError):
class MFAUnsetError(Exception):
error = reason_mfa_unset
msg = mfa_unset_msg
def __init__(self, user, request, url):
super().__init__(username=user.username, request=request)
self.user = user
self.url = url
@ -180,6 +177,14 @@ class BlockLoginError(AuthFailedNeedBlockMixin, AuthFailedError):
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):
msg = session_empty_msg
error = 'session_empty'
@ -340,27 +345,16 @@ class PasswordInvalid(JMSException):
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):
error = 'mfa_code_required'
msg = _("Please enter MFA code")
class SMSCodeRequiredError(AuthFailedError):
error = 'sms_code_required'
msg = _("Please enter SMS code")
class UserPhoneNotSet(AuthFailedError):
error = '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):
name = 'otp'
display_name = _('OTP')
placeholder = _('OTP verification code')
def check_code(self, code):
from users.utils import check_otp_code

View File

@ -9,7 +9,8 @@ mfa_failed_msg = _("Radius verify code invalid")
class MFARadius(BaseMFA):
name = 'otp_radius'
display_name = _('Radius MFA')
display_name = 'Radius'
placeholder = _("Radius verification code")
def check_code(self, code):
assert self.is_authenticated()

View File

@ -19,8 +19,12 @@ class MFASms(BaseMFA):
def check_code(self, code):
assert self.is_authenticated()
ok = self.sms.verify(code)
msg = '' if ok else sms_failed_msg
ok = False
msg = ''
try:
ok = self.sms.verify(code)
except Exception as e:
msg = str(e)
return ok, msg
def is_active(self):

View File

@ -8,7 +8,6 @@ from typing import Callable
from django.utils.http import urlencode
from django.core.cache import cache
from django.conf import settings
from django.urls import reverse_lazy
from django.contrib import auth
from django.utils.translation import ugettext as _
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 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 users.models import User
from users.utils import LoginBlockUtil, MFABlockUtils
from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil
from . import errors
from .utils import rsa_decrypt, gen_key_pair
from .signals import post_auth_success, post_auth_failed
@ -76,7 +75,9 @@ def authenticate(request=None, **credentials):
return user
# 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
@ -209,6 +210,10 @@ class AuthPreCheckMixin:
def _check_is_block(self, username, raise_exception=True):
ip = self.get_request_ip()
if LoginIpBlockUtil(ip).is_block():
raise errors.BlockGlobalIpLoginError(username=username, ip=ip)
is_block = LoginBlockUtil(username, ip).is_block()
if not is_block:
return
@ -224,6 +229,7 @@ class AuthPreCheckMixin:
username = self.request.data.get("username")
else:
username = self.request.POST.get("username")
self._check_is_block(username, raise_exception)
def _check_only_allow_exists_user_auth(self, username):
@ -242,16 +248,24 @@ class MFAMixin:
get_user_from_session: 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):
if not settings.SECURITY_MFA_IN_LOGIN_PAGE:
return
self._check_if_no_active_mfa(user)
request = self.request
data = request.data if hasattr(request, 'data') else request.POST
code = data.get('code')
mfa_type = data.get('mfa_type', 'otp')
if not code:
raise errors.MFACodeRequiredError
return
self._do_check_user_mfa(code, mfa_type, user=user)
def check_user_mfa_if_need(self, user):
@ -260,10 +274,9 @@ class MFAMixin:
if not user.mfa_enabled:
return
self._check_if_no_active_mfa(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)
raise errors.MFARequiredError(mfa_types=tuple(active_mfa_mapper.keys()))
def mark_mfa_ok(self, mfa_type):
@ -299,10 +312,13 @@ class MFAMixin:
ok = False
mfa_backend = user.get_mfa_backend_by_type(mfa_type)
if mfa_backend:
ok, msg = mfa_backend.check_code(code)
backend_error = _('The MFA type ({}) is not enabled')
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:
msg = _('The MFA type({}) is not supported'.format(mfa_type))
ok, msg = mfa_backend.check_code(code)
if ok:
self.mark_mfa_ok(mfa_type)

View File

@ -54,9 +54,9 @@ class BearerTokenSerializer(serializers.Serializer):
user.last_login = timezone.now()
user.save(update_fields=['last_login'])
def create(self, validated_data):
def get_request_user(self):
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
else:
user_id = request.session.get('user_id')
@ -65,6 +65,12 @@ class BearerTokenSerializer(serializers.Serializer):
raise serializers.ValidationError(
"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)
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 {
width: 22%;
width: 30%;
}
.mfa-div {

View File

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

View File

@ -122,10 +122,10 @@ class UserLoginView(mixins.AuthMixin, FormView):
self.request.session.set_test_cookie()
return self.render_to_response(context)
except (
errors.MFAUnsetError,
errors.PasswordTooSimple,
errors.PasswordRequireResetError,
errors.PasswordNeedUpdate,
errors.OTPBindRequiredError
errors.PasswordNeedUpdate
) as e:
return redirect(e.url)
except (
@ -133,7 +133,8 @@ class UserLoginView(mixins.AuthMixin, FormView):
errors.BlockMFAError,
errors.MFACodeRequiredError,
errors.SMSCodeRequiredError,
errors.UserPhoneNotSet
errors.UserPhoneNotSet,
errors.BlockGlobalIpLoginError
) as e:
form.add_error('code', e.msg)
return super().form_invalid(form)

View File

@ -292,6 +292,7 @@ class Config(dict):
'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True,
'SECURITY_VIEW_AUTH_NEED_MFA': True,
'SECURITY_LOGIN_LIMIT_COUNT': 7,
'SECURITY_LOGIN_IP_BLACK_LIST': [],
'SECURITY_LOGIN_LIMIT_TIME': 30,
'SECURITY_MAX_IDLE_TIME': 30,
'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_COMMAND_EXECUTION = CONFIG.SECURITY_COMMAND_EXECUTION
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_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_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
@ -118,7 +119,6 @@ REFERER_CHECK_ENABLED = CONFIG.REFERER_CHECK_ENABLED
CONNECTION_TOKEN_ENABLED = CONFIG.CONNECTION_TOKEN_ENABLED
FORGOT_PASSWORD_URL = CONFIG.FORGOT_PASSWORD_URL
# 自定义默认组织名
GLOBAL_ORG_DISPLAY_NAME = CONFIG.GLOBAL_ORG_DISPLAY_NAME
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
# SMS enabled
SMS_ENABLED = CONFIG.SMS_ENABLED
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 = 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
oid sha256:925c5a219a4ee6835ad59e3b8e9f7ea5074ee3df6527c0f73ef1a50eaedaf59c
size 91777
oid sha256:4fea2cdf5a5477757cb95ff36016ed754fd65f839c12adbac9247ebdcca138ef
size 93440

File diff suppressed because it is too large Load Diff

View File

@ -28,7 +28,7 @@ class AssetSystemUserSerializer(serializers.ModelSerializer):
model = SystemUser
only_fields = (
'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"]
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):
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:
return True
return False

View File

@ -59,6 +59,8 @@ class PublicSettingApi(generics.RetrieveAPIView):
"XRDP_ENABLED": settings.XRDP_ENABLED,
"ANNOUNCEMENT_ENABLED": settings.ANNOUNCEMENT_ENABLED,
"ANNOUNCEMENT": settings.ANNOUNCEMENT,
"HELP_DOCUMENT_URL": settings.HELP_DOCUMENT_URL,
"HELP_SUPPORT_URL": settings.HELP_SUPPORT_URL,
}
}
return instance

View File

@ -25,3 +25,8 @@ class CleaningSerializer(serializers.Serializer):
min_value=1, max_value=9999,
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 rest_framework import serializers
from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
class SecurityPasswordRuleSerializer(serializers.Serializer):
SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField(
@ -14,9 +16,24 @@ class SecurityPasswordRuleSerializer(serializers.Serializer):
SECURITY_PASSWORD_UPPER_CASE = serializers.BooleanField(
required=False, label=_('Must contain capital')
)
SECURITY_PASSWORD_LOWER_CASE = serializers.BooleanField(required=False, label=_('Must contain lowercase'))
SECURITY_PASSWORD_NUMBER = serializers.BooleanField(required=False, label=_('Must contain numeric'))
SECURITY_PASSWORD_SPECIAL_CHAR = serializers.BooleanField(required=False, label=_('Must contain special'))
SECURITY_PASSWORD_LOWER_CASE = serializers.BooleanField(
required=False, label=_('Must contain lowercase')
)
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):
@ -40,6 +57,14 @@ class SecurityAuthSerializer(serializers.Serializer):
'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(
min_value=1, max_value=99999, required=True,
label=_('User password expiration'),
@ -72,7 +97,9 @@ class SecurityAuthSerializer(serializers.Serializer):
SECURITY_MFA_VERIFY_TTL = serializers.IntegerField(
min_value=5, max_value=60 * 60 * 10,
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(
required=False, default=False,
@ -108,7 +135,9 @@ class SecurityAuthSerializer(serializers.Serializer):
class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSerializer):
SECURITY_SERVICE_ACCOUNT_REGISTRATION = serializers.BooleanField(
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(
required=True, label=_('Enable watermark'),
@ -142,6 +171,8 @@ class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSeri
)
SECURITY_CHECK_DIFFERENT_CITY_LOGIN = serializers.BooleanField(
required=False, label=_('Remote Login Protection'),
help_text=_('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')
help_text=_(
'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_PAGE_SIZE = serializers.ChoiceField(PAGE_SIZE_CHOICES, required=False,
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(
allow_blank=True, max_length=1024, required=False, label=_('Telnet login regex'),
help_text=_("The login success message varies with devices. "

View File

@ -54,9 +54,13 @@
$(document).ready(function () {
const mfaSelectRef = document.getElementById('mfa-select');
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;
}
const mfaSelect = mfaSelectRef.value;
if (mfaSelect !== null) {
selectChange(mfaSelect, true);
@ -71,9 +75,18 @@
}
$('.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) {

View File

@ -55,8 +55,18 @@ class Status(models.Model):
stat = cls(**data)
stat.terminal = terminal
stat.is_alive = terminal.is_alive
stat.keep_one_decimal_place()
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,
update_fields=None):
self.terminal.set_alive(ttl=120)

View File

@ -164,4 +164,4 @@ class ReplayStorage(CommonStorageModelMixin, CommonModelMixin):
return storage.is_valid(src, target)
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 -*-
#
import copy
from rest_framework import serializers
from urllib.parse import urlparse
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 ..models import ReplayStorage, CommandStorage
from .. import const
from rest_framework.validators import UniqueValidator
# Replay storage serializers
@ -220,6 +220,9 @@ class CommandStorageSerializer(BaseStorageSerializer):
class Meta(BaseStorageSerializer.Meta):
model = CommandStorage
extra_kwargs = {
'name': {'validators': [UniqueValidator(queryset=CommandStorage.objects.all())]}
}
# ReplayStorageSerializer
@ -230,4 +233,6 @@ class ReplayStorageSerializer(BaseStorageSerializer):
class Meta(BaseStorageSerializer.Meta):
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')
org_id = self.root.initial_data.get('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)
def validate(self, attrs):
apply_date_start = attrs['apply_date_start']
apply_date_expired = attrs['apply_date_expired']
apply_date_start = attrs['apply_date_start'].strftime('%Y-%m-%d %H:%M:%S')
apply_date_expired = attrs['apply_date_expired'].strftime('%Y-%m-%d %H:%M:%S')
if apply_date_expired <= apply_date_start:
error = _('The expiration date should be greater than the start date')
raise serializers.ValidationError({'apply_date_expired': error})
attrs['apply_date_start'] = apply_date_start
attrs['apply_date_expired'] = apply_date_expired
return attrs

View File

@ -64,10 +64,13 @@ class ApplySerializer(serializers.Serializer):
))
def validate(self, attrs):
apply_date_start = attrs['apply_date_start']
apply_date_expired = attrs['apply_date_expired']
apply_date_start = attrs['apply_date_start'].strftime('%Y-%m-%d %H:%M:%S')
apply_date_expired = attrs['apply_date_expired'].strftime('%Y-%m-%d %H:%M:%S')
if apply_date_expired <= apply_date_start:
error = _('The expiration date should be greater than the start date')
raise serializers.ValidationError({'apply_date_expired': error})
attrs['apply_date_start'] = apply_date_start
attrs['apply_date_expired'] = apply_date_expired
return attrs

View File

@ -5,7 +5,10 @@ from rest_framework import generics
from common.permissions import IsOrgAdmin
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 (
IsCurrentUserOrReadOnly
)
@ -87,4 +90,4 @@ class UserPublicKeyApi(generics.RetrieveUpdateAPIView):
def perform_update(self, 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__)
__all__ = [
'UserViewSet', 'UserChangePasswordApi',
'UserUnblockPKApi', 'UserResetOTPApi',
'UserUnblockPKApi', 'UserResetMFAApi',
]
@ -199,7 +199,7 @@ class UserUnblockPKApi(UserQuerysetMixin, generics.UpdateAPIView):
MFABlockUtils.unblock_user(username)
class UserResetOTPApi(UserQuerysetMixin, generics.RetrieveAPIView):
class UserResetMFAApi(UserQuerysetMixin, generics.RetrieveAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.ResetOTPSerializer
@ -209,9 +209,10 @@ class UserResetOTPApi(UserQuerysetMixin, generics.RetrieveAPIView):
msg = _("Could not reset self otp, use profile reset instead")
return Response({"error": msg}, status=401)
if user.mfa_enabled:
user.reset_mfa()
user.save()
backends = user.active_mfa_backends_mapper
for backend in backends:
if backend.can_disable():
backend.disable()
ResetMFAMsg(user).publish_async()
ResetMFAMsg(user).publish_async()
return Response({"msg": "success"})

View File

@ -507,8 +507,14 @@ class MFAMixin:
backends = [cls(user) for cls in MFA_BACKENDS if cls.global_enabled()]
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):
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)
if not backend:
return None

View File

@ -105,6 +105,38 @@ class ResetPasswordSuccessMsg(UserMessage):
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):
def get_html_msg(self) -> dict:
user = self.user

View File

@ -23,8 +23,8 @@ urlpatterns = [
path('profile/', api.UserProfileApi.as_view(), name='user-profile'),
path('profile/password/', api.UserPasswordApi.as_view(), name='user-password'),
path('profile/public-key/', api.UserPublicKeyApi.as_view(), name='user-public-key'),
path('otp/reset/', api.UserResetOTPApi.as_view(), name='my-otp-reset'),
path('users/<uuid:pk>/otp/reset/', api.UserResetOTPApi.as_view(), name='user-reset-otp'),
path('profile/mfa/reset/', api.UserResetMFAApi.as_view(), name='my-mfa-reset'),
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/reset/', api.UserResetPasswordApi.as_view(), name='user-reset-password'),
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 .models import User
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
else:
min_length = settings.SECURITY_PASSWORD_MIN_LENGTH
pattern += '.{' + str(min_length-1) + ',}$'
pattern += '.{' + str(min_length - 1) + ',}$'
match_obj = re.match(pattern, password)
return bool(match_obj)
@ -173,6 +172,33 @@ class BlockUtilBase:
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):
LIMIT_KEY_TMPL = "_LOGIN_LIMIT_{}_{}"
BLOCK_KEY_TMPL = "_LOGIN_BLOCK_{}"
@ -183,6 +209,11 @@ class MFABlockUtils(BlockUtilBase):
BLOCK_KEY_TMPL = "_MFA_BLOCK_{}"
class LoginIpBlockUtil(BlockGlobalIpUtilBase):
LIMIT_KEY_TMPL = "_LOGIN_LIMIT_{}"
BLOCK_KEY_TMPL = "_LOGIN_BLOCK_{}"
def construct_user_email(username, email):
if '@' not in email:
if '@' in username: