feat: 添加全局ip黑名单

pull/7182/head
feng626 2021-11-11 19:03:01 +08:00 committed by 老广
parent 353b66bf8f
commit 90477146ed
8 changed files with 103 additions and 14 deletions

View File

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

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

View File

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

View File

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

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

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