Merge pull request #1503 from jumpserver/update_loginlog

[Update] 记录用户登录失败日志,限制用户登录失败次数
pull/1508/head
老广 2018-07-10 02:31:39 -05:00 committed by GitHub
commit df95c93bb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 348 additions and 113 deletions

View File

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

View File

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

View File

@ -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 设置"

View File

@ -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 = {

View File

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

View File

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

View File

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

View File

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

View File

@ -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 %}

View File

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

View File

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