mirror of https://github.com/jumpserver/jumpserver
feat: 添加全局ip黑名单
parent
353b66bf8f
commit
90477146ed
|
@ -180,6 +180,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'
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -34,6 +34,7 @@ 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
|
||||
|
|
|
@ -171,6 +171,16 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. "
|
|||
msgid "Username"
|
||||
msgstr "用户名"
|
||||
|
||||
msgid "IP Black List"
|
||||
msgstr "IP 黑名单"
|
||||
|
||||
msgid ""
|
||||
"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"
|
||||
msgstr ""
|
||||
"格式为逗号分隔的字符串。例如: 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 (支持网域)"
|
||||
|
||||
#: acls/serializers/login_asset_acl.py:24
|
||||
msgid ""
|
||||
"Format for comma-delimited string, with * indicating a match all. Such as: "
|
||||
|
|
|
@ -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'
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue