Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
J
jumpserver
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ops
jumpserver
Commits
df95c93b
Unverified
Commit
df95c93b
authored
Jul 10, 2018
by
老广
Committed by
GitHub
Jul 10, 2018
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1503 from jumpserver/update_loginlog
[Update] 记录用户登录失败日志,限制用户登录失败次数
parents
75a9deeb
435acafc
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
348 additions
and
113 deletions
+348
-113
forms.py
apps/common/forms.py
+20
-6
security_setting.html
apps/common/templates/common/security_setting.html
+2
-2
django.mo
apps/i18n/zh/LC_MESSAGES/django.mo
+0
-0
django.po
apps/i18n/zh/LC_MESSAGES/django.po
+119
-65
settings.py
apps/jumpserver/settings.py
+2
-0
session_list.html
apps/terminal/templates/terminal/session_list.html
+2
-0
api.py
apps/users/api.py
+63
-16
authentication.py
apps/users/models/authentication.py
+28
-0
login.html
apps/users/templates/users/login.html
+5
-1
login_log_list.html
apps/users/templates/users/login_log_list.html
+6
-0
utils.py
apps/users/utils.py
+31
-6
login.py
apps/users/views/login.py
+70
-17
No files found.
apps/common/forms.py
View file @
df95c93b
...
...
@@ -170,7 +170,7 @@ class TerminalSettingForm(BaseForm):
class
SecuritySettingForm
(
BaseForm
):
# MFA
全局设置
# MFA
global setting
SECURITY_MFA_AUTH
=
forms
.
BooleanField
(
initial
=
False
,
required
=
False
,
label
=
_
(
"MFA Secondary certification"
),
...
...
@@ -179,12 +179,26 @@ class SecuritySettingForm(BaseForm):
'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
(
initial
=
6
,
label
=
_
(
"Password minimum length"
),
min_value
=
6
)
#
大写字母
#
upper case
SECURITY_PASSWORD_UPPER_CASE
=
forms
.
BooleanField
(
initial
=
False
,
required
=
False
,
...
...
@@ -193,21 +207,21 @@ class SecuritySettingForm(BaseForm):
'After opening, the user password changes '
'and resets must contain uppercase letters'
)
)
#
小写字母
#
lower case
SECURITY_PASSWORD_LOWER_CASE
=
forms
.
BooleanField
(
initial
=
False
,
required
=
False
,
label
=
_
(
"Must contain lowercase letters"
),
help_text
=
_
(
'After opening, the user password changes '
'and resets must contain lowercase letters'
)
)
#
数字
#
number
SECURITY_PASSWORD_NUMBER
=
forms
.
BooleanField
(
initial
=
False
,
required
=
False
,
label
=
_
(
"Must contain numeric characters"
),
help_text
=
_
(
'After opening, the user password changes '
'and resets must contain numeric characters'
)
)
#
特殊字符
#
special char
SECURITY_PASSWORD_SPECIAL_CHAR
=
forms
.
BooleanField
(
initial
=
False
,
required
=
False
,
label
=
_
(
"Must contain special characters"
),
...
...
apps/common/templates/common/security_setting.html
View file @
df95c93b
...
...
@@ -39,9 +39,9 @@
{% endif %}
{% csrf_token %}
<h3>
{% trans "
MFA setting
" %}
</h3>
<h3>
{% trans "
User login settings
" %}
</h3>
{% for field in form %}
{% if forloop.counter ==
2
%}
{% if forloop.counter ==
4
%}
<div
class=
"hr-line-dashed"
></div>
<h3>
{% trans "Password check rule" %}
</h3>
{% endif %}
...
...
apps/i18n/zh/LC_MESSAGES/django.mo
View file @
df95c93b
No preview for this file type
apps/i18n/zh/LC_MESSAGES/django.po
View file @
df95c93b
...
...
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Jumpserver 0.3.3\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-0
6-25 12:19
+0800\n"
"POT-Creation-Date: 2018-0
7-06 13:11
+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: Jumpserver team<ibuler@qq.com>\n"
...
...
@@ -118,7 +118,7 @@ msgstr "端口"
msgid "Asset"
msgstr "资产"
#: assets/forms/domain.py:54 assets/forms/user.py:79 assets/forms/user.py:13
8
#: assets/forms/domain.py:54 assets/forms/user.py:79 assets/forms/user.py:13
9
#: assets/models/base.py:21 assets/models/cluster.py:18
#: assets/models/domain.py:17 assets/models/group.py:20
#: assets/models/label.py:17 assets/templates/assets/admin_user_detail.html:56
...
...
@@ -147,16 +147,16 @@ msgstr "资产"
msgid "Name"
msgstr "名称"
#: assets/forms/domain.py:55 assets/forms/user.py:80 assets/forms/user.py:1
39
#: assets/forms/domain.py:55 assets/forms/user.py:80 assets/forms/user.py:1
40
#: assets/models/base.py:22 assets/templates/assets/admin_user_detail.html:60
#: assets/templates/assets/admin_user_list.html:24
#: assets/templates/assets/domain_gateway_list.html:60
#: assets/templates/assets/system_user_detail.html:62
#: assets/templates/assets/system_user_list.html:27
#: 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/login.html:
56
#: users/templates/users/login.html:
60
#: users/templates/users/login_log_list.html:49
#: users/templates/users/user_detail.html:67
#: 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
#: 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/user_create.html:10
#: users/templates/users/user_password_authentication.html:14
...
...
@@ -192,21 +192,21 @@ msgstr "ssh密钥不合法"
msgid "Password and private key file must be input one"
msgstr "密码和私钥, 必须输入一个"
#: assets/forms/user.py:12
4
#: assets/forms/user.py:12
5
msgid "* Automatic login mode, must fill in the username."
msgstr "自动登录模式,必须填写用户名"
#: assets/forms/user.py:14
4
#: assets/forms/user.py:14
5
msgid "Auto push system user to asset"
msgstr "自动推送系统用户到资产"
#: assets/forms/user.py:14
5
#: assets/forms/user.py:14
6
msgid ""
"High level will be using login asset as default, if user was granted more "
"than 2 system user"
msgstr "高优先级的系统用户将会作为默认登录用户"
#: assets/forms/user.py:14
7
#: assets/forms/user.py:14
8
msgid ""
"If you choose manual login mode, you do not need to fill in the username and "
"password."
...
...
@@ -480,7 +480,7 @@ msgstr "手动登录"
#: assets/views/asset.py:197 assets/views/domain.py:29
#: assets/views/domain.py:45 assets/views/domain.py:61
#: assets/views/domain.py:74 assets/views/domain.py:98
#: assets/views/domain.py:126 assets/views/domain.py:1
50
#: assets/views/domain.py:126 assets/views/domain.py:1
45
#: 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:60 assets/views/system_user.py:74
...
...
@@ -685,7 +685,7 @@ msgstr "重置"
#: common/templates/common/security_setting.html:71
#: common/templates/common/terminal_setting.html:108
#: perms/templates/perms/asset_permission_create_update.html:70
#: terminal/templates/terminal/session_list.html:12
4
#: terminal/templates/terminal/session_list.html:12
6
#: terminal/templates/terminal/terminal_update.html:48
#: users/templates/users/_user.html:47
#: 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/task_history.html:65 ops/templates/ops/task_list.html:42
#: perms/templates/perms/asset_permission_list.html:60
#: terminal/templates/terminal/session_list.html:8
0
#: terminal/templates/terminal/session_list.html:8
1
#: terminal/templates/terminal/terminal_list.html:36
#: users/templates/users/user_group_list.html:15
#: users/templates/users/user_list.html:29
...
...
@@ -1189,7 +1189,7 @@ msgstr "网域详情"
msgid "Domain gateway list"
msgstr "域网关列表"
#: assets/views/domain.py:1
51
#: assets/views/domain.py:1
46
msgid "Update gateway"
msgstr "创建网关"
...
...
@@ -1237,7 +1237,7 @@ msgid "Filename"
msgstr "文件名"
#: 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"
msgstr "成功"
...
...
@@ -1246,7 +1246,7 @@ msgstr "成功"
#: ops/templates/ops/adhoc_history_detail.html:61
#: ops/templates/ops/task_history.html:58 perms/models.py:36
#: perms/templates/perms/asset_permission_detail.html:86 terminal/models.py:137
#: terminal/templates/terminal/session_list.html:7
7
#: terminal/templates/terminal/session_list.html:7
8
msgid "Date start"
msgstr "开始日期"
...
...
@@ -1433,45 +1433,60 @@ msgid ""
"for all users, including administrators)"
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"
msgstr "密码最小长度 "
#: common/forms.py:
191
#: common/forms.py:
205
msgid "Must contain capital letters"
msgstr "必须包含大写字母"
#: common/forms.py:
193
#: common/forms.py:
207
msgid ""
"After opening, the user password changes and resets must contain uppercase "
"letters"
msgstr "开启后,用户密码修改、重置必须包含大写字母"
#: common/forms.py:
199
#: common/forms.py:
213
msgid "Must contain lowercase letters"
msgstr "必须包含小写字母"
#: common/forms.py:2
00
#: common/forms.py:2
14
msgid ""
"After opening, the user password changes and resets must contain lowercase "
"letters"
msgstr "开启后,用户密码修改、重置必须包含小写字母"
#: common/forms.py:2
06
#: common/forms.py:2
20
msgid "Must contain numeric characters"
msgstr "必须包含数字字符"
#: common/forms.py:2
07
#: common/forms.py:2
21
msgid ""
"After opening, the user password changes and resets must contain numeric "
"characters"
msgstr "开启后,用户密码修改、重置必须包含数字字符"
#: common/forms.py:2
13
#: common/forms.py:2
27
msgid "Must contain special characters"
msgstr "必须包含特殊字符"
#: common/forms.py:2
14
#: common/forms.py:2
28
msgid ""
"After opening, the user password changes and resets must contain special "
"characters"
...
...
@@ -1485,7 +1500,8 @@ msgstr ""
msgid "discard time"
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"
msgstr "启用"
...
...
@@ -1531,8 +1547,8 @@ msgid "Security setting"
msgstr "安全设置"
#: common/templates/common/security_setting.html:42
msgid "
MFA setting
"
msgstr "
MFA
设置"
msgid "
User login settings
"
msgstr "
用户登录
设置"
#: common/templates/common/security_setting.html:46
msgid "Password check rule"
...
...
@@ -1803,7 +1819,7 @@ msgid "Versions"
msgstr "版本"
#: ops/templates/ops/task_list.html:40
#: users/templates/users/login_log_list.html:5
4
#: users/templates/users/login_log_list.html:5
7
msgid "Date"
msgstr "日期"
...
...
@@ -2005,7 +2021,7 @@ msgid "Logout"
msgstr "注销登录"
#: templates/_header_bar.html:49 users/templates/users/login.html:44
#: users/templates/users/login.html:6
4
#: users/templates/users/login.html:6
8
msgid "Login"
msgstr "登录"
...
...
@@ -2045,7 +2061,7 @@ msgstr "关闭"
#: 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/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:330 users/views/user.py:380 users/views/user.py:415
msgid "Users"
...
...
@@ -2161,14 +2177,14 @@ msgstr "线程数"
msgid "Boot Time"
msgstr "运行时间"
#: terminal/models.py:132 terminal/templates/terminal/session_list.html:10
2
#: terminal/models.py:132 terminal/templates/terminal/session_list.html:10
4
msgid "Replay"
msgstr "回放"
#: terminal/models.py:133 terminal/templates/terminal/command_list.html:55
#: terminal/templates/terminal/command_list.html:71
#: terminal/templates/terminal/session_detail.html:48
#: terminal/templates/terminal/session_list.html:7
6
#: terminal/templates/terminal/session_list.html:7
7
msgid "Command"
msgstr "命令"
...
...
@@ -2219,24 +2235,28 @@ msgstr "监控"
msgid "Terminate session"
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"
msgstr "时长"
#: terminal/templates/terminal/session_list.html:10
4
#: terminal/templates/terminal/session_list.html:10
6
msgid "Monitor"
msgstr "监控"
#: terminal/templates/terminal/session_list.html:106
#: terminal/templates/terminal/session_list.html:108
#: terminal/templates/terminal/session_list.html:110
msgid "Terminate"
msgstr "终断"
#: terminal/templates/terminal/session_list.html:12
0
#: terminal/templates/terminal/session_list.html:12
2
msgid "Terminate selected"
msgstr "终断所选"
#: terminal/templates/terminal/session_list.html:14
0
#: terminal/templates/terminal/session_list.html:14
2
msgid "Terminate task send, waiting ..."
msgstr "终断任务已发送,请等待"
...
...
@@ -2306,6 +2326,10 @@ msgid ""
"You should use your ssh client tools connect terminal: {} <br /> <br />{}"
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
msgid "Invalid signature header. No credentials provided."
msgstr ""
...
...
@@ -2406,8 +2430,9 @@ msgstr ""
msgid "* Enable MFA authentication to make the account more secure."
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/login_log_list.html:54
msgid "MFA"
msgstr "MFA"
...
...
@@ -2467,23 +2492,53 @@ msgstr "ssh公钥"
msgid "Private Token"
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"
msgstr "登录方式"
#: users/models/authentication.py:
47
#: users/models/authentication.py:
72
msgid "Login ip"
msgstr "登录IP"
#: users/models/authentication.py:
48
#: users/models/authentication.py:
73
msgid "Login city"
msgstr "登录城市"
#: users/models/authentication.py:
49
#: users/models/authentication.py:
74
msgid "User 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"
msgstr "登录日期"
...
...
@@ -2609,7 +2664,7 @@ msgid " for more information"
msgstr "获取更多信息"
#: users/templates/users/forgot_password.html:26
#: users/templates/users/login.html:7
3
#: users/templates/users/login.html:7
7
msgid "Forgot password"
msgstr "忘记密码"
...
...
@@ -2617,7 +2672,7 @@ msgstr "忘记密码"
msgid "Input your email, that will send a mail to your"
msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中"
#: users/templates/users/login.html:5
0
#: users/templates/users/login.html:5
3
msgid "Captcha invalid"
msgstr "验证码错误"
...
...
@@ -2696,10 +2751,6 @@ msgstr "授权的资产"
msgid "Force enabled"
msgstr "强制启用"
#: users/templates/users/user_detail.html:98
msgid "Disabled"
msgstr "禁用"
#: users/templates/users/user_detail.html:119
#: users/templates/users/user_profile.html:108
msgid "Last login"
...
...
@@ -3001,7 +3052,7 @@ msgstr "禁用或失效"
msgid "Password or SSH public key invalid"
msgstr "密码或密钥不合法"
#: users/utils.py:2
90 users/utils.py:300
#: users/utils.py:2
89 users/utils.py:299
msgid "Bit"
msgstr " 位"
...
...
@@ -3017,60 +3068,60 @@ msgstr "更新用户组"
msgid "User group granted asset"
msgstr "用户组授权资产"
#: users/views/login.py:
62
#: users/views/login.py:
75
msgid "Please enable cookies and try again."
msgstr "设置你的浏览器支持cookie"
#: users/views/login.py:1
2
8 users/views/user.py:500 users/views/user.py:525
#: users/views/login.py:1
7
8 users/views/user.py:500 users/views/user.py:525
msgid "MFA code invalid"
msgstr "MFA码认证失败"
#: users/views/login.py:
154
#: users/views/login.py:
207
msgid "Logout success"
msgstr "退出登录成功"
#: users/views/login.py:
155
#: users/views/login.py:
208
msgid "Logout success, return login page"
msgstr "退出登录成功,返回到登录页面"
#: users/views/login.py:
171
#: users/views/login.py:
224
msgid "Email address invalid, please input again"
msgstr "邮箱地址错误,重新输入"
#: users/views/login.py:
184
#: users/views/login.py:
237
msgid "Send reset password message"
msgstr "发送重置密码邮件"
#: users/views/login.py:
185
#: users/views/login.py:
238
msgid "Send reset password mail success, login your mail box and follow it "
msgstr ""
"发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)"
#: users/views/login.py:
198
#: users/views/login.py:
251
msgid "Reset password success"
msgstr "重置密码成功"
#: users/views/login.py:
199
#: users/views/login.py:
252
msgid "Reset password success, return to login page"
msgstr "重置密码成功,返回到登录页面"
#: users/views/login.py:2
20 users/views/login.py:233
#: users/views/login.py:2
73 users/views/login.py:286
msgid "Token invalid or expired"
msgstr "Token错误或失效"
#: users/views/login.py:2
29
#: users/views/login.py:2
82
msgid "Password not same"
msgstr "密码不一致"
#: users/views/login.py:2
39
users/views/user.py:118 users/views/user.py:398
#: users/views/login.py:2
92
users/views/user.py:118 users/views/user.py:398
msgid "* Your password does not meet the requirements"
msgstr "* 您的密码不符合要求"
#: users/views/login.py:
277
#: users/views/login.py:
330
msgid "First login"
msgstr "首次登陆"
#: users/views/login.py:3
36
#: users/views/login.py:3
89
msgid "Login log list"
msgstr "登录日志"
...
...
@@ -3117,3 +3168,6 @@ msgstr "MFA 解绑成功"
#: users/views/user.py:555
msgid "MFA disable success, return login page"
msgstr "MFA 解绑成功,返回登录页面"
#~ msgid "MFA setting"
#~ msgstr "MFA 设置"
apps/jumpserver/settings.py
View file @
df95c93b
...
...
@@ -405,6 +405,8 @@ TERMINAL_REPLAY_STORAGE = {
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
BOOTSTRAP3
=
{
...
...
apps/terminal/templates/terminal/session_list.html
View file @
df95c93b
...
...
@@ -73,6 +73,7 @@
<th
class=
"text-center"
>
{% trans 'System user' %}
</th>
<th
class=
"text-center"
>
{% trans 'Remote addr' %}
</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 'Date start' %}
</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.remote_addr|default:"" }}
</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.date_start }}
</td>
...
...
apps/users/api.py
View file @
df95c93b
...
...
@@ -3,6 +3,7 @@ import uuid
from
django.core.cache
import
cache
from
django.urls
import
reverse
from
django.utils.translation
import
ugettext
as
_
from
rest_framework
import
generics
from
rest_framework.permissions
import
AllowAny
,
IsAuthenticated
...
...
@@ -14,10 +15,11 @@ from .serializers import UserSerializer, UserGroupSerializer, \
UserGroupUpdateMemeberSerializer
,
UserPKUpdateSerializer
,
\
UserUpdateGroupSerializer
,
ChangeUserPasswordSerializer
from
.tasks
import
write_login_log_async
from
.models
import
User
,
UserGroup
from
.models
import
User
,
UserGroup
,
LoginLog
from
.permissions
import
IsSuperUser
,
IsValidUser
,
IsCurrentUserOrReadOnly
,
\
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.utils
import
get_logger
...
...
@@ -149,10 +151,23 @@ class UserOtpAuthApi(APIView):
return
Response
({
'msg'
:
'请先进行用户名和密码验证'
},
status
=
401
)
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
)
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
)
self
.
write_login_log
(
request
,
user
)
return
Response
(
{
'token'
:
token
,
...
...
@@ -161,7 +176,7 @@ class UserOtpAuthApi(APIView):
)
@staticmethod
def
write_login_log
(
request
,
user
):
def
write_login_log
(
request
,
data
):
login_ip
=
request
.
data
.
get
(
'remote_addr'
,
None
)
login_type
=
request
.
data
.
get
(
'login_type'
,
''
)
user_agent
=
request
.
data
.
get
(
'HTTP_USER_AGENT'
,
''
)
...
...
@@ -169,25 +184,52 @@ class UserOtpAuthApi(APIView):
if
not
login_ip
:
login_ip
=
get_login_ip
(
request
)
write_login_log_async
.
delay
(
user
.
username
,
ip
=
login_ip
,
type
=
login_type
,
user_agent
=
user_agent
,
)
tmp_data
=
{
'ip'
:
login_ip
,
'type'
:
login_type
,
'user_agent'
:
user_agent
}
data
.
update
(
tmp_data
)
write_login_log_async
.
delay
(
**
data
)
class
UserAuthApi
(
APIView
):
permission_classes
=
(
AllowAny
,)
serializer_class
=
UserSerializer
key_prefix_limit
=
"_LOGIN_LIMIT_{}_{}"
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
:
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
)
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
)
self
.
write_login_log
(
request
,
user
)
return
Response
(
{
'token'
:
token
,
...
...
@@ -204,7 +246,8 @@ class UserAuthApi(APIView):
'otp_url'
:
reverse
(
'api-users:user-otp-auth'
),
'seed'
:
seed
,
'user'
:
self
.
serializer_class
(
user
)
.
data
},
status
=
300
)
},
status
=
300
)
@staticmethod
def
check_user_valid
(
request
):
...
...
@@ -218,7 +261,7 @@ class UserAuthApi(APIView):
return
user
,
msg
@staticmethod
def
write_login_log
(
request
,
user
):
def
write_login_log
(
request
,
data
):
login_ip
=
request
.
data
.
get
(
'remote_addr'
,
None
)
login_type
=
request
.
data
.
get
(
'login_type'
,
''
)
user_agent
=
request
.
data
.
get
(
'HTTP_USER_AGENT'
,
''
)
...
...
@@ -226,10 +269,14 @@ class UserAuthApi(APIView):
if
not
login_ip
:
login_ip
=
get_login_ip
(
request
)
write_login_log_async
.
delay
(
user
.
username
,
ip
=
login_ip
,
type
=
login_type
,
user_agent
=
user_agent
,
)
tmp_data
=
{
'ip'
:
login_ip
,
'type'
:
login_type
,
'user_agent'
:
user_agent
,
}
data
.
update
(
tmp_data
)
write_login_log_async
.
delay
(
**
data
)
class
UserConnectionTokenApi
(
APIView
):
...
...
apps/users/models/authentication.py
View file @
df95c93b
...
...
@@ -41,12 +41,40 @@ class LoginLog(models.Model):
(
'W'
,
'Web'
),
(
'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
)
username
=
models
.
CharField
(
max_length
=
20
,
verbose_name
=
_
(
'Username'
))
type
=
models
.
CharField
(
choices
=
LOGIN_TYPE_CHOICE
,
max_length
=
2
,
verbose_name
=
_
(
'Login type'
))
ip
=
models
.
GenericIPAddressField
(
verbose_name
=
_
(
'Login ip'
))
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'
))
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'
))
class
Meta
:
...
...
apps/users/templates/users/login.html
View file @
df95c93b
...
...
@@ -45,13 +45,17 @@
</div>
<form
class=
"m-t"
role=
"form"
method=
"post"
action=
""
>
{% 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 %}
<p
class=
"red-fonts"
>
{% trans 'Captcha invalid' %}
</p>
{% else %}
<p
class=
"red-fonts"
>
{{ form.non_field_errors.as_text }}
</p>
{% endif %}
{% endif %}
<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 %}"
>
</div>
...
...
apps/users/templates/users/login_log_list.html
View file @
df95c93b
...
...
@@ -51,6 +51,9 @@
<th
class=
"text-center"
>
{% trans 'UA' %}
</th>
<th
class=
"text-center"
>
{% trans 'IP' %}
</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>
{% endblock %}
...
...
@@ -65,6 +68,9 @@
</td>
<td
class=
"text-center"
>
{{ login_log.ip }}
</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>
</tr>
{% endfor %}
...
...
apps/users/utils.py
View file @
df95c93b
...
...
@@ -13,7 +13,7 @@ import ipaddress
from
django.http
import
Http404
from
django.conf
import
settings
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.core.cache
import
cache
...
...
@@ -200,16 +200,15 @@ def get_login_ip(request):
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
)):
ip
=
ip
[:
15
]
city
=
"Unknown"
else
:
city
=
get_ip_city
(
ip
)
LoginLog
.
objects
.
create
(
username
=
username
,
type
=
type
,
ip
=
ip
,
city
=
city
,
user_agent
=
user_agent
)
kwargs
.
update
({
'ip'
:
ip
,
'city'
:
city
})
LoginLog
.
objects
.
create
(
**
kwargs
)
def
get_ip_city
(
ip
,
timeout
=
10
):
...
...
@@ -332,3 +331,29 @@ def check_password_rules(password):
match_obj
=
re
.
match
(
pattern
,
password
)
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
apps/users/views/login.py
View file @
df95c93b
...
...
@@ -25,8 +25,10 @@ from common.utils import get_object_or_none
from
common.mixins
import
DatetimeSearchMixin
,
AdminUserRequiredMixin
from
common.models
import
Setting
from
..models
import
User
,
LoginLog
from
..utils
import
send_reset_password_mail
,
check_otp_code
,
get_login_ip
,
redirect_user_first_login_or_index
,
\
get_user_or_tmp_user
,
set_tmp_user_to_cache
,
get_password_check_rules
,
check_password_rules
from
..utils
import
send_reset_password_mail
,
check_otp_code
,
get_login_ip
,
\
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
..
import
forms
...
...
@@ -47,7 +49,8 @@ class UserLoginView(FormView):
form_class
=
forms
.
UserLoginForm
form_class_captcha
=
forms
.
UserLoginCaptchaForm
redirect_field_name
=
'next'
key_prefix
=
"_LOGIN_INVALID_{}"
key_prefix_captcha
=
"_LOGIN_INVALID_{}"
key_prefix_limit
=
"_LOGIN_LIMIT_{}_{}"
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
if
request
.
user
.
is_staff
:
...
...
@@ -57,6 +60,16 @@ class UserLoginView(FormView):
request
.
session
.
set_test_cookie
()
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
):
if
not
self
.
request
.
session
.
test_cookie_worked
():
return
HttpResponse
(
_
(
"Please enable cookies and try again."
))
...
...
@@ -65,8 +78,23 @@ class UserLoginView(FormView):
return
redirect
(
self
.
get_success_url
())
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
)
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
form
=
self
.
form_class_captcha
(
data
=
form
.
data
)
form
.
_errors
=
old_form
.
errors
...
...
@@ -74,7 +102,7 @@ class UserLoginView(FormView):
def
get_form_class
(
self
):
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
else
:
return
self
.
form_class
...
...
@@ -91,7 +119,13 @@ class UserLoginView(FormView):
elif
not
user
.
otp_enabled
:
# 0 & T,F
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
)
def
get_context_data
(
self
,
**
kwargs
):
...
...
@@ -101,13 +135,16 @@ class UserLoginView(FormView):
kwargs
.
update
(
context
)
return
super
()
.
get_context_data
(
**
kwargs
)
def
write_login_log
(
self
):
def
write_login_log
(
self
,
data
):
login_ip
=
get_login_ip
(
self
.
request
)
user_agent
=
self
.
request
.
META
.
get
(
'HTTP_USER_AGENT'
,
''
)
write_login_log_async
.
delay
(
self
.
request
.
user
.
username
,
type
=
'W'
,
ip
=
login_ip
,
user_agent
=
user_agent
)
tmp_data
=
{
'ip'
:
login_ip
,
'type'
:
'W'
,
'user_agent'
:
user_agent
}
data
.
update
(
tmp_data
)
write_login_log_async
.
delay
(
**
data
)
class
UserLoginOtpView
(
FormView
):
...
...
@@ -122,22 +159,38 @@ class UserLoginOtpView(FormView):
if
check_otp_code
(
otp_secret_key
,
otp_code
):
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
())
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'
))
return
super
()
.
form_invalid
(
form
)
def
get_success_url
(
self
):
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
)
user_agent
=
self
.
request
.
META
.
get
(
'HTTP_USER_AGENT'
,
''
)
write_login_log_async
.
delay
(
self
.
request
.
user
.
username
,
type
=
'W'
,
ip
=
login_ip
,
user_agent
=
user_agent
)
tmp_data
=
{
'ip'
:
login_ip
,
'type'
:
'W'
,
'user_agent'
:
user_agent
}
data
.
update
(
tmp_data
)
write_login_log_async
.
delay
(
**
data
)
@method_decorator
(
never_cache
,
name
=
'dispatch'
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment