diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index abd78d9eb..9a75b4691 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -51,10 +51,14 @@ invalid_login_msg = _( "You can also try {times_try} times " "(The account will be temporarily locked for {block_time} minutes)" ) -block_login_msg = _( +block_user_login_msg = _( "The account has been locked " "(please contact admin to unlock it or try again after {} minutes)" ) +block_ip_login_msg = _( + "The ip has been locked " + "(please contact admin to unlock it or try again after {} minutes)" +) block_mfa_msg = _( "The account has been locked " "(please contact admin to unlock it or try again after {} minutes)" @@ -118,7 +122,7 @@ class BlockGlobalIpLoginError(AuthFailedError): error = 'block_global_ip_login' def __init__(self, username, ip, **kwargs): - self.msg = _("IP is not allowed") + self.msg = block_ip_login_msg.format(settings.SECURITY_LOGIN_IP_LIMIT_TIME) LoginIpBlockUtil(ip).set_block_if_need() super().__init__(username=username, ip=ip, **kwargs) @@ -133,7 +137,7 @@ class CredentialError( block_time = settings.SECURITY_LOGIN_LIMIT_TIME if times_remainder < 1: - self.msg = block_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME) + self.msg = block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME) return default_msg = invalid_login_msg.format( @@ -184,7 +188,7 @@ class BlockLoginError(AuthFailedNeedBlockMixin, AuthFailedError): error = 'block_login' def __init__(self, username, ip): - self.msg = block_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME) + self.msg = block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME) super().__init__(username=username, ip=ip) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 9d95609d2..a0f535414 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -291,8 +291,11 @@ class Config(dict): 'SECURITY_COMMAND_EXECUTION': True, 'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True, 'SECURITY_VIEW_AUTH_NEED_MFA': True, - 'SECURITY_LOGIN_LIMIT_COUNT': 7, 'SECURITY_LOGIN_IP_BLACK_LIST': [], + 'SECURITY_LOGIN_IP_WHITE_LIST': [], + 'SECURITY_LOGIN_LIMIT_COUNT': 7, + 'SECURITY_LOGIN_IP_LIMIT_COUNT': 99999, + 'SECURITY_LOGIN_IP_LIMIT_TIME': 30, 'SECURITY_LOGIN_LIMIT_TIME': 30, 'SECURITY_MAX_IDLE_TIME': 30, 'SECURITY_PASSWORD_EXPIRATION_TIME': 9999, diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 0673cbc1d..04e5905e4 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -35,6 +35,9 @@ 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_IP_WHITE_LIST = CONFIG.SECURITY_LOGIN_IP_WHITE_LIST +SECURITY_LOGIN_IP_LIMIT_COUNT = CONFIG.SECURITY_LOGIN_IP_LIMIT_COUNT +SECURITY_LOGIN_IP_LIMIT_TIME = CONFIG.SECURITY_LOGIN_IP_LIMIT_TIME 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 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 8d9bd1f22..a4239f2bd 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-11-18 19:00+0800\n" +"POT-Creation-Date: 2021-11-19 10:40+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -1619,13 +1619,19 @@ msgstr "" "您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将" "被临时 锁定 {block_time} 分钟)" -#: authentication/errors.py:55 authentication/errors.py:59 +#: authentication/errors.py:55 authentication/errors.py:63 msgid "" "The account has been locked (please contact admin to unlock it or try again " "after {} minutes)" msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)" -#: authentication/errors.py:63 +#: authentication/errors.py:59 +msgid "" +"The ip has been locked (please contact admin to unlock it or try again after " +"{} minutes)" +msgstr "IP已被锁定(请联系管理员解锁 或 {}分钟后重试)" + +#: authentication/errors.py:67 #, python-brace-format msgid "" "{error}, You can also try {times_try} times (The account will be temporarily " @@ -1633,63 +1639,63 @@ msgid "" msgstr "" "{error},您还可以尝试 {times_try} 次(账号将被临时锁定 {block_time} 分钟)" -#: authentication/errors.py:67 +#: authentication/errors.py:71 msgid "MFA required" msgstr "需要 MFA 认证" -#: authentication/errors.py:68 +#: authentication/errors.py:72 msgid "MFA not set, please set it first" msgstr "MFA 没有设置,请先完成设置" -#: authentication/errors.py:69 +#: authentication/errors.py:73 msgid "Login confirm required" msgstr "需要登录复核" -#: authentication/errors.py:70 +#: authentication/errors.py:74 msgid "Wait login confirm ticket for accept" msgstr "等待登录复核处理" -#: authentication/errors.py:71 +#: authentication/errors.py:75 msgid "Login confirm ticket was {}" msgstr "登录复核 {}" -#: authentication/errors.py:121 authentication/errors.py:251 +#: authentication/errors.py:255 msgid "IP is not allowed" msgstr "来源 IP 不被允许登录" -#: authentication/errors.py:258 +#: authentication/errors.py:262 msgid "Time Period is not allowed" msgstr "该 时间段 不被允许登录" -#: authentication/errors.py:291 +#: authentication/errors.py:295 msgid "SSO auth closed" msgstr "SSO 认证关闭了" -#: authentication/errors.py:296 authentication/mixins.py:360 +#: authentication/errors.py:300 authentication/mixins.py:360 msgid "Your password is too simple, please change it for security" msgstr "你的密码过于简单,为了安全,请修改" -#: authentication/errors.py:305 authentication/mixins.py:367 +#: authentication/errors.py:309 authentication/mixins.py:367 msgid "You should to change your password before login" msgstr "登录完成前,请先修改密码" -#: authentication/errors.py:314 authentication/mixins.py:374 +#: authentication/errors.py:318 authentication/mixins.py:374 msgid "Your password has expired, please reset before logging in" msgstr "您的密码已过期,先修改再登录" -#: authentication/errors.py:348 +#: authentication/errors.py:352 msgid "Your password is invalid" msgstr "您的密码无效" -#: authentication/errors.py:353 +#: authentication/errors.py:357 msgid "Please enter MFA code" msgstr "请输入 MFA 验证码" -#: authentication/errors.py:358 +#: authentication/errors.py:362 msgid "Please enter SMS code" msgstr "请输入短信验证码" -#: authentication/errors.py:363 users/exceptions.py:15 +#: authentication/errors.py:367 users/exceptions.py:15 msgid "Phone not set" msgstr "手机号没有设置" @@ -3339,12 +3345,12 @@ msgid "Global MFA auth" msgstr "全局启用 MFA 认证" #: settings/serializers/security.py:50 -msgid "Limit the number of login failures" -msgstr "限制登录失败次数" +msgid "Limit the number of user login failures" +msgstr "限制用户登录失败次数" #: settings/serializers/security.py:54 -msgid "Block logon interval" -msgstr "禁止登录时间间隔" +msgid "Block user login interval" +msgstr "禁止用户登录时间间隔" #: settings/serializers/security.py:56 msgid "" @@ -3352,11 +3358,25 @@ msgid "" "times, no login is allowed during this time interval." msgstr "单位:分, 当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录" -#: settings/serializers/security.py:61 -msgid "Login IP Black List" -msgstr "登录 IP 黑名单" +#: settings/serializers/security.py:62 +msgid "Limit the number of IP login failures" +msgstr "限制IP登录失败次数" -#: settings/serializers/security.py:64 +#: settings/serializers/security.py:66 +msgid "Block IP login interval" +msgstr "禁止IP登录时间间隔" + +#: settings/serializers/security.py:68 +msgid "" +"Unit: minute, If the IP has failed to log in for a limited number of times, " +"no login is allowed during this time interval." +msgstr "单位:分, 当IP登录失败次数达到限制后,那么在此时间间隔内禁止登录" + +#: settings/serializers/security.py:73 +msgid "Login IP White List" +msgstr "IP登录白名单" + +#: settings/serializers/security.py:76 settings/serializers/security.py:84 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" @@ -3364,11 +3384,15 @@ 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 (支持网域)" -#: settings/serializers/security.py:70 +#: settings/serializers/security.py:81 +msgid "Login IP Black List" +msgstr "IP登录黑名单" + +#: settings/serializers/security.py:90 msgid "User password expiration" msgstr "用户密码过期时间" -#: settings/serializers/security.py:72 +#: settings/serializers/security.py:92 msgid "" "Unit: day, If the user does not update the password during the time, the " "user password will expire failure;The password expiration reminder mail will " @@ -3378,55 +3402,55 @@ msgstr "" "单位:天, 如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期提醒邮件" "将在密码过期前5天内由系统(每天)自动发送给用户" -#: settings/serializers/security.py:79 +#: settings/serializers/security.py:99 msgid "Number of repeated historical passwords" msgstr "不能设置近几次密码" -#: settings/serializers/security.py:81 +#: settings/serializers/security.py:101 msgid "" "Tip: When the user resets the password, it cannot be the previous n " "historical passwords of the user" msgstr "提示:用户重置密码时,不能为该用户前几次使用过的密码" -#: settings/serializers/security.py:86 +#: settings/serializers/security.py:106 msgid "Only single device login" msgstr "仅一台设备登录" -#: settings/serializers/security.py:87 +#: settings/serializers/security.py:107 msgid "Next device login, pre login will be logout" msgstr "下个设备登录,上次登录会被顶掉" -#: settings/serializers/security.py:90 +#: settings/serializers/security.py:110 msgid "Only exist user login" msgstr "仅已存在用户登录" -#: settings/serializers/security.py:91 +#: settings/serializers/security.py:111 msgid "If enable, CAS、OIDC auth will be failed, if user not exist yet" msgstr "开启后,如果系统中不存在该用户,CAS、OIDC 登录将会失败" -#: settings/serializers/security.py:94 +#: settings/serializers/security.py:114 msgid "Only from source login" msgstr "仅从用户来源登录" -#: settings/serializers/security.py:95 +#: settings/serializers/security.py:115 msgid "Only log in from the user source property" msgstr "开启后,如果用户来源为本地,CAS、OIDC 登录将会失败" -#: settings/serializers/security.py:99 +#: settings/serializers/security.py:119 msgid "MFA verify TTL" msgstr "MFA 校验有效期" -#: settings/serializers/security.py:101 +#: settings/serializers/security.py:121 msgid "" "Unit: second, The verification MFA takes effect only when you view the " "account password" msgstr "单位: 秒, 目前仅在查看账号密码校验 MFA 时生效" -#: settings/serializers/security.py:106 +#: settings/serializers/security.py:126 msgid "Enable Login dynamic code" msgstr "启用登录附加码" -#: settings/serializers/security.py:107 +#: settings/serializers/security.py:127 msgid "" "The password and additional code are sent to a third party authentication " "system for verification" @@ -3434,89 +3458,89 @@ msgstr "" "密码和附加码一并发送给第三方认证系统进行校验, 如:有的第三方认证系统,需要 密" "码+6位数字 完成认证" -#: settings/serializers/security.py:112 +#: settings/serializers/security.py:132 msgid "MFA in login page" msgstr "MFA 在登录页面输入" -#: settings/serializers/security.py:113 +#: settings/serializers/security.py:133 msgid "Eu security regulations(GDPR) require MFA to be on the login page" msgstr "欧盟数据安全法规(GDPR) 要求 MFA 在登录页面,来确保系统登录安全" -#: settings/serializers/security.py:116 +#: settings/serializers/security.py:136 msgid "Enable Login captcha" msgstr "启用登录验证码" -#: settings/serializers/security.py:117 +#: settings/serializers/security.py:137 msgid "Enable captcha to prevent robot authentication" msgstr "开启验证码,防止机器人登录" -#: settings/serializers/security.py:137 +#: settings/serializers/security.py:157 msgid "Enable terminal register" msgstr "终端注册" -#: settings/serializers/security.py:139 +#: settings/serializers/security.py:159 msgid "" "Allow terminal register, after all terminal setup, you should disable this " "for security" msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭" -#: settings/serializers/security.py:143 +#: settings/serializers/security.py:163 msgid "Enable watermark" msgstr "开启水印" -#: settings/serializers/security.py:144 +#: settings/serializers/security.py:164 msgid "Enabled, the web session and replay contains watermark information" msgstr "启用后,Web 会话和录像将包含水印信息" -#: settings/serializers/security.py:148 +#: settings/serializers/security.py:168 msgid "Connection max idle time" msgstr "连接最大空闲时间" -#: settings/serializers/security.py:149 +#: settings/serializers/security.py:169 msgid "If idle time more than it, disconnect connection Unit: minute" msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)" -#: settings/serializers/security.py:152 +#: settings/serializers/security.py:172 msgid "Remember manual auth" msgstr "保存手动输入密码" -#: settings/serializers/security.py:155 +#: settings/serializers/security.py:175 msgid "Enable change auth secure mode" msgstr "启用改密安全模式" -#: settings/serializers/security.py:158 +#: settings/serializers/security.py:178 msgid "Insecure command alert" msgstr "危险命令告警" -#: settings/serializers/security.py:161 +#: settings/serializers/security.py:181 msgid "Email recipient" msgstr "邮件收件人" -#: settings/serializers/security.py:162 +#: settings/serializers/security.py:182 msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" -#: settings/serializers/security.py:165 +#: settings/serializers/security.py:185 msgid "Batch command execution" msgstr "批量命令执行" -#: settings/serializers/security.py:166 +#: settings/serializers/security.py:186 msgid "Allow user run batch command or not using ansible" msgstr "是否允许用户使用 ansible 执行批量命令" -#: settings/serializers/security.py:169 +#: settings/serializers/security.py:189 msgid "Session share" msgstr "会话分享" -#: settings/serializers/security.py:170 +#: settings/serializers/security.py:190 msgid "Enabled, Allows user active session to be shared with other users" msgstr "开启后允许用户分享已连接的资产会话给它人,协同工作" -#: settings/serializers/security.py:173 +#: settings/serializers/security.py:193 msgid "Remote Login Protection" msgstr "异地登录保护" -#: settings/serializers/security.py:175 +#: settings/serializers/security.py:195 msgid "" "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 " diff --git a/apps/settings/serializers/security.py b/apps/settings/serializers/security.py index b8c9e6de6..aad205616 100644 --- a/apps/settings/serializers/security.py +++ b/apps/settings/serializers/security.py @@ -47,16 +47,36 @@ class SecurityAuthSerializer(serializers.Serializer): ) SECURITY_LOGIN_LIMIT_COUNT = serializers.IntegerField( min_value=3, max_value=99999, - label=_('Limit the number of login failures') + label=_('Limit the number of user login failures') ) SECURITY_LOGIN_LIMIT_TIME = serializers.IntegerField( min_value=5, max_value=99999, required=True, - label=_('Block logon interval'), + label=_('Block user login interval'), help_text=_( 'Unit: minute, If the user has failed to log in for a limited number of times, ' 'no login is allowed during this time interval.' ) ) + SECURITY_LOGIN_IP_LIMIT_COUNT = serializers.IntegerField( + min_value=3, max_value=99999, + label=_('Limit the number of IP login failures') + ) + SECURITY_LOGIN_IP_LIMIT_TIME = serializers.IntegerField( + min_value=5, max_value=99999, required=True, + label=_('Block IP login interval'), + help_text=_( + 'Unit: minute, If the IP has failed to log in for a limited number of times, ' + 'no login is allowed during this time interval.' + ) + ) + SECURITY_LOGIN_IP_WHITE_LIST = serializers.ListField( + default=[], label=_('Login IP White 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_LOGIN_IP_BLACK_LIST = serializers.ListField( default=[], label=_('Login IP Black List'), allow_empty=True, child=serializers.CharField(max_length=1024, validators=[ip_child_validator]), diff --git a/apps/users/utils.py b/apps/users/utils.py index 370240d75..807c7cc0f 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -180,34 +180,38 @@ class BlockGlobalIpUtilBase: 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 + self.key_ttl = int(settings.SECURITY_LOGIN_IP_LIMIT_TIME) * 60 @property def ip_in_black_list(self): return self.ip in settings.SECURITY_LOGIN_IP_BLACK_LIST - def set_block_if_need(self): - if not self.ip_in_black_list: - return - count = cache.get(self.limit_key, 0) - count += 1 - cache.set(self.limit_key, count, self.key_ttl) + @property + def ip_in_white_list(self): + return self.ip in settings.SECURITY_LOGIN_IP_WHITE_LIST - limit_count = settings.SECURITY_LOGIN_LIMIT_COUNT - if count < limit_count: - return - cache.set(self.block_key, True, self.key_ttl) + def set_block_if_need(self): + if not self.ip_in_white_list and not self.ip_in_black_list: + count = cache.get(self.limit_key, 0) + count += 1 + cache.set(self.limit_key, count, self.key_ttl) + + limit_count = settings.SECURITY_LOGIN_IP_LIMIT_COUNT + if count < limit_count: + return + cache.set(self.block_key, True, self.key_ttl) def clean_block_if_need(self): - if not self.ip_in_black_list: - return cache.delete(self.limit_key) cache.delete(self.block_key) def is_block(self): - if not self.ip_in_black_list: + if self.ip_in_white_list: return False - return bool(cache.get(self.block_key)) + if self.ip_in_black_list: + return True + if not self.ip_in_white_list and not self.ip_in_black_list: + return bool(cache.get(self.block_key)) class LoginBlockUtil(BlockUtilBase):