mirror of https://github.com/jumpserver/jumpserver
Merge pull request #1503 from jumpserver/update_loginlog
[Update] 记录用户登录失败日志,限制用户登录失败次数pull/1508/head
commit
df95c93bb1
|
@ -170,7 +170,7 @@ class TerminalSettingForm(BaseForm):
|
||||||
|
|
||||||
|
|
||||||
class SecuritySettingForm(BaseForm):
|
class SecuritySettingForm(BaseForm):
|
||||||
# MFA全局设置
|
# MFA global setting
|
||||||
SECURITY_MFA_AUTH = forms.BooleanField(
|
SECURITY_MFA_AUTH = forms.BooleanField(
|
||||||
initial=False, required=False,
|
initial=False, required=False,
|
||||||
label=_("MFA Secondary certification"),
|
label=_("MFA Secondary certification"),
|
||||||
|
@ -179,12 +179,26 @@ class SecuritySettingForm(BaseForm):
|
||||||
'authentication (valid for all users, including administrators)'
|
'authentication (valid for all users, including administrators)'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# 最小长度
|
# limit login count
|
||||||
|
SECURITY_LOGIN_LIMIT_COUNT = forms.IntegerField(
|
||||||
|
initial=3, min_value=3,
|
||||||
|
label=_("Limit the number of login failures")
|
||||||
|
)
|
||||||
|
# limit login time
|
||||||
|
SECURITY_LOGIN_LIMIT_TIME = forms.IntegerField(
|
||||||
|
initial=30, min_value=5,
|
||||||
|
label=_("No logon interval"),
|
||||||
|
help_text=_(
|
||||||
|
"Tip :(unit/minute) if the user has failed to log in for a limited "
|
||||||
|
"number of times, no login is allowed during this time interval."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# min length
|
||||||
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
|
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
|
||||||
initial=6, label=_("Password minimum length"),
|
initial=6, label=_("Password minimum length"),
|
||||||
min_value=6
|
min_value=6
|
||||||
)
|
)
|
||||||
# 大写字母
|
# upper case
|
||||||
SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField(
|
SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField(
|
||||||
|
|
||||||
initial=False, required=False,
|
initial=False, required=False,
|
||||||
|
@ -193,21 +207,21 @@ class SecuritySettingForm(BaseForm):
|
||||||
'After opening, the user password changes '
|
'After opening, the user password changes '
|
||||||
'and resets must contain uppercase letters')
|
'and resets must contain uppercase letters')
|
||||||
)
|
)
|
||||||
# 小写字母
|
# lower case
|
||||||
SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField(
|
SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField(
|
||||||
initial=False, required=False,
|
initial=False, required=False,
|
||||||
label=_("Must contain lowercase letters"),
|
label=_("Must contain lowercase letters"),
|
||||||
help_text=_('After opening, the user password changes '
|
help_text=_('After opening, the user password changes '
|
||||||
'and resets must contain lowercase letters')
|
'and resets must contain lowercase letters')
|
||||||
)
|
)
|
||||||
# 数字
|
# number
|
||||||
SECURITY_PASSWORD_NUMBER = forms.BooleanField(
|
SECURITY_PASSWORD_NUMBER = forms.BooleanField(
|
||||||
initial=False, required=False,
|
initial=False, required=False,
|
||||||
label=_("Must contain numeric characters"),
|
label=_("Must contain numeric characters"),
|
||||||
help_text=_('After opening, the user password changes '
|
help_text=_('After opening, the user password changes '
|
||||||
'and resets must contain numeric characters')
|
'and resets must contain numeric characters')
|
||||||
)
|
)
|
||||||
# 特殊字符
|
# special char
|
||||||
SECURITY_PASSWORD_SPECIAL_CHAR= forms.BooleanField(
|
SECURITY_PASSWORD_SPECIAL_CHAR= forms.BooleanField(
|
||||||
initial=False, required=False,
|
initial=False, required=False,
|
||||||
label=_("Must contain special characters"),
|
label=_("Must contain special characters"),
|
||||||
|
|
|
@ -39,9 +39,9 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<h3>{% trans "MFA setting" %}</h3>
|
<h3>{% trans "User login settings" %}</h3>
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
{% if forloop.counter == 2 %}
|
{% if forloop.counter == 4 %}
|
||||||
<div class="hr-line-dashed"></div>
|
<div class="hr-line-dashed"></div>
|
||||||
<h3>{% trans "Password check rule" %}</h3>
|
<h3>{% trans "Password check rule" %}</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
Binary file not shown.
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Jumpserver 0.3.3\n"
|
"Project-Id-Version: Jumpserver 0.3.3\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2018-06-25 12:19+0800\n"
|
"POT-Creation-Date: 2018-07-06 13:11+0800\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: ibuler <ibuler@qq.com>\n"
|
"Last-Translator: ibuler <ibuler@qq.com>\n"
|
||||||
"Language-Team: Jumpserver team<ibuler@qq.com>\n"
|
"Language-Team: Jumpserver team<ibuler@qq.com>\n"
|
||||||
|
@ -118,7 +118,7 @@ msgstr "端口"
|
||||||
msgid "Asset"
|
msgid "Asset"
|
||||||
msgstr "资产"
|
msgstr "资产"
|
||||||
|
|
||||||
#: assets/forms/domain.py:54 assets/forms/user.py:79 assets/forms/user.py:138
|
#: assets/forms/domain.py:54 assets/forms/user.py:79 assets/forms/user.py:139
|
||||||
#: assets/models/base.py:21 assets/models/cluster.py:18
|
#: assets/models/base.py:21 assets/models/cluster.py:18
|
||||||
#: assets/models/domain.py:17 assets/models/group.py:20
|
#: assets/models/domain.py:17 assets/models/group.py:20
|
||||||
#: assets/models/label.py:17 assets/templates/assets/admin_user_detail.html:56
|
#: assets/models/label.py:17 assets/templates/assets/admin_user_detail.html:56
|
||||||
|
@ -147,16 +147,16 @@ msgstr "资产"
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr "名称"
|
msgstr "名称"
|
||||||
|
|
||||||
#: assets/forms/domain.py:55 assets/forms/user.py:80 assets/forms/user.py:139
|
#: assets/forms/domain.py:55 assets/forms/user.py:80 assets/forms/user.py:140
|
||||||
#: assets/models/base.py:22 assets/templates/assets/admin_user_detail.html:60
|
#: assets/models/base.py:22 assets/templates/assets/admin_user_detail.html:60
|
||||||
#: assets/templates/assets/admin_user_list.html:24
|
#: assets/templates/assets/admin_user_list.html:24
|
||||||
#: assets/templates/assets/domain_gateway_list.html:60
|
#: assets/templates/assets/domain_gateway_list.html:60
|
||||||
#: assets/templates/assets/system_user_detail.html:62
|
#: assets/templates/assets/system_user_detail.html:62
|
||||||
#: assets/templates/assets/system_user_list.html:27
|
#: assets/templates/assets/system_user_list.html:27
|
||||||
#: perms/templates/perms/asset_permission_user.html:55 users/forms.py:13
|
#: perms/templates/perms/asset_permission_user.html:55 users/forms.py:13
|
||||||
#: users/forms.py:31 users/models/authentication.py:45 users/models/user.py:47
|
#: users/forms.py:31 users/models/authentication.py:70 users/models/user.py:47
|
||||||
#: users/templates/users/_select_user_modal.html:14
|
#: users/templates/users/_select_user_modal.html:14
|
||||||
#: users/templates/users/login.html:56
|
#: users/templates/users/login.html:60
|
||||||
#: users/templates/users/login_log_list.html:49
|
#: users/templates/users/login_log_list.html:49
|
||||||
#: users/templates/users/user_detail.html:67
|
#: users/templates/users/user_detail.html:67
|
||||||
#: users/templates/users/user_list.html:24
|
#: users/templates/users/user_list.html:24
|
||||||
|
@ -170,7 +170,7 @@ msgstr "密码或密钥密码"
|
||||||
|
|
||||||
#: assets/forms/user.py:25 assets/models/base.py:23 common/forms.py:113
|
#: assets/forms/user.py:25 assets/models/base.py:23 common/forms.py:113
|
||||||
#: users/forms.py:15 users/forms.py:33 users/forms.py:45
|
#: users/forms.py:15 users/forms.py:33 users/forms.py:45
|
||||||
#: users/templates/users/login.html:59
|
#: users/templates/users/login.html:63
|
||||||
#: users/templates/users/reset_password.html:53
|
#: users/templates/users/reset_password.html:53
|
||||||
#: users/templates/users/user_create.html:10
|
#: users/templates/users/user_create.html:10
|
||||||
#: users/templates/users/user_password_authentication.html:14
|
#: users/templates/users/user_password_authentication.html:14
|
||||||
|
@ -192,21 +192,21 @@ msgstr "ssh密钥不合法"
|
||||||
msgid "Password and private key file must be input one"
|
msgid "Password and private key file must be input one"
|
||||||
msgstr "密码和私钥, 必须输入一个"
|
msgstr "密码和私钥, 必须输入一个"
|
||||||
|
|
||||||
#: assets/forms/user.py:124
|
#: assets/forms/user.py:125
|
||||||
msgid "* Automatic login mode, must fill in the username."
|
msgid "* Automatic login mode, must fill in the username."
|
||||||
msgstr "自动登录模式,必须填写用户名"
|
msgstr "自动登录模式,必须填写用户名"
|
||||||
|
|
||||||
#: assets/forms/user.py:144
|
#: assets/forms/user.py:145
|
||||||
msgid "Auto push system user to asset"
|
msgid "Auto push system user to asset"
|
||||||
msgstr "自动推送系统用户到资产"
|
msgstr "自动推送系统用户到资产"
|
||||||
|
|
||||||
#: assets/forms/user.py:145
|
#: assets/forms/user.py:146
|
||||||
msgid ""
|
msgid ""
|
||||||
"High level will be using login asset as default, if user was granted more "
|
"High level will be using login asset as default, if user was granted more "
|
||||||
"than 2 system user"
|
"than 2 system user"
|
||||||
msgstr "高优先级的系统用户将会作为默认登录用户"
|
msgstr "高优先级的系统用户将会作为默认登录用户"
|
||||||
|
|
||||||
#: assets/forms/user.py:147
|
#: assets/forms/user.py:148
|
||||||
msgid ""
|
msgid ""
|
||||||
"If you choose manual login mode, you do not need to fill in the username and "
|
"If you choose manual login mode, you do not need to fill in the username and "
|
||||||
"password."
|
"password."
|
||||||
|
@ -480,7 +480,7 @@ msgstr "手动登录"
|
||||||
#: assets/views/asset.py:197 assets/views/domain.py:29
|
#: assets/views/asset.py:197 assets/views/domain.py:29
|
||||||
#: assets/views/domain.py:45 assets/views/domain.py:61
|
#: assets/views/domain.py:45 assets/views/domain.py:61
|
||||||
#: assets/views/domain.py:74 assets/views/domain.py:98
|
#: assets/views/domain.py:74 assets/views/domain.py:98
|
||||||
#: assets/views/domain.py:126 assets/views/domain.py:150
|
#: assets/views/domain.py:126 assets/views/domain.py:145
|
||||||
#: assets/views/label.py:26 assets/views/label.py:42 assets/views/label.py:58
|
#: assets/views/label.py:26 assets/views/label.py:42 assets/views/label.py:58
|
||||||
#: assets/views/system_user.py:28 assets/views/system_user.py:44
|
#: assets/views/system_user.py:28 assets/views/system_user.py:44
|
||||||
#: assets/views/system_user.py:60 assets/views/system_user.py:74
|
#: assets/views/system_user.py:60 assets/views/system_user.py:74
|
||||||
|
@ -685,7 +685,7 @@ msgstr "重置"
|
||||||
#: common/templates/common/security_setting.html:71
|
#: common/templates/common/security_setting.html:71
|
||||||
#: common/templates/common/terminal_setting.html:108
|
#: common/templates/common/terminal_setting.html:108
|
||||||
#: perms/templates/perms/asset_permission_create_update.html:70
|
#: perms/templates/perms/asset_permission_create_update.html:70
|
||||||
#: terminal/templates/terminal/session_list.html:124
|
#: terminal/templates/terminal/session_list.html:126
|
||||||
#: terminal/templates/terminal/terminal_update.html:48
|
#: terminal/templates/terminal/terminal_update.html:48
|
||||||
#: users/templates/users/_user.html:47
|
#: users/templates/users/_user.html:47
|
||||||
#: users/templates/users/forgot_password.html:44
|
#: users/templates/users/forgot_password.html:44
|
||||||
|
@ -847,7 +847,7 @@ msgstr "比例"
|
||||||
#: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:64
|
#: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:64
|
||||||
#: ops/templates/ops/task_history.html:65 ops/templates/ops/task_list.html:42
|
#: ops/templates/ops/task_history.html:65 ops/templates/ops/task_list.html:42
|
||||||
#: perms/templates/perms/asset_permission_list.html:60
|
#: perms/templates/perms/asset_permission_list.html:60
|
||||||
#: terminal/templates/terminal/session_list.html:80
|
#: terminal/templates/terminal/session_list.html:81
|
||||||
#: terminal/templates/terminal/terminal_list.html:36
|
#: terminal/templates/terminal/terminal_list.html:36
|
||||||
#: users/templates/users/user_group_list.html:15
|
#: users/templates/users/user_group_list.html:15
|
||||||
#: users/templates/users/user_list.html:29
|
#: users/templates/users/user_list.html:29
|
||||||
|
@ -1189,7 +1189,7 @@ msgstr "网域详情"
|
||||||
msgid "Domain gateway list"
|
msgid "Domain gateway list"
|
||||||
msgstr "域网关列表"
|
msgstr "域网关列表"
|
||||||
|
|
||||||
#: assets/views/domain.py:151
|
#: assets/views/domain.py:146
|
||||||
msgid "Update gateway"
|
msgid "Update gateway"
|
||||||
msgstr "创建网关"
|
msgstr "创建网关"
|
||||||
|
|
||||||
|
@ -1237,7 +1237,7 @@ msgid "Filename"
|
||||||
msgstr "文件名"
|
msgstr "文件名"
|
||||||
|
|
||||||
#: audits/models.py:15 audits/templates/audits/ftp_log_list.html:77
|
#: audits/models.py:15 audits/templates/audits/ftp_log_list.html:77
|
||||||
#: ops/templates/ops/task_list.html:39
|
#: ops/templates/ops/task_list.html:39 users/models/authentication.py:66
|
||||||
msgid "Success"
|
msgid "Success"
|
||||||
msgstr "成功"
|
msgstr "成功"
|
||||||
|
|
||||||
|
@ -1246,7 +1246,7 @@ msgstr "成功"
|
||||||
#: ops/templates/ops/adhoc_history_detail.html:61
|
#: ops/templates/ops/adhoc_history_detail.html:61
|
||||||
#: ops/templates/ops/task_history.html:58 perms/models.py:36
|
#: ops/templates/ops/task_history.html:58 perms/models.py:36
|
||||||
#: perms/templates/perms/asset_permission_detail.html:86 terminal/models.py:137
|
#: perms/templates/perms/asset_permission_detail.html:86 terminal/models.py:137
|
||||||
#: terminal/templates/terminal/session_list.html:77
|
#: terminal/templates/terminal/session_list.html:78
|
||||||
msgid "Date start"
|
msgid "Date start"
|
||||||
msgstr "开始日期"
|
msgstr "开始日期"
|
||||||
|
|
||||||
|
@ -1433,45 +1433,60 @@ msgid ""
|
||||||
"for all users, including administrators)"
|
"for all users, including administrators)"
|
||||||
msgstr "开启后,用户登录必须使用MFA二次认证(对所有用户有效,包括管理员)"
|
msgstr "开启后,用户登录必须使用MFA二次认证(对所有用户有效,包括管理员)"
|
||||||
|
|
||||||
#: common/forms.py:184
|
#: common/forms.py:185
|
||||||
|
msgid "Limit the number of login failures"
|
||||||
|
msgstr "限制登录失败次数"
|
||||||
|
|
||||||
|
#: common/forms.py:190
|
||||||
|
msgid "No logon interval"
|
||||||
|
msgstr "禁止登录时间间隔"
|
||||||
|
|
||||||
|
#: common/forms.py:192
|
||||||
|
msgid ""
|
||||||
|
"Tip :(unit/minute) if the user has failed to log in for a limited number of "
|
||||||
|
"times, no login is allowed during this time interval."
|
||||||
|
msgstr ""
|
||||||
|
"提示:(单位 / 分钟)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录."
|
||||||
|
|
||||||
|
#: common/forms.py:198
|
||||||
msgid "Password minimum length"
|
msgid "Password minimum length"
|
||||||
msgstr "密码最小长度 "
|
msgstr "密码最小长度 "
|
||||||
|
|
||||||
#: common/forms.py:191
|
#: common/forms.py:205
|
||||||
msgid "Must contain capital letters"
|
msgid "Must contain capital letters"
|
||||||
msgstr "必须包含大写字母"
|
msgstr "必须包含大写字母"
|
||||||
|
|
||||||
#: common/forms.py:193
|
#: common/forms.py:207
|
||||||
msgid ""
|
msgid ""
|
||||||
"After opening, the user password changes and resets must contain uppercase "
|
"After opening, the user password changes and resets must contain uppercase "
|
||||||
"letters"
|
"letters"
|
||||||
msgstr "开启后,用户密码修改、重置必须包含大写字母"
|
msgstr "开启后,用户密码修改、重置必须包含大写字母"
|
||||||
|
|
||||||
#: common/forms.py:199
|
#: common/forms.py:213
|
||||||
msgid "Must contain lowercase letters"
|
msgid "Must contain lowercase letters"
|
||||||
msgstr "必须包含小写字母"
|
msgstr "必须包含小写字母"
|
||||||
|
|
||||||
#: common/forms.py:200
|
#: common/forms.py:214
|
||||||
msgid ""
|
msgid ""
|
||||||
"After opening, the user password changes and resets must contain lowercase "
|
"After opening, the user password changes and resets must contain lowercase "
|
||||||
"letters"
|
"letters"
|
||||||
msgstr "开启后,用户密码修改、重置必须包含小写字母"
|
msgstr "开启后,用户密码修改、重置必须包含小写字母"
|
||||||
|
|
||||||
#: common/forms.py:206
|
#: common/forms.py:220
|
||||||
msgid "Must contain numeric characters"
|
msgid "Must contain numeric characters"
|
||||||
msgstr "必须包含数字字符"
|
msgstr "必须包含数字字符"
|
||||||
|
|
||||||
#: common/forms.py:207
|
#: common/forms.py:221
|
||||||
msgid ""
|
msgid ""
|
||||||
"After opening, the user password changes and resets must contain numeric "
|
"After opening, the user password changes and resets must contain numeric "
|
||||||
"characters"
|
"characters"
|
||||||
msgstr "开启后,用户密码修改、重置必须包含数字字符"
|
msgstr "开启后,用户密码修改、重置必须包含数字字符"
|
||||||
|
|
||||||
#: common/forms.py:213
|
#: common/forms.py:227
|
||||||
msgid "Must contain special characters"
|
msgid "Must contain special characters"
|
||||||
msgstr "必须包含特殊字符"
|
msgstr "必须包含特殊字符"
|
||||||
|
|
||||||
#: common/forms.py:214
|
#: common/forms.py:228
|
||||||
msgid ""
|
msgid ""
|
||||||
"After opening, the user password changes and resets must contain special "
|
"After opening, the user password changes and resets must contain special "
|
||||||
"characters"
|
"characters"
|
||||||
|
@ -1485,7 +1500,8 @@ msgstr ""
|
||||||
msgid "discard time"
|
msgid "discard time"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: common/models.py:29 users/templates/users/user_detail.html:96
|
#: common/models.py:29 users/models/authentication.py:51
|
||||||
|
#: users/templates/users/user_detail.html:96
|
||||||
msgid "Enabled"
|
msgid "Enabled"
|
||||||
msgstr "启用"
|
msgstr "启用"
|
||||||
|
|
||||||
|
@ -1531,8 +1547,8 @@ msgid "Security setting"
|
||||||
msgstr "安全设置"
|
msgstr "安全设置"
|
||||||
|
|
||||||
#: common/templates/common/security_setting.html:42
|
#: common/templates/common/security_setting.html:42
|
||||||
msgid "MFA setting"
|
msgid "User login settings"
|
||||||
msgstr "MFA 设置"
|
msgstr "用户登录设置"
|
||||||
|
|
||||||
#: common/templates/common/security_setting.html:46
|
#: common/templates/common/security_setting.html:46
|
||||||
msgid "Password check rule"
|
msgid "Password check rule"
|
||||||
|
@ -1803,7 +1819,7 @@ msgid "Versions"
|
||||||
msgstr "版本"
|
msgstr "版本"
|
||||||
|
|
||||||
#: ops/templates/ops/task_list.html:40
|
#: ops/templates/ops/task_list.html:40
|
||||||
#: users/templates/users/login_log_list.html:54
|
#: users/templates/users/login_log_list.html:57
|
||||||
msgid "Date"
|
msgid "Date"
|
||||||
msgstr "日期"
|
msgstr "日期"
|
||||||
|
|
||||||
|
@ -2005,7 +2021,7 @@ msgid "Logout"
|
||||||
msgstr "注销登录"
|
msgstr "注销登录"
|
||||||
|
|
||||||
#: templates/_header_bar.html:49 users/templates/users/login.html:44
|
#: templates/_header_bar.html:49 users/templates/users/login.html:44
|
||||||
#: users/templates/users/login.html:64
|
#: users/templates/users/login.html:68
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "登录"
|
msgstr "登录"
|
||||||
|
|
||||||
|
@ -2045,7 +2061,7 @@ msgstr "关闭"
|
||||||
|
|
||||||
#: templates/_nav.html:10 users/views/group.py:28 users/views/group.py:44
|
#: templates/_nav.html:10 users/views/group.py:28 users/views/group.py:44
|
||||||
#: users/views/group.py:62 users/views/group.py:79 users/views/group.py:95
|
#: users/views/group.py:62 users/views/group.py:79 users/views/group.py:95
|
||||||
#: users/views/login.py:277 users/views/login.py:335 users/views/user.py:65
|
#: users/views/login.py:330 users/views/login.py:388 users/views/user.py:65
|
||||||
#: users/views/user.py:80 users/views/user.py:102 users/views/user.py:175
|
#: users/views/user.py:80 users/views/user.py:102 users/views/user.py:175
|
||||||
#: users/views/user.py:330 users/views/user.py:380 users/views/user.py:415
|
#: users/views/user.py:330 users/views/user.py:380 users/views/user.py:415
|
||||||
msgid "Users"
|
msgid "Users"
|
||||||
|
@ -2161,14 +2177,14 @@ msgstr "线程数"
|
||||||
msgid "Boot Time"
|
msgid "Boot Time"
|
||||||
msgstr "运行时间"
|
msgstr "运行时间"
|
||||||
|
|
||||||
#: terminal/models.py:132 terminal/templates/terminal/session_list.html:102
|
#: terminal/models.py:132 terminal/templates/terminal/session_list.html:104
|
||||||
msgid "Replay"
|
msgid "Replay"
|
||||||
msgstr "回放"
|
msgstr "回放"
|
||||||
|
|
||||||
#: terminal/models.py:133 terminal/templates/terminal/command_list.html:55
|
#: terminal/models.py:133 terminal/templates/terminal/command_list.html:55
|
||||||
#: terminal/templates/terminal/command_list.html:71
|
#: terminal/templates/terminal/command_list.html:71
|
||||||
#: terminal/templates/terminal/session_detail.html:48
|
#: terminal/templates/terminal/session_detail.html:48
|
||||||
#: terminal/templates/terminal/session_list.html:76
|
#: terminal/templates/terminal/session_list.html:77
|
||||||
msgid "Command"
|
msgid "Command"
|
||||||
msgstr "命令"
|
msgstr "命令"
|
||||||
|
|
||||||
|
@ -2219,24 +2235,28 @@ msgstr "监控"
|
||||||
msgid "Terminate session"
|
msgid "Terminate session"
|
||||||
msgstr "终止会话"
|
msgstr "终止会话"
|
||||||
|
|
||||||
#: terminal/templates/terminal/session_list.html:79
|
#: terminal/templates/terminal/session_list.html:76
|
||||||
|
msgid "Login from"
|
||||||
|
msgstr "登录来源"
|
||||||
|
|
||||||
|
#: terminal/templates/terminal/session_list.html:80
|
||||||
msgid "Duration"
|
msgid "Duration"
|
||||||
msgstr "时长"
|
msgstr "时长"
|
||||||
|
|
||||||
#: terminal/templates/terminal/session_list.html:104
|
#: terminal/templates/terminal/session_list.html:106
|
||||||
msgid "Monitor"
|
msgid "Monitor"
|
||||||
msgstr "监控"
|
msgstr "监控"
|
||||||
|
|
||||||
#: terminal/templates/terminal/session_list.html:106
|
|
||||||
#: terminal/templates/terminal/session_list.html:108
|
#: terminal/templates/terminal/session_list.html:108
|
||||||
|
#: terminal/templates/terminal/session_list.html:110
|
||||||
msgid "Terminate"
|
msgid "Terminate"
|
||||||
msgstr "终断"
|
msgstr "终断"
|
||||||
|
|
||||||
#: terminal/templates/terminal/session_list.html:120
|
#: terminal/templates/terminal/session_list.html:122
|
||||||
msgid "Terminate selected"
|
msgid "Terminate selected"
|
||||||
msgstr "终断所选"
|
msgstr "终断所选"
|
||||||
|
|
||||||
#: terminal/templates/terminal/session_list.html:140
|
#: terminal/templates/terminal/session_list.html:142
|
||||||
msgid "Terminate task send, waiting ..."
|
msgid "Terminate task send, waiting ..."
|
||||||
msgstr "终断任务已发送,请等待"
|
msgstr "终断任务已发送,请等待"
|
||||||
|
|
||||||
|
@ -2306,6 +2326,10 @@ msgid ""
|
||||||
"You should use your ssh client tools connect terminal: {} <br /> <br />{}"
|
"You should use your ssh client tools connect terminal: {} <br /> <br />{}"
|
||||||
msgstr "你可以使用ssh客户端工具连接终端"
|
msgstr "你可以使用ssh客户端工具连接终端"
|
||||||
|
|
||||||
|
#: users/api.py:208 users/templates/users/login.html:50
|
||||||
|
msgid "Log in frequently and try again later"
|
||||||
|
msgstr "登录频繁, 稍后重试"
|
||||||
|
|
||||||
#: users/authentication.py:56
|
#: users/authentication.py:56
|
||||||
msgid "Invalid signature header. No credentials provided."
|
msgid "Invalid signature header. No credentials provided."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -2406,8 +2430,9 @@ msgstr ""
|
||||||
msgid "* Enable MFA authentication to make the account more secure."
|
msgid "* Enable MFA authentication to make the account more secure."
|
||||||
msgstr "* 启用MFA认证,使账号更加安全."
|
msgstr "* 启用MFA认证,使账号更加安全."
|
||||||
|
|
||||||
#: users/forms.py:143 users/models/user.py:71
|
#: users/forms.py:143 users/models/authentication.py:75 users/models/user.py:71
|
||||||
#: users/templates/users/first_login.html:45
|
#: users/templates/users/first_login.html:45
|
||||||
|
#: users/templates/users/login_log_list.html:54
|
||||||
msgid "MFA"
|
msgid "MFA"
|
||||||
msgstr "MFA"
|
msgstr "MFA"
|
||||||
|
|
||||||
|
@ -2467,23 +2492,53 @@ msgstr "ssh公钥"
|
||||||
msgid "Private Token"
|
msgid "Private Token"
|
||||||
msgstr "ssh密钥"
|
msgstr "ssh密钥"
|
||||||
|
|
||||||
#: users/models/authentication.py:46
|
#: users/models/authentication.py:50 users/templates/users/user_detail.html:98
|
||||||
|
msgid "Disabled"
|
||||||
|
msgstr "禁用"
|
||||||
|
|
||||||
|
#: users/models/authentication.py:52 users/models/authentication.py:60
|
||||||
|
msgid "-"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: users/models/authentication.py:61
|
||||||
|
msgid "Username/password check failed"
|
||||||
|
msgstr "用户名/密码 校验失败"
|
||||||
|
|
||||||
|
#: users/models/authentication.py:62
|
||||||
|
msgid "MFA authentication failed"
|
||||||
|
msgstr "MFA 认证失败"
|
||||||
|
|
||||||
|
#: users/models/authentication.py:67
|
||||||
|
msgid "Failed"
|
||||||
|
msgstr "失败"
|
||||||
|
|
||||||
|
#: users/models/authentication.py:71
|
||||||
msgid "Login type"
|
msgid "Login type"
|
||||||
msgstr "登录方式"
|
msgstr "登录方式"
|
||||||
|
|
||||||
#: users/models/authentication.py:47
|
#: users/models/authentication.py:72
|
||||||
msgid "Login ip"
|
msgid "Login ip"
|
||||||
msgstr "登录IP"
|
msgstr "登录IP"
|
||||||
|
|
||||||
#: users/models/authentication.py:48
|
#: users/models/authentication.py:73
|
||||||
msgid "Login city"
|
msgid "Login city"
|
||||||
msgstr "登录城市"
|
msgstr "登录城市"
|
||||||
|
|
||||||
#: users/models/authentication.py:49
|
#: users/models/authentication.py:74
|
||||||
msgid "User agent"
|
msgid "User agent"
|
||||||
msgstr "Agent"
|
msgstr "Agent"
|
||||||
|
|
||||||
#: users/models/authentication.py:50
|
#: users/models/authentication.py:76
|
||||||
|
#: users/templates/users/login_log_list.html:55
|
||||||
|
msgid "Reason"
|
||||||
|
msgstr "原因"
|
||||||
|
|
||||||
|
#: users/models/authentication.py:77
|
||||||
|
#: users/templates/users/login_log_list.html:56
|
||||||
|
msgid "Status"
|
||||||
|
msgstr "状态"
|
||||||
|
|
||||||
|
#: users/models/authentication.py:78
|
||||||
msgid "Date login"
|
msgid "Date login"
|
||||||
msgstr "登录日期"
|
msgstr "登录日期"
|
||||||
|
|
||||||
|
@ -2609,7 +2664,7 @@ msgid " for more information"
|
||||||
msgstr "获取更多信息"
|
msgstr "获取更多信息"
|
||||||
|
|
||||||
#: users/templates/users/forgot_password.html:26
|
#: users/templates/users/forgot_password.html:26
|
||||||
#: users/templates/users/login.html:73
|
#: users/templates/users/login.html:77
|
||||||
msgid "Forgot password"
|
msgid "Forgot password"
|
||||||
msgstr "忘记密码"
|
msgstr "忘记密码"
|
||||||
|
|
||||||
|
@ -2617,7 +2672,7 @@ msgstr "忘记密码"
|
||||||
msgid "Input your email, that will send a mail to your"
|
msgid "Input your email, that will send a mail to your"
|
||||||
msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中"
|
msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中"
|
||||||
|
|
||||||
#: users/templates/users/login.html:50
|
#: users/templates/users/login.html:53
|
||||||
msgid "Captcha invalid"
|
msgid "Captcha invalid"
|
||||||
msgstr "验证码错误"
|
msgstr "验证码错误"
|
||||||
|
|
||||||
|
@ -2696,10 +2751,6 @@ msgstr "授权的资产"
|
||||||
msgid "Force enabled"
|
msgid "Force enabled"
|
||||||
msgstr "强制启用"
|
msgstr "强制启用"
|
||||||
|
|
||||||
#: users/templates/users/user_detail.html:98
|
|
||||||
msgid "Disabled"
|
|
||||||
msgstr "禁用"
|
|
||||||
|
|
||||||
#: users/templates/users/user_detail.html:119
|
#: users/templates/users/user_detail.html:119
|
||||||
#: users/templates/users/user_profile.html:108
|
#: users/templates/users/user_profile.html:108
|
||||||
msgid "Last login"
|
msgid "Last login"
|
||||||
|
@ -3001,7 +3052,7 @@ msgstr "禁用或失效"
|
||||||
msgid "Password or SSH public key invalid"
|
msgid "Password or SSH public key invalid"
|
||||||
msgstr "密码或密钥不合法"
|
msgstr "密码或密钥不合法"
|
||||||
|
|
||||||
#: users/utils.py:290 users/utils.py:300
|
#: users/utils.py:289 users/utils.py:299
|
||||||
msgid "Bit"
|
msgid "Bit"
|
||||||
msgstr " 位"
|
msgstr " 位"
|
||||||
|
|
||||||
|
@ -3017,60 +3068,60 @@ msgstr "更新用户组"
|
||||||
msgid "User group granted asset"
|
msgid "User group granted asset"
|
||||||
msgstr "用户组授权资产"
|
msgstr "用户组授权资产"
|
||||||
|
|
||||||
#: users/views/login.py:62
|
#: users/views/login.py:75
|
||||||
msgid "Please enable cookies and try again."
|
msgid "Please enable cookies and try again."
|
||||||
msgstr "设置你的浏览器支持cookie"
|
msgstr "设置你的浏览器支持cookie"
|
||||||
|
|
||||||
#: users/views/login.py:128 users/views/user.py:500 users/views/user.py:525
|
#: users/views/login.py:178 users/views/user.py:500 users/views/user.py:525
|
||||||
msgid "MFA code invalid"
|
msgid "MFA code invalid"
|
||||||
msgstr "MFA码认证失败"
|
msgstr "MFA码认证失败"
|
||||||
|
|
||||||
#: users/views/login.py:154
|
#: users/views/login.py:207
|
||||||
msgid "Logout success"
|
msgid "Logout success"
|
||||||
msgstr "退出登录成功"
|
msgstr "退出登录成功"
|
||||||
|
|
||||||
#: users/views/login.py:155
|
#: users/views/login.py:208
|
||||||
msgid "Logout success, return login page"
|
msgid "Logout success, return login page"
|
||||||
msgstr "退出登录成功,返回到登录页面"
|
msgstr "退出登录成功,返回到登录页面"
|
||||||
|
|
||||||
#: users/views/login.py:171
|
#: users/views/login.py:224
|
||||||
msgid "Email address invalid, please input again"
|
msgid "Email address invalid, please input again"
|
||||||
msgstr "邮箱地址错误,重新输入"
|
msgstr "邮箱地址错误,重新输入"
|
||||||
|
|
||||||
#: users/views/login.py:184
|
#: users/views/login.py:237
|
||||||
msgid "Send reset password message"
|
msgid "Send reset password message"
|
||||||
msgstr "发送重置密码邮件"
|
msgstr "发送重置密码邮件"
|
||||||
|
|
||||||
#: users/views/login.py:185
|
#: users/views/login.py:238
|
||||||
msgid "Send reset password mail success, login your mail box and follow it "
|
msgid "Send reset password mail success, login your mail box and follow it "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)"
|
"发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)"
|
||||||
|
|
||||||
#: users/views/login.py:198
|
#: users/views/login.py:251
|
||||||
msgid "Reset password success"
|
msgid "Reset password success"
|
||||||
msgstr "重置密码成功"
|
msgstr "重置密码成功"
|
||||||
|
|
||||||
#: users/views/login.py:199
|
#: users/views/login.py:252
|
||||||
msgid "Reset password success, return to login page"
|
msgid "Reset password success, return to login page"
|
||||||
msgstr "重置密码成功,返回到登录页面"
|
msgstr "重置密码成功,返回到登录页面"
|
||||||
|
|
||||||
#: users/views/login.py:220 users/views/login.py:233
|
#: users/views/login.py:273 users/views/login.py:286
|
||||||
msgid "Token invalid or expired"
|
msgid "Token invalid or expired"
|
||||||
msgstr "Token错误或失效"
|
msgstr "Token错误或失效"
|
||||||
|
|
||||||
#: users/views/login.py:229
|
#: users/views/login.py:282
|
||||||
msgid "Password not same"
|
msgid "Password not same"
|
||||||
msgstr "密码不一致"
|
msgstr "密码不一致"
|
||||||
|
|
||||||
#: users/views/login.py:239 users/views/user.py:118 users/views/user.py:398
|
#: users/views/login.py:292 users/views/user.py:118 users/views/user.py:398
|
||||||
msgid "* Your password does not meet the requirements"
|
msgid "* Your password does not meet the requirements"
|
||||||
msgstr "* 您的密码不符合要求"
|
msgstr "* 您的密码不符合要求"
|
||||||
|
|
||||||
#: users/views/login.py:277
|
#: users/views/login.py:330
|
||||||
msgid "First login"
|
msgid "First login"
|
||||||
msgstr "首次登陆"
|
msgstr "首次登陆"
|
||||||
|
|
||||||
#: users/views/login.py:336
|
#: users/views/login.py:389
|
||||||
msgid "Login log list"
|
msgid "Login log list"
|
||||||
msgstr "登录日志"
|
msgstr "登录日志"
|
||||||
|
|
||||||
|
@ -3117,3 +3168,6 @@ msgstr "MFA 解绑成功"
|
||||||
#: users/views/user.py:555
|
#: users/views/user.py:555
|
||||||
msgid "MFA disable success, return login page"
|
msgid "MFA disable success, return login page"
|
||||||
msgstr "MFA 解绑成功,返回登录页面"
|
msgstr "MFA 解绑成功,返回登录页面"
|
||||||
|
|
||||||
|
#~ msgid "MFA setting"
|
||||||
|
#~ msgstr "MFA 设置"
|
||||||
|
|
|
@ -405,6 +405,8 @@ TERMINAL_REPLAY_STORAGE = {
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_PASSWORD_MIN_LENGTH = 6
|
DEFAULT_PASSWORD_MIN_LENGTH = 6
|
||||||
|
DEFAULT_LOGIN_LIMIT_COUNT = 3
|
||||||
|
DEFAULT_LOGIN_LIMIT_TIME = 30
|
||||||
|
|
||||||
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
|
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
|
||||||
BOOTSTRAP3 = {
|
BOOTSTRAP3 = {
|
||||||
|
|
|
@ -73,6 +73,7 @@
|
||||||
<th class="text-center">{% trans 'System user' %}</th>
|
<th class="text-center">{% trans 'System user' %}</th>
|
||||||
<th class="text-center">{% trans 'Remote addr' %}</th>
|
<th class="text-center">{% trans 'Remote addr' %}</th>
|
||||||
<th class="text-center">{% trans 'Protocol' %}</th>
|
<th class="text-center">{% trans 'Protocol' %}</th>
|
||||||
|
<th class="text-center">{% trans 'Login from' %}</th>
|
||||||
<th class="text-center">{% trans 'Command' %}</th>
|
<th class="text-center">{% trans 'Command' %}</th>
|
||||||
<th class="text-center">{% trans 'Date start' %}</th>
|
<th class="text-center">{% trans 'Date start' %}</th>
|
||||||
{# <th class="text-center">{% trans 'Date last active' %}</th>#}
|
{# <th class="text-center">{% trans 'Date last active' %}</th>#}
|
||||||
|
@ -92,6 +93,7 @@
|
||||||
<td class="text-center">{{ session.system_user }}</td>
|
<td class="text-center">{{ session.system_user }}</td>
|
||||||
<td class="text-center">{{ session.remote_addr|default:"" }}</td>
|
<td class="text-center">{{ session.remote_addr|default:"" }}</td>
|
||||||
<td class="text-center">{{ session.protocol }}</td>
|
<td class="text-center">{{ session.protocol }}</td>
|
||||||
|
<td class="text-center">{{ session.get_login_from_display }}</td>
|
||||||
<td class="text-center">{{ session.id | get_session_command_amount }}</td>
|
<td class="text-center">{{ session.id | get_session_command_amount }}</td>
|
||||||
|
|
||||||
<td class="text-center">{{ session.date_start }}</td>
|
<td class="text-center">{{ session.date_start }}</td>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import uuid
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
@ -14,10 +15,11 @@ from .serializers import UserSerializer, UserGroupSerializer, \
|
||||||
UserGroupUpdateMemeberSerializer, UserPKUpdateSerializer, \
|
UserGroupUpdateMemeberSerializer, UserPKUpdateSerializer, \
|
||||||
UserUpdateGroupSerializer, ChangeUserPasswordSerializer
|
UserUpdateGroupSerializer, ChangeUserPasswordSerializer
|
||||||
from .tasks import write_login_log_async
|
from .tasks import write_login_log_async
|
||||||
from .models import User, UserGroup
|
from .models import User, UserGroup, LoginLog
|
||||||
from .permissions import IsSuperUser, IsValidUser, IsCurrentUserOrReadOnly, \
|
from .permissions import IsSuperUser, IsValidUser, IsCurrentUserOrReadOnly, \
|
||||||
IsSuperUserOrAppUser
|
IsSuperUserOrAppUser
|
||||||
from .utils import check_user_valid, generate_token, get_login_ip, check_otp_code
|
from .utils import check_user_valid, generate_token, get_login_ip, \
|
||||||
|
check_otp_code, set_user_login_failed_count_to_cache, is_block_login
|
||||||
from common.mixins import IDInFilterMixin
|
from common.mixins import IDInFilterMixin
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
|
|
||||||
|
@ -149,10 +151,23 @@ class UserOtpAuthApi(APIView):
|
||||||
return Response({'msg': '请先进行用户名和密码验证'}, status=401)
|
return Response({'msg': '请先进行用户名和密码验证'}, status=401)
|
||||||
|
|
||||||
if not check_otp_code(user.otp_secret_key, otp_code):
|
if not check_otp_code(user.otp_secret_key, otp_code):
|
||||||
|
data = {
|
||||||
|
'username': user.username,
|
||||||
|
'mfa': int(user.otp_enabled),
|
||||||
|
'reason': LoginLog.REASON_MFA,
|
||||||
|
'status': False
|
||||||
|
}
|
||||||
|
self.write_login_log(request, data)
|
||||||
return Response({'msg': 'MFA认证失败'}, status=401)
|
return Response({'msg': 'MFA认证失败'}, status=401)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'username': user.username,
|
||||||
|
'mfa': int(user.otp_enabled),
|
||||||
|
'reason': LoginLog.REASON_NOTHING,
|
||||||
|
'status': True
|
||||||
|
}
|
||||||
|
self.write_login_log(request, data)
|
||||||
token = generate_token(request, user)
|
token = generate_token(request, user)
|
||||||
self.write_login_log(request, user)
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
'token': token,
|
'token': token,
|
||||||
|
@ -161,7 +176,7 @@ class UserOtpAuthApi(APIView):
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def write_login_log(request, user):
|
def write_login_log(request, data):
|
||||||
login_ip = request.data.get('remote_addr', None)
|
login_ip = request.data.get('remote_addr', None)
|
||||||
login_type = request.data.get('login_type', '')
|
login_type = request.data.get('login_type', '')
|
||||||
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
||||||
|
@ -169,25 +184,52 @@ class UserOtpAuthApi(APIView):
|
||||||
if not login_ip:
|
if not login_ip:
|
||||||
login_ip = get_login_ip(request)
|
login_ip = get_login_ip(request)
|
||||||
|
|
||||||
write_login_log_async.delay(
|
tmp_data = {
|
||||||
user.username, ip=login_ip,
|
'ip': login_ip,
|
||||||
type=login_type, user_agent=user_agent,
|
'type': login_type,
|
||||||
)
|
'user_agent': user_agent
|
||||||
|
}
|
||||||
|
data.update(tmp_data)
|
||||||
|
write_login_log_async.delay(**data)
|
||||||
|
|
||||||
|
|
||||||
class UserAuthApi(APIView):
|
class UserAuthApi(APIView):
|
||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
|
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
user, msg = self.check_user_valid(request)
|
# limit login
|
||||||
|
username = request.data.get('username')
|
||||||
|
ip = request.data.get('remote_addr', None)
|
||||||
|
ip = ip if ip else get_login_ip(request)
|
||||||
|
key_limit = self.key_prefix_limit.format(ip, username)
|
||||||
|
if is_block_login(key_limit):
|
||||||
|
msg = _("Log in frequently and try again later")
|
||||||
|
return Response({'msg': msg}, status=401)
|
||||||
|
|
||||||
|
user, msg = self.check_user_valid(request)
|
||||||
if not user:
|
if not user:
|
||||||
|
data = {
|
||||||
|
'username': request.data.get('username', ''),
|
||||||
|
'mfa': LoginLog.MFA_UNKNOWN,
|
||||||
|
'reason': LoginLog.REASON_PASSWORD,
|
||||||
|
'status': False
|
||||||
|
}
|
||||||
|
self.write_login_log(request, data)
|
||||||
|
|
||||||
|
set_user_login_failed_count_to_cache(key_limit)
|
||||||
return Response({'msg': msg}, status=401)
|
return Response({'msg': msg}, status=401)
|
||||||
|
|
||||||
if not user.otp_enabled:
|
if not user.otp_enabled:
|
||||||
|
data = {
|
||||||
|
'username': user.username,
|
||||||
|
'mfa': int(user.otp_enabled),
|
||||||
|
'reason': LoginLog.REASON_NOTHING,
|
||||||
|
'status': True
|
||||||
|
}
|
||||||
|
self.write_login_log(request, data)
|
||||||
token = generate_token(request, user)
|
token = generate_token(request, user)
|
||||||
self.write_login_log(request, user)
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
'token': token,
|
'token': token,
|
||||||
|
@ -204,7 +246,8 @@ class UserAuthApi(APIView):
|
||||||
'otp_url': reverse('api-users:user-otp-auth'),
|
'otp_url': reverse('api-users:user-otp-auth'),
|
||||||
'seed': seed,
|
'seed': seed,
|
||||||
'user': self.serializer_class(user).data
|
'user': self.serializer_class(user).data
|
||||||
}, status=300)
|
}, status=300
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_user_valid(request):
|
def check_user_valid(request):
|
||||||
|
@ -218,7 +261,7 @@ class UserAuthApi(APIView):
|
||||||
return user, msg
|
return user, msg
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def write_login_log(request, user):
|
def write_login_log(request, data):
|
||||||
login_ip = request.data.get('remote_addr', None)
|
login_ip = request.data.get('remote_addr', None)
|
||||||
login_type = request.data.get('login_type', '')
|
login_type = request.data.get('login_type', '')
|
||||||
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
||||||
|
@ -226,10 +269,14 @@ class UserAuthApi(APIView):
|
||||||
if not login_ip:
|
if not login_ip:
|
||||||
login_ip = get_login_ip(request)
|
login_ip = get_login_ip(request)
|
||||||
|
|
||||||
write_login_log_async.delay(
|
tmp_data = {
|
||||||
user.username, ip=login_ip,
|
'ip': login_ip,
|
||||||
type=login_type, user_agent=user_agent,
|
'type': login_type,
|
||||||
)
|
'user_agent': user_agent,
|
||||||
|
}
|
||||||
|
data.update(tmp_data)
|
||||||
|
|
||||||
|
write_login_log_async.delay(**data)
|
||||||
|
|
||||||
|
|
||||||
class UserConnectionTokenApi(APIView):
|
class UserConnectionTokenApi(APIView):
|
||||||
|
|
|
@ -41,12 +41,40 @@ class LoginLog(models.Model):
|
||||||
('W', 'Web'),
|
('W', 'Web'),
|
||||||
('T', 'Terminal'),
|
('T', 'Terminal'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MFA_DISABLED = 0
|
||||||
|
MFA_ENABLED = 1
|
||||||
|
MFA_UNKNOWN = 2
|
||||||
|
|
||||||
|
MFA_CHOICE = (
|
||||||
|
(MFA_DISABLED, _('Disabled')),
|
||||||
|
(MFA_ENABLED, _('Enabled')),
|
||||||
|
(MFA_UNKNOWN, _('-')),
|
||||||
|
)
|
||||||
|
|
||||||
|
REASON_NOTHING = 0
|
||||||
|
REASON_PASSWORD = 1
|
||||||
|
REASON_MFA = 2
|
||||||
|
|
||||||
|
REASON_CHOICE = (
|
||||||
|
(REASON_NOTHING, _('-')),
|
||||||
|
(REASON_PASSWORD, _('Username/password check failed')),
|
||||||
|
(REASON_MFA, _('MFA authentication failed')),
|
||||||
|
)
|
||||||
|
|
||||||
|
STATUS_CHOICE = (
|
||||||
|
(True, _('Success')),
|
||||||
|
(False, _('Failed'))
|
||||||
|
)
|
||||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
username = models.CharField(max_length=20, verbose_name=_('Username'))
|
username = models.CharField(max_length=20, verbose_name=_('Username'))
|
||||||
type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type'))
|
type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type'))
|
||||||
ip = models.GenericIPAddressField(verbose_name=_('Login ip'))
|
ip = models.GenericIPAddressField(verbose_name=_('Login ip'))
|
||||||
city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city'))
|
city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city'))
|
||||||
user_agent = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('User agent'))
|
user_agent = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('User agent'))
|
||||||
|
mfa = models.SmallIntegerField(default=MFA_UNKNOWN, choices=MFA_CHOICE, verbose_name=_('MFA'))
|
||||||
|
reason = models.SmallIntegerField(default=REASON_NOTHING, choices=REASON_CHOICE, verbose_name=_('Reason'))
|
||||||
|
status = models.BooleanField(max_length=2, default=True, choices=STATUS_CHOICE, verbose_name=_('Status'))
|
||||||
datetime = models.DateTimeField(auto_now_add=True, verbose_name=_('Date login'))
|
datetime = models.DateTimeField(auto_now_add=True, verbose_name=_('Date login'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -45,13 +45,17 @@
|
||||||
</div>
|
</div>
|
||||||
<form class="m-t" role="form" method="post" action="">
|
<form class="m-t" role="form" method="post" action="">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if form.errors %}
|
|
||||||
|
{% if block_login %}
|
||||||
|
<p class="red-fonts">{% trans 'Log in frequently and try again later' %}</p>
|
||||||
|
{% elif form.errors %}
|
||||||
{% if 'captcha' in form.errors %}
|
{% if 'captcha' in form.errors %}
|
||||||
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
|
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
|
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text" class="form-control" name="{{ form.username.html_name }}" placeholder="{% trans 'Username' %}" required="" value="{% if form.username.value %}{{ form.username.value }}{% endif %}">
|
<input type="text" class="form-control" name="{{ form.username.html_name }}" placeholder="{% trans 'Username' %}" required="" value="{% if form.username.value %}{{ form.username.value }}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -51,6 +51,9 @@
|
||||||
<th class="text-center">{% trans 'UA' %}</th>
|
<th class="text-center">{% trans 'UA' %}</th>
|
||||||
<th class="text-center">{% trans 'IP' %}</th>
|
<th class="text-center">{% trans 'IP' %}</th>
|
||||||
<th class="text-center">{% trans 'City' %}</th>
|
<th class="text-center">{% trans 'City' %}</th>
|
||||||
|
<th class="text-center">{% trans 'MFA' %}</th>
|
||||||
|
<th class="text-center">{% trans 'Reason' %}</th>
|
||||||
|
<th class="text-center">{% trans 'Status' %}</th>
|
||||||
<th class="text-center">{% trans 'Date' %}</th>
|
<th class="text-center">{% trans 'Date' %}</th>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -65,6 +68,9 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">{{ login_log.ip }}</td>
|
<td class="text-center">{{ login_log.ip }}</td>
|
||||||
<td class="text-center">{{ login_log.city }}</td>
|
<td class="text-center">{{ login_log.city }}</td>
|
||||||
|
<td class="text-center">{{ login_log.get_mfa_display }}</td>
|
||||||
|
<td class="text-center">{{ login_log.get_reason_display }}</td>
|
||||||
|
<td class="text-center">{{ login_log.get_status_display }}</td>
|
||||||
<td class="text-center">{{ login_log.datetime }}</td>
|
<td class="text-center">{{ login_log.datetime }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import ipaddress
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
from django.contrib.auth import authenticate, login as auth_login
|
from django.contrib.auth import authenticate
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
@ -200,16 +200,15 @@ def get_login_ip(request):
|
||||||
return login_ip
|
return login_ip
|
||||||
|
|
||||||
|
|
||||||
def write_login_log(username, type='', ip='', user_agent=''):
|
def write_login_log(*args, **kwargs):
|
||||||
|
ip = kwargs.get('ip', '')
|
||||||
if not (ip and validate_ip(ip)):
|
if not (ip and validate_ip(ip)):
|
||||||
ip = ip[:15]
|
ip = ip[:15]
|
||||||
city = "Unknown"
|
city = "Unknown"
|
||||||
else:
|
else:
|
||||||
city = get_ip_city(ip)
|
city = get_ip_city(ip)
|
||||||
LoginLog.objects.create(
|
kwargs.update({'ip': ip, 'city': city})
|
||||||
username=username, type=type,
|
LoginLog.objects.create(**kwargs)
|
||||||
ip=ip, city=city, user_agent=user_agent
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_ip_city(ip, timeout=10):
|
def get_ip_city(ip, timeout=10):
|
||||||
|
@ -332,3 +331,29 @@ def check_password_rules(password):
|
||||||
|
|
||||||
match_obj = re.match(pattern, password)
|
match_obj = re.match(pattern, password)
|
||||||
return bool(match_obj)
|
return bool(match_obj)
|
||||||
|
|
||||||
|
|
||||||
|
def set_user_login_failed_count_to_cache(key_limit):
|
||||||
|
count = cache.get(key_limit)
|
||||||
|
count = count + 1 if count else 1
|
||||||
|
|
||||||
|
setting_limit_time = Setting.objects.filter(
|
||||||
|
name='SECURITY_LOGIN_LIMIT_TIME'
|
||||||
|
).first()
|
||||||
|
limit_time = setting_limit_time.cleaned_value if setting_limit_time \
|
||||||
|
else settings.DEFAULT_LOGIN_LIMIT_TIME
|
||||||
|
|
||||||
|
cache.set(key_limit, count, int(limit_time)*60)
|
||||||
|
|
||||||
|
|
||||||
|
def is_block_login(key_limit):
|
||||||
|
count = cache.get(key_limit)
|
||||||
|
|
||||||
|
setting_limit_count = Setting.objects.filter(
|
||||||
|
name='SECURITY_LOGIN_LIMIT_COUNT'
|
||||||
|
).first()
|
||||||
|
limit_count = setting_limit_count.cleaned_value if setting_limit_count \
|
||||||
|
else settings.DEFAULT_LOGIN_LIMIT_COUNT
|
||||||
|
|
||||||
|
if count and count >= limit_count:
|
||||||
|
return True
|
||||||
|
|
|
@ -25,8 +25,10 @@ from common.utils import get_object_or_none
|
||||||
from common.mixins import DatetimeSearchMixin, AdminUserRequiredMixin
|
from common.mixins import DatetimeSearchMixin, AdminUserRequiredMixin
|
||||||
from common.models import Setting
|
from common.models import Setting
|
||||||
from ..models import User, LoginLog
|
from ..models import User, LoginLog
|
||||||
from ..utils import send_reset_password_mail, check_otp_code, get_login_ip, redirect_user_first_login_or_index, \
|
from ..utils import send_reset_password_mail, check_otp_code, get_login_ip, \
|
||||||
get_user_or_tmp_user, set_tmp_user_to_cache, get_password_check_rules, check_password_rules
|
redirect_user_first_login_or_index, get_user_or_tmp_user, \
|
||||||
|
set_tmp_user_to_cache, get_password_check_rules, check_password_rules, \
|
||||||
|
is_block_login, set_user_login_failed_count_to_cache
|
||||||
from ..tasks import write_login_log_async
|
from ..tasks import write_login_log_async
|
||||||
from .. import forms
|
from .. import forms
|
||||||
|
|
||||||
|
@ -47,7 +49,8 @@ class UserLoginView(FormView):
|
||||||
form_class = forms.UserLoginForm
|
form_class = forms.UserLoginForm
|
||||||
form_class_captcha = forms.UserLoginCaptchaForm
|
form_class_captcha = forms.UserLoginCaptchaForm
|
||||||
redirect_field_name = 'next'
|
redirect_field_name = 'next'
|
||||||
key_prefix = "_LOGIN_INVALID_{}"
|
key_prefix_captcha = "_LOGIN_INVALID_{}"
|
||||||
|
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if request.user.is_staff:
|
if request.user.is_staff:
|
||||||
|
@ -57,6 +60,16 @@ class UserLoginView(FormView):
|
||||||
request.session.set_test_cookie()
|
request.session.set_test_cookie()
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
# limit login authentication
|
||||||
|
ip = get_login_ip(request)
|
||||||
|
username = self.request.POST.get('username')
|
||||||
|
key_limit = self.key_prefix_limit.format(ip, username)
|
||||||
|
if is_block_login(key_limit):
|
||||||
|
return self.render_to_response(self.get_context_data(block_login=True))
|
||||||
|
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
if not self.request.session.test_cookie_worked():
|
if not self.request.session.test_cookie_worked():
|
||||||
return HttpResponse(_("Please enable cookies and try again."))
|
return HttpResponse(_("Please enable cookies and try again."))
|
||||||
|
@ -65,8 +78,23 @@ class UserLoginView(FormView):
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
def form_invalid(self, form):
|
def form_invalid(self, form):
|
||||||
|
# write login failed log
|
||||||
|
username = form.cleaned_data.get('username')
|
||||||
|
data = {
|
||||||
|
'username': username,
|
||||||
|
'mfa': LoginLog.MFA_UNKNOWN,
|
||||||
|
'reason': LoginLog.REASON_PASSWORD,
|
||||||
|
'status': False
|
||||||
|
}
|
||||||
|
self.write_login_log(data)
|
||||||
|
|
||||||
|
# limit user login failed count
|
||||||
ip = get_login_ip(self.request)
|
ip = get_login_ip(self.request)
|
||||||
cache.set(self.key_prefix.format(ip), 1, 3600)
|
key_limit = self.key_prefix_limit.format(ip, username)
|
||||||
|
set_user_login_failed_count_to_cache(key_limit)
|
||||||
|
|
||||||
|
# show captcha
|
||||||
|
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
|
||||||
old_form = form
|
old_form = form
|
||||||
form = self.form_class_captcha(data=form.data)
|
form = self.form_class_captcha(data=form.data)
|
||||||
form._errors = old_form.errors
|
form._errors = old_form.errors
|
||||||
|
@ -74,7 +102,7 @@ class UserLoginView(FormView):
|
||||||
|
|
||||||
def get_form_class(self):
|
def get_form_class(self):
|
||||||
ip = get_login_ip(self.request)
|
ip = get_login_ip(self.request)
|
||||||
if cache.get(self.key_prefix.format(ip)):
|
if cache.get(self.key_prefix_captcha.format(ip)):
|
||||||
return self.form_class_captcha
|
return self.form_class_captcha
|
||||||
else:
|
else:
|
||||||
return self.form_class
|
return self.form_class
|
||||||
|
@ -91,7 +119,13 @@ class UserLoginView(FormView):
|
||||||
elif not user.otp_enabled:
|
elif not user.otp_enabled:
|
||||||
# 0 & T,F
|
# 0 & T,F
|
||||||
auth_login(self.request, user)
|
auth_login(self.request, user)
|
||||||
self.write_login_log()
|
data = {
|
||||||
|
'username': self.request.user.username,
|
||||||
|
'mfa': int(self.request.user.otp_enabled),
|
||||||
|
'reason': LoginLog.REASON_NOTHING,
|
||||||
|
'status': True
|
||||||
|
}
|
||||||
|
self.write_login_log(data)
|
||||||
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
|
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
@ -101,13 +135,16 @@ class UserLoginView(FormView):
|
||||||
kwargs.update(context)
|
kwargs.update(context)
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
def write_login_log(self):
|
def write_login_log(self, data):
|
||||||
login_ip = get_login_ip(self.request)
|
login_ip = get_login_ip(self.request)
|
||||||
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
|
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
|
||||||
write_login_log_async.delay(
|
tmp_data = {
|
||||||
self.request.user.username, type='W',
|
'ip': login_ip,
|
||||||
ip=login_ip, user_agent=user_agent
|
'type': 'W',
|
||||||
)
|
'user_agent': user_agent
|
||||||
|
}
|
||||||
|
data.update(tmp_data)
|
||||||
|
write_login_log_async.delay(**data)
|
||||||
|
|
||||||
|
|
||||||
class UserLoginOtpView(FormView):
|
class UserLoginOtpView(FormView):
|
||||||
|
@ -122,22 +159,38 @@ class UserLoginOtpView(FormView):
|
||||||
|
|
||||||
if check_otp_code(otp_secret_key, otp_code):
|
if check_otp_code(otp_secret_key, otp_code):
|
||||||
auth_login(self.request, user)
|
auth_login(self.request, user)
|
||||||
self.write_login_log()
|
data = {
|
||||||
|
'username': self.request.user.username,
|
||||||
|
'mfa': int(self.request.user.otp_enabled),
|
||||||
|
'reason': LoginLog.REASON_NOTHING,
|
||||||
|
'status': True
|
||||||
|
}
|
||||||
|
self.write_login_log(data)
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
else:
|
else:
|
||||||
|
data = {
|
||||||
|
'username': user.username,
|
||||||
|
'mfa': int(user.otp_enabled),
|
||||||
|
'reason': LoginLog.REASON_MFA,
|
||||||
|
'status': False
|
||||||
|
}
|
||||||
|
self.write_login_log(data)
|
||||||
form.add_error('otp_code', _('MFA code invalid'))
|
form.add_error('otp_code', _('MFA code invalid'))
|
||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
|
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
|
||||||
|
|
||||||
def write_login_log(self):
|
def write_login_log(self, data):
|
||||||
login_ip = get_login_ip(self.request)
|
login_ip = get_login_ip(self.request)
|
||||||
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
|
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
|
||||||
write_login_log_async.delay(
|
tmp_data = {
|
||||||
self.request.user.username, type='W',
|
'ip': login_ip,
|
||||||
ip=login_ip, user_agent=user_agent
|
'type': 'W',
|
||||||
)
|
'user_agent': user_agent
|
||||||
|
}
|
||||||
|
data.update(tmp_data)
|
||||||
|
write_login_log_async.delay(**data)
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(never_cache, name='dispatch')
|
@method_decorator(never_cache, name='dispatch')
|
||||||
|
|
Loading…
Reference in New Issue