From e1be86791351e987e706126e6a2c74a0fa20910d Mon Sep 17 00:00:00 2001 From: BaiJiangJie Date: Tue, 3 Jul 2018 17:47:12 +0800 Subject: [PATCH] =?UTF-8?q?[Update]=20=E6=9B=B4=E6=96=B0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=97=A5=E5=BF=97=EF=BC=8C=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E5=A4=B1=E8=B4=A5=E6=97=A5=E5=BF=97=E5=B9=B6?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0MFA=E5=90=AF=E7=94=A8=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/i18n/zh/LC_MESSAGES/django.mo | Bin 35966 -> 36216 bytes apps/i18n/zh/LC_MESSAGES/django.po | 118 +++++++++++------- apps/users/api.py | 66 +++++++--- apps/users/models/authentication.py | 28 +++++ .../users/templates/users/login_log_list.html | 6 + apps/users/utils.py | 12 +- apps/users/views/login.py | 66 +++++++--- 7 files changed, 217 insertions(+), 79 deletions(-) diff --git a/apps/i18n/zh/LC_MESSAGES/django.mo b/apps/i18n/zh/LC_MESSAGES/django.mo index d842930e4e5b0eece76196f4cd257d50a9117912..339af4456cb72a4e21841737e7c98bce3a39fc6a 100644 GIT binary patch delta 12337 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\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,14 +147,14 @@ 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_log_list.html:49 @@ -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." @@ -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 "成功" @@ -1485,7 +1485,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 "启用" @@ -1803,7 +1804,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 "日期" @@ -2045,7 +2046,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:311 users/views/login.py:369 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" @@ -2406,8 +2407,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 +2469,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 "登录日期" @@ -2646,7 +2678,7 @@ msgid "Can't provide security? Please contact the administrator!" msgstr "如果不能提供MFA验证码,请联系管理员!" #: users/templates/users/reset_password.html:46 -#: users/templates/users/user_detail.html:352 users/utils.py:80 +#: users/templates/users/user_detail.html:352 users/utils.py:81 msgid "Reset password" msgstr "重置密码" @@ -2696,10 +2728,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" @@ -2867,11 +2895,11 @@ msgstr "新的公钥已设置成功,请下载对应的私钥" msgid "Update user" msgstr "更新用户" -#: users/utils.py:41 +#: users/utils.py:42 msgid "Create account successfully" msgstr "创建账户成功" -#: users/utils.py:43 +#: users/utils.py:44 #, python-format msgid "" "\n" @@ -2916,7 +2944,7 @@ msgstr "" "
\n" " " -#: users/utils.py:82 +#: users/utils.py:83 #, python-format msgid "" "\n" @@ -2960,11 +2988,11 @@ msgstr "" "
\n" " " -#: users/utils.py:113 +#: users/utils.py:114 msgid "SSH Key Reset" msgstr "重置ssh密钥" -#: users/utils.py:115 +#: users/utils.py:116 #, python-format msgid "" "\n" @@ -2989,15 +3017,15 @@ msgstr "" "
\n" " " -#: users/utils.py:148 +#: users/utils.py:149 msgid "User not exist" msgstr "用户不存在" -#: users/utils.py:150 +#: users/utils.py:151 msgid "Disabled or expired" msgstr "禁用或失效" -#: users/utils.py:163 +#: users/utils.py:164 msgid "Password or SSH public key invalid" msgstr "密码或密钥不合法" @@ -3017,60 +3045,60 @@ msgstr "更新用户组" msgid "User group granted asset" msgstr "用户组授权资产" -#: users/views/login.py:62 +#: users/views/login.py:63 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:159 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:188 msgid "Logout success" msgstr "退出登录成功" -#: users/views/login.py:155 +#: users/views/login.py:189 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: users/views/login.py:171 +#: users/views/login.py:205 msgid "Email address invalid, please input again" msgstr "邮箱地址错误,重新输入" -#: users/views/login.py:184 +#: users/views/login.py:218 msgid "Send reset password message" msgstr "发送重置密码邮件" -#: users/views/login.py:185 +#: users/views/login.py:219 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:232 msgid "Reset password success" msgstr "重置密码成功" -#: users/views/login.py:199 +#: users/views/login.py:233 msgid "Reset password success, return to login page" msgstr "重置密码成功,返回到登录页面" -#: users/views/login.py:220 users/views/login.py:233 +#: users/views/login.py:254 users/views/login.py:267 msgid "Token invalid or expired" msgstr "Token错误或失效" -#: users/views/login.py:229 +#: users/views/login.py:263 msgid "Password not same" msgstr "密码不一致" -#: users/views/login.py:239 users/views/user.py:118 users/views/user.py:398 +#: users/views/login.py:273 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:311 msgid "First login" msgstr "首次登陆" -#: users/views/login.py:336 +#: users/views/login.py:370 msgid "Login log list" msgstr "登录日志" diff --git a/apps/users/api.py b/apps/users/api.py index dbc5b66a8..5df312fcf 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -14,7 +14,7 @@ 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 @@ -153,10 +153,25 @@ class UserOtpAuthApi(APIView): return Response({'msg': '请先进行用户名和密码验证'}, status=401) if not check_otp_code(user.otp_secret_key, otp_code): + # Write login failed log + kwargs = { + 'username': user.username, + 'mfa': int(user.otp_enabled), + 'reason': LoginLog.REASON_MFA, + 'status': False + } + self.write_login_log(request, **kwargs) return Response({'msg': 'MFA认证失败'}, status=401) + # Write login success log + kwargs = { + 'username': user.username, + 'mfa': int(user.otp_enabled), + 'reason': LoginLog.REASON_NOTHING, + 'status': True + } + self.write_login_log(request, **kwargs) token = generate_token(request, user) - self.write_login_log(request, user) return Response( { 'token': token, @@ -165,7 +180,7 @@ class UserOtpAuthApi(APIView): ) @staticmethod - def write_login_log(request, user): + def write_login_log(request, **kwargs): login_ip = request.data.get('remote_addr', None) login_type = request.data.get('login_type', '') user_agent = request.data.get('HTTP_USER_AGENT', '') @@ -173,10 +188,13 @@ 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, - ) + data = { + 'ip': login_ip, + 'type': login_type, + 'user_agent': user_agent + } + kwargs.update(data) + write_login_log_async.delay(**kwargs) class UserAuthApi(APIView): @@ -187,11 +205,26 @@ class UserAuthApi(APIView): user, msg = self.check_user_valid(request) if not user: + # Write login failed log + kwargs = { + 'username': request.data.get('username', ''), + 'mfa': LoginLog.MFA_UNKNOWN, + 'reason': LoginLog.REASON_PASSWORD, + 'status': False + } + self.write_login_log(request, **kwargs) return Response({'msg': msg}, status=401) if not user.otp_enabled: + # Write login success log + kwargs = { + 'username': user.username, + 'mfa': int(user.otp_enabled), + 'reason': LoginLog.REASON_NOTHING, + 'status': True + } + self.write_login_log(request, **kwargs) token = generate_token(request, user) - self.write_login_log(request, user) return Response( { 'token': token, @@ -208,7 +241,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): @@ -222,7 +256,7 @@ class UserAuthApi(APIView): return user, msg @staticmethod - def write_login_log(request, user): + def write_login_log(request, **kwargs): login_ip = request.data.get('remote_addr', None) login_type = request.data.get('login_type', '') user_agent = request.data.get('HTTP_USER_AGENT', '') @@ -230,10 +264,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, - ) + data = { + 'ip': login_ip, + 'type': login_type, + 'user_agent': user_agent, + } + kwargs.update(data) + + write_login_log_async.delay(**kwargs) class UserConnectionTokenApi(APIView): diff --git a/apps/users/models/authentication.py b/apps/users/models/authentication.py index 5169a79d2..493e4bb59 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_DISABLED, 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_log_list.html b/apps/users/templates/users/login_log_list.html index 4a08c28db..afaf671a5 100644 --- a/apps/users/templates/users/login_log_list.html +++ b/apps/users/templates/users/login_log_list.html @@ -51,6 +51,9 @@ {% trans 'UA' %} {% trans 'IP' %} {% trans 'City' %} + {% trans 'MFA' %} + {% trans 'Reason' %} + {% trans 'Status' %} {% trans 'Date' %} {% endblock %} @@ -65,6 +68,9 @@ {{ login_log.ip }} {{ login_log.city }} + {{ login_log.get_mfa_display }} + {{ login_log.get_reason_display }} + {{ login_log.get_status_display }} {{ login_log.datetime }} {% endfor %} diff --git a/apps/users/utils.py b/apps/users/utils.py index 989632e2c..fb2a8d93e 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -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 @@ -22,6 +22,7 @@ from common.utils import reverse, get_object_or_none from common.models import Setting from common.forms import SecuritySettingForm from .models import User, LoginLog +# from .tasks import write_login_log_async logger = logging.getLogger('jumpserver') @@ -200,16 +201,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): diff --git a/apps/users/views/login.py b/apps/users/views/login.py index feaf47e89..02f3b66af 100644 --- a/apps/users/views/login.py +++ b/apps/users/views/login.py @@ -25,8 +25,9 @@ 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 from ..tasks import write_login_log_async from .. import forms @@ -65,6 +66,15 @@ class UserLoginView(FormView): return redirect(self.get_success_url()) def form_invalid(self, form): + # Write login failed log + kwargs = { + 'username': form.cleaned_data.get('username'), + 'mfa': LoginLog.MFA_UNKNOWN, + 'reason': LoginLog.REASON_PASSWORD, + 'status': False + } + self.write_login_log(**kwargs) + ip = get_login_ip(self.request) cache.set(self.key_prefix.format(ip), 1, 3600) old_form = form @@ -91,7 +101,14 @@ class UserLoginView(FormView): elif not user.otp_enabled: # 0 & T,F auth_login(self.request, user) - self.write_login_log() + # Write login success log + kwargs = { + 'username': self.request.user.username, + 'mfa': int(self.request.user.otp_enabled), + 'reason': LoginLog.REASON_NOTHING, + 'status': True + } + self.write_login_log(**kwargs) return redirect_user_first_login_or_index(self.request, self.redirect_field_name) def get_context_data(self, **kwargs): @@ -101,13 +118,16 @@ class UserLoginView(FormView): kwargs.update(context) return super().get_context_data(**kwargs) - def write_login_log(self): + def write_login_log(self, **kwargs): 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 - ) + data = { + 'ip': login_ip, + 'type': 'W', + 'user_agent': user_agent + } + kwargs.update(data) + write_login_log_async.delay(**kwargs) class UserLoginOtpView(FormView): @@ -122,22 +142,40 @@ class UserLoginOtpView(FormView): if check_otp_code(otp_secret_key, otp_code): auth_login(self.request, user) - self.write_login_log() + # Write login success log + kwargs = { + 'username': self.request.user.username, + 'mfa': int(self.request.user.otp_enabled), + 'reason': LoginLog.REASON_NOTHING, + 'status': True + } + self.write_login_log(**kwargs) return redirect(self.get_success_url()) else: + # Write login failed log + kwargs = { + 'username': user.username, + 'mfa': int(user.otp_enabled), + 'reason': LoginLog.REASON_MFA, + 'status': False + } + self.write_login_log(**kwargs) 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, **kwargs): 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 - ) + data = { + 'ip': login_ip, + 'type': 'W', + 'user_agent': user_agent + } + kwargs.update(data) + write_login_log_async.delay(**kwargs) @method_decorator(never_cache, name='dispatch')