diff --git a/apps/audits/const.py b/apps/audits/const.py new file mode 100644 index 000000000..97f6cc6b1 --- /dev/null +++ b/apps/audits/const.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# +from django.utils.translation import ugettext_lazy as _ + +DEFAULT_CITY = _("Unknown") diff --git a/apps/audits/signals_handler.py b/apps/audits/signals_handler.py index c793481e8..4ba7e8408 100644 --- a/apps/audits/signals_handler.py +++ b/apps/audits/signals_handler.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # from django.db.models.signals import ( - post_save, post_delete, m2m_changed, pre_delete + post_save, m2m_changed, pre_delete ) from django.dispatch import receiver from django.conf import settings @@ -14,25 +14,25 @@ from rest_framework.renderers import JSONRenderer from rest_framework.request import Request from assets.models import Asset, SystemUser -from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR +from authentication.signals import post_auth_failed, post_auth_success +from authentication.utils import check_different_city_login from jumpserver.utils import current_request -from common.utils import get_request_ip, get_logger, get_syslogger from users.models import User from users.signals import post_user_change_password -from authentication.signals import post_auth_failed, post_auth_success from terminal.models import Session, Command -from common.utils.encode import model_to_json from .utils import write_login_log from . import models from .models import OperateLog from orgs.utils import current_org from perms.models import AssetPermission, ApplicationPermission +from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR +from common.utils import get_request_ip, get_logger, get_syslogger +from common.utils.encode import model_to_json logger = get_logger(__name__) sys_logger = get_syslogger(__name__) json_render = JSONRenderer() - MODELS_NEED_RECORD = ( # users 'User', 'UserGroup', @@ -165,7 +165,6 @@ M2M_NEED_RECORD = { ), } - M2M_ACTION = { POST_ADD: 'add', POST_REMOVE: 'remove', @@ -305,6 +304,7 @@ def generate_data(username, request, login_type=None): @receiver(post_auth_success) def on_user_auth_success(sender, user, request, login_type=None, **kwargs): logger.debug('User login success: {}'.format(user.username)) + check_different_city_login(user, request) data = generate_data(user.username, request, login_type=login_type) data.update({'mfa': int(user.mfa_enabled), 'status': True}) write_login_log(**data) diff --git a/apps/audits/utils.py b/apps/audits/utils.py index 8710bfc0f..e569a7ebb 100644 --- a/apps/audits/utils.py +++ b/apps/audits/utils.py @@ -1,8 +1,8 @@ import csv import codecs from django.http import HttpResponse -from django.utils.translation import ugettext as _ +from .const import DEFAULT_CITY from common.utils import validate_ip, get_ip_city @@ -27,12 +27,12 @@ def write_content_to_excel(response, header=None, login_logs=None, fields=None): def write_login_log(*args, **kwargs): from audits.models import UserLoginLog - default_city = _("Unknown") + ip = kwargs.get('ip') or '' if not (ip and validate_ip(ip)): ip = ip[:15] - city = default_city + city = DEFAULT_CITY else: - city = get_ip_city(ip) or default_city + city = get_ip_city(ip) or DEFAULT_CITY kwargs.update({'ip': ip, 'city': city}) UserLoginLog.objects.create(**kwargs) diff --git a/apps/authentication/notifications.py b/apps/authentication/notifications.py new file mode 100644 index 000000000..cad3c0f2c --- /dev/null +++ b/apps/authentication/notifications.py @@ -0,0 +1,75 @@ +from django.utils import timezone +from django.utils.translation import ugettext as _ + +from notifications.notifications import UserMessage +from settings.api import PublicSettingApi +from common.utils import get_logger + +logger = get_logger(__file__) + +EMAIL_TEMPLATE = _( + "" + "
Dear {server_name} user, Hello!
" + "Your account has remote login behavior, please pay attention.
" + "User: {username}
" + "Login time: {time}
" + "Login location: {city} ({ip})
" + "If you suspect that the login behavior is abnormal, please modify " + "
the account password in time.
" + "Thank you for your attention to {server_name}!
") + + +class DifferentCityLoginMessage(UserMessage): + def __init__(self, user, ip, city): + self.ip = ip + self.city = city + super().__init__(user) + + @property + def time(self): + return timezone.now().strftime("%Y-%m-%d %H:%M:%S") + + @property + def subject(self): + return _('Different city login reminder') + + def get_text_msg(self) -> dict: + message = _( + "" + "{subject}\n" + "Dear {server_name} user, Hello!\n" + "Your account has remote login behavior, please pay attention.\n" + "User: {username}\n" + "Login time: {time}\n" + "Login location: {city} ({ip})\n" + "If you suspect that the login behavior is abnormal, please modify " + "the account password in time.\n" + "Thank you for your attention to {server_name}!\n" + ).format( + subject=self.subject, + server_name=PublicSettingApi.get_login_title(), + username=self.user.username, + ip=self.ip, + time=self.time, + city=self.city, + ) + return { + 'subject': self.subject, + 'message': message + } + + def get_html_msg(self) -> dict: + message = EMAIL_TEMPLATE.format( + subject=self.subject, + server_name=PublicSettingApi.get_login_title(), + username=self.user.username, + ip=self.ip, + time=self.time, + city=self.city, + ) + return { + 'subject': self.subject, + 'message': message + } diff --git a/apps/authentication/utils.py b/apps/authentication/utils.py index 594e4e68a..8c3820eb8 100644 --- a/apps/authentication/utils.py +++ b/apps/authentication/utils.py @@ -4,6 +4,12 @@ import base64 from Cryptodome.PublicKey import RSA from Cryptodome.Cipher import PKCS1_v1_5 from Cryptodome import Random + +from .notifications import DifferentCityLoginMessage +from audits.models import UserLoginLog +from audits.const import DEFAULT_CITY +from common.utils import get_request_ip +from common.utils import validate_ip, get_ip_city from common.utils import get_logger logger = get_logger(__file__) @@ -43,3 +49,16 @@ def rsa_decrypt(cipher_text, rsa_private_key=None): cipher_decoded = base64.b16decode(hex_fixed.upper()) message = cipher.decrypt(cipher_decoded, b'error').decode() return message + + +def check_different_city_login(user, request): + ip = get_request_ip(request) or '0.0.0.0' + + if not (ip and validate_ip(ip)): + city = DEFAULT_CITY + else: + city = get_ip_city(ip) or DEFAULT_CITY + + last_user_login = UserLoginLog.objects.filter(username=user.username, status=True).first() + if last_user_login.city != city: + DifferentCityLoginMessage(user, ip, city).publish_async() diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 54bf2d53a..dfaf51aa5 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-09-29 14:42+0800\n" +"POT-Creation-Date: 2021-09-29 14:51+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibulerDear {server_name} user, Hello!
Your account has " +"remote login behavior, please pay attention.
User: {username}" +"p>
Login time: {time}
Login location: {city} ({ip})
If you " +"suspect that the login behavior is abnormal, please modify
the account " +"password in time.
Thank you for your attention to {server_name}!" +"p>" +msgstr "" +"
尊敬的{server_name}用户, 您好!
您的账" +"号存在异地登录行为,请关注。
用户: {username}
登录时间: {time}" +"p>
登录地点: {city} ({ip})
若怀疑此次登录行为异常,请及时修改账号密" +"码。
感谢您对{server_name}的关注!
" + +#: authentication/notifications.py:36 +msgid "Different city login reminder" +msgstr "异地登录提醒" + +#: authentication/notifications.py:40 +#, python-brace-format +msgid "" +"{subject}\n" +"Dear {server_name} user, Hello!\n" +"Your account has remote login behavior, please pay attention.\n" +"User: {username}\n" +"Login time: {time}\n" +"Login location: {city} ({ip})\n" +"If you suspect that the login behavior is abnormal, please modify the " +"account password in time.\n" +"Thank you for your attention to {server_name}!\n" +msgstr "" +"{subject}\n" +"尊敬的{server_name}用户, 您好!\n" +"您的账号存在异地登录行为,请关注。\n" +"用户: {username}\n" +"登录时间: {time}\n" +"登录地点: {city} ({ip})\n" +"若怀疑此次登录行为异常,请及时修改账号密码。\n" +"感谢您对{server_name}的关注!\n" + #: authentication/sms_verify_code.py:17 msgid "The verification code has expired. Please resend it" msgstr "验证码已过期,请重新发送" @@ -2853,12 +2894,16 @@ msgid "Enable WeCom Auth" msgstr "启用企业微信认证" #: settings/serializers/basic.py:9 +#, fuzzy +#| msgid "Object" msgid "Subject" -msgstr "主题" +msgstr "对象" #: settings/serializers/basic.py:13 +#, fuzzy +#| msgid "Target url" msgid "More url" -msgstr "更多信息链接" +msgstr "目标URL" #: settings/serializers/basic.py:28 msgid "Site url" @@ -6126,8 +6171,6 @@ msgid "AP-Singapore" msgstr "亚太-新加坡" #: xpack/plugins/cloud/providers/huaweicloud.py:48 -#, fuzzy -#| msgid "CN-Hong Kong" msgid "CN-Hong Kong" msgstr "中国-香港" @@ -6281,9 +6324,6 @@ msgstr "旗舰版" msgid "Community edition" msgstr "社区版" -#~ msgid "Announcement enabled" -#~ msgstr "启用公告" - #~ msgid "OpenID" #~ msgstr "OpenID" diff --git a/requirements/requirements.txt b/requirements/requirements.txt index e0fd7b738..9142dcaad 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -14,7 +14,7 @@ coreapi==2.3.3 coreschema==0.0.4 cryptography==3.3.2 decorator==4.1.2 -Django==3.1.12 +Django==3.1.13 django-auth-ldap==2.2.0 django-bootstrap3==14.2.0 django-celery-beat==2.0 @@ -31,7 +31,7 @@ docutils==0.14 ecdsa==0.13.3 enum-compat==0.0.2 ephem==3.7.6.0 -eventlet==0.24.1 +eventlet==0.31.1 future==0.16.0 ForgeryPy3==0.3.1 greenlet==0.4.14 @@ -49,7 +49,7 @@ olefile==0.44 openapi-codec==1.3.2 paramiko==2.7.2 passlib==1.7.1 -Pillow==8.2.0 +Pillow==8.3.2 pyasn1==0.4.8 pycparser==2.19 pycryptodome==3.10.1 @@ -59,7 +59,7 @@ PyNaCl==1.2.1 python-dateutil==2.6.1 #python-gssapi==0.6.4 pytz==2018.3 -PyYAML==5.2 +PyYAML==5.4 redis==3.5.3 requests==2.25.1 jms-storage==0.0.39