diff --git a/apps/common/forms.py b/apps/common/forms.py
index 87b2f4f12..a11420498 100644
--- a/apps/common/forms.py
+++ b/apps/common/forms.py
@@ -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"),
diff --git a/apps/common/templates/common/security_setting.html b/apps/common/templates/common/security_setting.html
index 2260b76b9..08d978d23 100644
--- a/apps/common/templates/common/security_setting.html
+++ b/apps/common/templates/common/security_setting.html
@@ -39,9 +39,9 @@
{% endif %}
{% csrf_token %}
-
{% trans "MFA setting" %}
+ {% trans "User login settings" %}
{% for field in form %}
- {% if forloop.counter == 2 %}
+ {% if forloop.counter == 4 %}
{% trans "Password check rule" %}
{% endif %}
diff --git a/apps/i18n/zh/LC_MESSAGES/django.mo b/apps/i18n/zh/LC_MESSAGES/django.mo
index d842930e4..78d56177e 100644
Binary files a/apps/i18n/zh/LC_MESSAGES/django.mo and b/apps/i18n/zh/LC_MESSAGES/django.mo differ
diff --git a/apps/i18n/zh/LC_MESSAGES/django.po b/apps/i18n/zh/LC_MESSAGES/django.po
index c596db6ac..0cf576ebe 100644
--- a/apps/i18n/zh/LC_MESSAGES/django.po
+++ b/apps/i18n/zh/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Jumpserver 0.3.3\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"
"Last-Translator: ibuler \n"
"Language-Team: Jumpserver team\n"
@@ -118,7 +118,7 @@ msgstr "端口"
msgid "Asset"
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/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: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/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:124
+#: assets/forms/user.py:125
msgid "* Automatic login mode, must fill in the username."
msgstr "自动登录模式,必须填写用户名"
-#: assets/forms/user.py:144
+#: assets/forms/user.py:145
msgid "Auto push system user to asset"
msgstr "自动推送系统用户到资产"
-#: assets/forms/user.py:145
+#: assets/forms/user.py:146
msgid ""
"High level will be using login asset as default, if user was granted more "
"than 2 system user"
msgstr "高优先级的系统用户将会作为默认登录用户"
-#: assets/forms/user.py:147
+#: assets/forms/user.py:148
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: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/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:124
+#: terminal/templates/terminal/session_list.html:126
#: 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:80
+#: terminal/templates/terminal/session_list.html:81
#: 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:151
+#: assets/views/domain.py:146
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:77
+#: terminal/templates/terminal/session_list.html:78
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:200
+#: common/forms.py:214
msgid ""
"After opening, the user password changes and resets must contain lowercase "
"letters"
msgstr "开启后,用户密码修改、重置必须包含小写字母"
-#: common/forms.py:206
+#: common/forms.py:220
msgid "Must contain numeric characters"
msgstr "必须包含数字字符"
-#: common/forms.py:207
+#: common/forms.py:221
msgid ""
"After opening, the user password changes and resets must contain numeric "
"characters"
msgstr "开启后,用户密码修改、重置必须包含数字字符"
-#: common/forms.py:213
+#: common/forms.py:227
msgid "Must contain special characters"
msgstr "必须包含特殊字符"
-#: common/forms.py:214
+#: common/forms.py:228
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:54
+#: users/templates/users/login_log_list.html:57
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:64
+#: users/templates/users/login.html:68
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:102
+#: terminal/models.py:132 terminal/templates/terminal/session_list.html:104
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:76
+#: terminal/templates/terminal/session_list.html:77
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:104
+#: terminal/templates/terminal/session_list.html:106
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:120
+#: terminal/templates/terminal/session_list.html:122
msgid "Terminate selected"
msgstr "终断所选"
-#: terminal/templates/terminal/session_list.html:140
+#: terminal/templates/terminal/session_list.html:142
msgid "Terminate task send, waiting ..."
msgstr "终断任务已发送,请等待"
@@ -2306,6 +2326,10 @@ msgid ""
"You should use your ssh client tools connect terminal: {}
{}"
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:73
+#: users/templates/users/login.html:77
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:50
+#: users/templates/users/login.html:53
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:290 users/utils.py:300
+#: users/utils.py:289 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: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"
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:220 users/views/login.py:233
+#: users/views/login.py:273 users/views/login.py:286
msgid "Token invalid or expired"
msgstr "Token错误或失效"
-#: users/views/login.py:229
+#: users/views/login.py:282
msgid "Password not same"
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"
msgstr "* 您的密码不符合要求"
-#: users/views/login.py:277
+#: users/views/login.py:330
msgid "First login"
msgstr "首次登陆"
-#: users/views/login.py:336
+#: users/views/login.py:389
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 设置"
diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py
index c8ed1505e..c0a536276 100644
--- a/apps/jumpserver/settings.py
+++ b/apps/jumpserver/settings.py
@@ -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 = {
diff --git a/apps/terminal/templates/terminal/session_list.html b/apps/terminal/templates/terminal/session_list.html
index 8cea8e217..33ae09877 100644
--- a/apps/terminal/templates/terminal/session_list.html
+++ b/apps/terminal/templates/terminal/session_list.html
@@ -73,6 +73,7 @@
{% trans 'System user' %} |
{% trans 'Remote addr' %} |
{% trans 'Protocol' %} |
+ {% trans 'Login from' %} |
{% trans 'Command' %} |
{% trans 'Date start' %} |
{# {% trans 'Date last active' %} | #}
@@ -92,6 +93,7 @@
{{ session.system_user }} |
{{ session.remote_addr|default:"" }} |
{{ session.protocol }} |
+ {{ session.get_login_from_display }} |
{{ session.id | get_session_command_amount }} |
{{ session.date_start }} |
diff --git a/apps/users/api.py b/apps/users/api.py
index ae4e47b60..c23112384 100644
--- a/apps/users/api.py
+++ b/apps/users/api.py
@@ -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):
diff --git a/apps/users/models/authentication.py b/apps/users/models/authentication.py
index 5169a79d2..cb0b7d85f 100644
--- a/apps/users/models/authentication.py
+++ b/apps/users/models/authentication.py
@@ -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:
diff --git a/apps/users/templates/users/login.html b/apps/users/templates/users/login.html
index 7dd3f5d0a..6b55a58bf 100644
--- a/apps/users/templates/users/login.html
+++ b/apps/users/templates/users/login.html
@@ -45,13 +45,17 @@