diff --git a/apps/authentication/signals.py b/apps/authentication/signals.py index f33a3b821f401006b78b45ec97de232f4b25b7fe..5ba50355096088f08eeea1046df8c5b85e305146 100644 --- a/apps/authentication/signals.py +++ b/apps/authentication/signals.py @@ -2,3 +2,5 @@ from django.dispatch import Signal post_create_openid_user = Signal(providing_args=('user',)) +post_auth_success = Signal(providing_args=('user', 'request')) +post_auth_failed = Signal(providing_args=('username', 'request', 'reason')) diff --git a/apps/authentication/signals_handlers.py b/apps/authentication/signals_handlers.py index d45ea1dfa45f856a066671bd10cf289bcc1a03b8..5f00c2b8dd87f91c7f600c0fd1ea187d6d57c2f4 100644 --- a/apps/authentication/signals_handlers.py +++ b/apps/authentication/signals_handlers.py @@ -2,9 +2,15 @@ from django.http.request import QueryDict from django.conf import settings from django.dispatch import receiver from django.contrib.auth.signals import user_logged_out +from django.utils import timezone from django_auth_ldap.backend import populate_user + +from common.utils import get_request_ip from .openid import client -from .signals import post_create_openid_user +from .tasks import write_login_log_async +from .signals import ( + post_create_openid_user, post_auth_success, post_auth_failed +) @receiver(user_logged_out) @@ -38,3 +44,36 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs): if user and user.name != 'admin': user.source = user.SOURCE_LDAP user.save() + + +def generate_data(username, request): + if not request.user.is_anonymous and request.user.is_app: + login_ip = request.data.get('remote_addr', None) + login_type = request.data.get('login_type', '') + user_agent = request.data.get('HTTP_USER_AGENT', '') + else: + login_ip = get_request_ip(request) + user_agent = request.META.get('HTTP_USER_AGENT', '') + login_type = 'W' + data = { + 'username': username, + 'ip': login_ip, + 'type': login_type, + 'user_agent': user_agent, + 'datetime': timezone.now() + } + return data + + +@receiver(post_auth_success) +def on_user_auth_success(sender, user, request, **kwargs): + data = generate_data(user.username, request) + data.update({'mfa': int(user.otp_enabled), 'status': True}) + write_login_log_async.delay(**data) + + +@receiver(post_auth_failed) +def on_user_auth_failed(sender, username, request, reason, **kwargs): + data = generate_data(username, request) + data.update({'reason': reason, 'status': False}) + write_login_log_async.delay(**data) diff --git a/apps/authentication/tasks.py b/apps/authentication/tasks.py new file mode 100644 index 0000000000000000000000000000000000000000..1b37c7a018cbcc8ebd474c4e02bc88bec598a5e6 --- /dev/null +++ b/apps/authentication/tasks.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# + +from celery import shared_task + +from .utils import write_login_log + + +@shared_task +def write_login_log_async(*args, **kwargs): + write_login_log(*args, **kwargs) diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index 4d4e6753a10c628a81c515152ee4b208d27f781d..9e3aae6d3ecb305096b6c469d76d573ffe3e2a79 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -2,15 +2,13 @@ # from django.urls import path -from authentication.openid import views +from .. import views app_name = 'authentication' urlpatterns = [ # openid - path('openid/login/', views.LoginView.as_view(), name='openid-login'), - path('openid/login/complete/', views.LoginCompleteView.as_view(), + path('openid/login/', views.OpenIDLoginView.as_view(), name='openid-login'), + path('openid/login/complete/', views.OpenIDLoginCompleteView.as_view(), name='openid-login-complete'), - - # other ] diff --git a/apps/authentication/utils.py b/apps/authentication/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..237730a21b0fb8bfef013df60f40a62f918d2e3d --- /dev/null +++ b/apps/authentication/utils.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# +from django.utils.translation import ugettext as _ +from common.utils import get_ip_city, validate_ip + + +def write_login_log(*args, **kwargs): + from users.models import LoginLog + default_city = _("Unknown") + ip = kwargs.get('ip', '') + if not (ip and validate_ip(ip)): + ip = ip[:15] + city = default_city + else: + city = get_ip_city(ip) or default_city + kwargs.update({'ip': ip, 'city': city}) + LoginLog.objects.create(**kwargs) + diff --git a/apps/authentication/views.py b/apps/authentication/views.py deleted file mode 100644 index 8b137891791fe96927ad78e64b0aad7bded08bdc..0000000000000000000000000000000000000000 --- a/apps/authentication/views.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/authentication/views/__init__.py b/apps/authentication/views/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1e55af13721a4232db7e103c57f5a374c80b1a60 --- /dev/null +++ b/apps/authentication/views/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# + +from .openid import * diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py new file mode 100644 index 0000000000000000000000000000000000000000..ba37e3a60364f7d3ba2d621adc71a10c7447b8e7 --- /dev/null +++ b/apps/authentication/views/login.py @@ -0,0 +1,212 @@ +# ~*~ coding: utf-8 ~*~ + +from __future__ import unicode_literals +import os +from django.core.cache import cache +from django.shortcuts import render +from django.utils import timezone +from django.contrib.auth import login as auth_login, logout as auth_logout +from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic import ListView +from django.core.files.storage import default_storage +from django.http import HttpResponseRedirect, HttpResponse +from django.shortcuts import reverse, redirect +from django.utils.decorators import method_decorator +from django.utils.translation import ugettext as _ +from django.views.decorators.cache import never_cache +from django.views.decorators.csrf import csrf_protect +from django.views.decorators.debug import sensitive_post_parameters +from django.views.generic.base import TemplateView +from django.views.generic.edit import FormView +from django.conf import settings +from formtools.wizard.views import SessionWizardView + +from common.utils import get_object_or_none, get_request_ip +from authentication.signals import post_auth_success, post_auth_failed +from users.models import User, LoginLog +from users.utils import send_reset_password_mail, check_otp_code, \ + 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, increase_login_failed_count, clean_failed_count +from users import forms + + +__all__ = [ + 'UserLoginView', 'UserLoginOtpView', 'UserLogoutView', + 'UserForgotPasswordView', 'UserForgotPasswordSendmailSuccessView', + 'UserResetPasswordView', 'UserResetPasswordSuccessView', + 'UserFirstLoginView', 'LoginLogListView' +] + + +@method_decorator(sensitive_post_parameters(), name='dispatch') +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(never_cache, name='dispatch') +class UserLoginView(FormView): + form_class = forms.UserLoginForm + form_class_captcha = forms.UserLoginCaptchaForm + redirect_field_name = 'next' + key_prefix_captcha = "_LOGIN_INVALID_{}" + + def get_template_names(self): + template_name = 'users/login.html' + if not settings.XPACK_ENABLED: + return template_name + + from xpack.plugins.license.models import License + if not License.has_valid_license(): + return template_name + + template_name = 'users/new_login.html' + return template_name + + def get(self, request, *args, **kwargs): + if request.user.is_staff: + return redirect(redirect_user_first_login_or_index( + request, self.redirect_field_name) + ) + request.session.set_test_cookie() + return super().get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + # limit login authentication + ip = get_request_ip(request) + username = self.request.POST.get('username') + if is_block_login(username, ip): + 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.")) + user = form.get_user() + # user password expired + if user.password_has_expired: + reason = LoginLog.REASON_PASSWORD_EXPIRED + self.send_auth_signal(success=False, username=user.username, reason=reason) + return self.render_to_response(self.get_context_data(password_expired=True)) + + set_tmp_user_to_cache(self.request, user) + username = form.cleaned_data.get('username') + ip = get_request_ip(self.request) + # 登陆成功,清除缓存计数 + clean_failed_count(username, ip) + return redirect(self.get_success_url()) + + def form_invalid(self, form): + # write login failed log + username = form.cleaned_data.get('username') + exist = User.objects.filter(username=username).first() + reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST + # limit user login failed count + ip = get_request_ip(self.request) + increase_login_failed_count(username, ip) + # show captcha + cache.set(self.key_prefix_captcha.format(ip), 1, 3600) + self.send_auth_signal(success=False, username=username, reason=reason) + + old_form = form + form = self.form_class_captcha(data=form.data) + form._errors = old_form.errors + return super().form_invalid(form) + + def get_form_class(self): + ip = get_request_ip(self.request) + if cache.get(self.key_prefix_captcha.format(ip)): + return self.form_class_captcha + else: + return self.form_class + + def get_success_url(self): + user = get_user_or_tmp_user(self.request) + + if user.otp_enabled and user.otp_secret_key: + # 1,2,mfa_setting & T + return reverse('users:login-otp') + elif user.otp_enabled and not user.otp_secret_key: + # 1,2,mfa_setting & F + return reverse('users:user-otp-enable-authentication') + elif not user.otp_enabled: + # 0 & T,F + auth_login(self.request, user) + self.send_auth_signal(success=True, user=user) + return redirect_user_first_login_or_index(self.request, self.redirect_field_name) + + def get_context_data(self, **kwargs): + context = { + 'demo_mode': os.environ.get("DEMO_MODE"), + 'AUTH_OPENID': settings.AUTH_OPENID, + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + def send_auth_signal(self, success=True, user=None, username='', reason=''): + if success: + post_auth_success.send(sender=self.__class__, user=user, request=self.request) + else: + post_auth_failed.send( + sender=self.__class__, username=username, + request=self.request, reason=reason + ) + + +class UserLoginOtpView(FormView): + template_name = 'users/login_otp.html' + form_class = forms.UserCheckOtpCodeForm + redirect_field_name = 'next' + + def form_valid(self, form): + user = get_user_or_tmp_user(self.request) + otp_code = form.cleaned_data.get('otp_code') + otp_secret_key = user.otp_secret_key + + if check_otp_code(otp_secret_key, otp_code): + auth_login(self.request, user) + self.send_auth_signal(success=True, user=user) + return redirect(self.get_success_url()) + else: + self.send_auth_signal( + success=False, username=user.username, + reason=LoginLog.REASON_MFA + ) + form.add_error('otp_code', _('MFA code invalid, or ntp sync server time')) + return super().form_invalid(form) + + def get_success_url(self): + return redirect_user_first_login_or_index(self.request, self.redirect_field_name) + + def send_auth_signal(self, success=True, user=None, username='', reason=''): + if success: + post_auth_success.send(sender=self.__class__, user=user, request=self.request) + else: + post_auth_failed.send( + sender=self.__class__, username=username, + request=self.request, reason=reason + ) + + +@method_decorator(never_cache, name='dispatch') +class UserLogoutView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + auth_logout(request) + next_uri = request.COOKIES.get("next") + if next_uri: + return redirect(next_uri) + response = super().get(request, *args, **kwargs) + return response + + def get_context_data(self, **kwargs): + context = { + 'title': _('Logout success'), + 'messages': _('Logout success, return login page'), + 'interval': 1, + 'redirect_url': reverse('users:login'), + 'auto_redirect': True, + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + + diff --git a/apps/authentication/openid/views.py b/apps/authentication/views/openid.py similarity index 64% rename from apps/authentication/openid/views.py rename to apps/authentication/views/openid.py index 9aeb0bf7b300709ee5f0deb981261c14ca173158..612cf2c685c63a6842e0a805b22ab85c907c6bb0 100644 --- a/apps/authentication/openid/views.py +++ b/apps/authentication/views/openid.py @@ -14,43 +14,36 @@ from django.http.response import ( HttpResponseRedirect ) -from . import client -from .models import Nonce -from users.models import LoginLog -from users.tasks import write_login_log_async -from common.utils import get_request_ip +from ..openid import client +from ..openid.models import Nonce +from ..signals import post_auth_success logger = logging.getLogger(__name__) -def get_base_site_url(): - return settings.BASE_SITE_URL +__all__ = ['OpenIDLoginView', 'OpenIDLoginCompleteView'] -class LoginView(RedirectView): +class OpenIDLoginView(RedirectView): def get_redirect_url(self, *args, **kwargs): + redirect_uri = settings.BASE_SITE_URL + \ + reverse("authentication:openid-login-complete") nonce = Nonce( - redirect_uri=get_base_site_url() + reverse( - "authentication:openid-login-complete"), - + redirect_uri=redirect_uri, next_path=self.request.GET.get('next') ) - cache.set(str(nonce.state), nonce, 24*3600) - self.request.session['openid_state'] = str(nonce.state) - authorization_url = client.openid_connect_client.\ authorization_url( redirect_uri=nonce.redirect_uri, scope='code', state=str(nonce.state) ) - return authorization_url -class LoginCompleteView(RedirectView): +class OpenIDLoginCompleteView(RedirectView): def get(self, request, *args, **kwargs): if 'error' in request.GET: @@ -79,24 +72,6 @@ class LoginCompleteView(RedirectView): return HttpResponseBadRequest() login(self.request, user) - - data = { - 'username': user.username, - 'mfa': int(user.otp_enabled), - 'reason': LoginLog.REASON_NOTHING, - 'status': True - } - self.write_login_log(data) - + post_auth_success.send(sender=self.__class__, user=user, request=self.request) return HttpResponseRedirect(nonce.next_path or '/') - def write_login_log(self, data): - login_ip = get_request_ip(self.request) - user_agent = self.request.META.get('HTTP_USER_AGENT', '') - tmp_data = { - 'ip': login_ip, - 'type': 'W', - 'user_agent': user_agent - } - data.update(tmp_data) - write_login_log_async.delay(**data) diff --git a/apps/common/utils/__init__.py b/apps/common/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8029430722fc8cb1e4a646bc16b4401a4bc7ea9a --- /dev/null +++ b/apps/common/utils/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# + +from .common import * +from .django import * +from .encode import * +from .http import * +from .ipip import * diff --git a/apps/common/utils.py b/apps/common/utils/common.py similarity index 54% rename from apps/common/utils.py rename to apps/common/utils/common.py index 85f47ae00570b1b27e7a08c1419eb59df2d66459..dcd7daf160831cf67a2c906f8928aa282e0644dc 100644 --- a/apps/common/utils.py +++ b/apps/common/utils/common.py @@ -1,104 +1,18 @@ # -*- coding: utf-8 -*- # import re -import sys from collections import OrderedDict -from six import string_types -import base64 -import os from itertools import chain import logging import datetime -import time -import hashlib -from email.utils import formatdate -import calendar -import threading -from io import StringIO import uuid from functools import wraps import copy - -import paramiko -import sshpubkeys -from itsdangerous import TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer, \ - BadSignature, SignatureExpired -from django.shortcuts import reverse as dj_reverse -from django.conf import settings -from django.utils import timezone +import ipaddress UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}') - - -def reverse(view_name, urlconf=None, args=None, kwargs=None, - current_app=None, external=False): - url = dj_reverse(view_name, urlconf=urlconf, args=args, - kwargs=kwargs, current_app=current_app) - - if external: - site_url = settings.SITE_URL - url = site_url.strip('/') + url - return url - - -def get_object_or_none(model, **kwargs): - try: - obj = model.objects.get(**kwargs) - except model.DoesNotExist: - return None - return obj - - -class Singleton(type): - def __init__(cls, *args, **kwargs): - cls.__instance = None - super().__init__(*args, **kwargs) - - def __call__(cls, *args, **kwargs): - if cls.__instance is None: - cls.__instance = super().__call__(*args, **kwargs) - return cls.__instance - else: - return cls.__instance - - -class Signer(metaclass=Singleton): - """用来加密,解密,和基于时间戳的方式验证token""" - def __init__(self, secret_key=None): - self.secret_key = secret_key - - def sign(self, value): - s = JSONWebSignatureSerializer(self.secret_key, algorithm_name='HS256') - return s.dumps(value).decode() - - def unsign(self, value): - if value is None: - return value - s = JSONWebSignatureSerializer(self.secret_key, algorithm_name='HS256') - try: - return s.loads(value) - except BadSignature: - return {} - - def sign_t(self, value, expires_in=3600): - s = TimedJSONWebSignatureSerializer(self.secret_key, expires_in=expires_in) - return str(s.dumps(value), encoding="utf8") - - def unsign_t(self, value): - s = TimedJSONWebSignatureSerializer(self.secret_key) - try: - return s.loads(value) - except (BadSignature, SignatureExpired): - return {} - - -def date_expired_default(): - try: - years = int(settings.DEFAULT_EXPIRED_YEARS) - except TypeError: - years = 70 - return timezone.now() + timezone.timedelta(days=365*years) +ipip_db = None def combine_seq(s1, s2, callback=None): @@ -146,88 +60,6 @@ def timesince(dt, since='', default="just now"): return default -def ssh_key_string_to_obj(text, password=None): - key = None - try: - key = paramiko.RSAKey.from_private_key(StringIO(text), password=password) - except paramiko.SSHException: - pass - - try: - key = paramiko.DSSKey.from_private_key(StringIO(text), password=password) - except paramiko.SSHException: - pass - return key - - -def ssh_pubkey_gen(private_key=None, username='jumpserver', hostname='localhost', password=None): - if isinstance(private_key, bytes): - private_key = private_key.decode("utf-8") - if isinstance(private_key, string_types): - private_key = ssh_key_string_to_obj(private_key, password=password) - if not isinstance(private_key, (paramiko.RSAKey, paramiko.DSSKey)): - raise IOError('Invalid private key') - - public_key = "%(key_type)s %(key_content)s %(username)s@%(hostname)s" % { - 'key_type': private_key.get_name(), - 'key_content': private_key.get_base64(), - 'username': username, - 'hostname': hostname, - } - return public_key - - -def ssh_key_gen(length=2048, type='rsa', password=None, username='jumpserver', hostname=None): - """Generate user ssh private and public key - - Use paramiko RSAKey generate it. - :return private key str and public key str - """ - - if hostname is None: - hostname = os.uname()[1] - - f = StringIO() - try: - if type == 'rsa': - private_key_obj = paramiko.RSAKey.generate(length) - elif type == 'dsa': - private_key_obj = paramiko.DSSKey.generate(length) - else: - raise IOError('SSH private key must be `rsa` or `dsa`') - private_key_obj.write_private_key(f, password=password) - private_key = f.getvalue() - public_key = ssh_pubkey_gen(private_key_obj, username=username, hostname=hostname) - return private_key, public_key - except IOError: - raise IOError('These is error when generate ssh key.') - - -def validate_ssh_private_key(text, password=None): - if isinstance(text, bytes): - try: - text = text.decode("utf-8") - except UnicodeDecodeError: - return False - - key = ssh_key_string_to_obj(text, password=password) - if key is None: - return False - else: - return True - - -def validate_ssh_public_key(text): - ssh = sshpubkeys.SSHKey(text) - try: - ssh.parse() - except (sshpubkeys.InvalidKeyException, UnicodeDecodeError): - return False - except NotImplementedError as e: - return False - return True - - def setattr_bulk(seq, key, value): def set_attr(obj): setattr(obj, key, value) @@ -243,70 +75,6 @@ def set_or_append_attr_bulk(seq, key, value): setattr(obj, key, value) -def content_md5(data): - """计算data的MD5值,经过Base64编码并返回str类型。 - - 返回值可以直接作为HTTP Content-Type头部的值 - """ - if isinstance(data, str): - data = hashlib.md5(data.encode('utf-8')) - value = base64.b64encode(data.hexdigest().encode('utf-8')) - return value.decode('utf-8') - - -_STRPTIME_LOCK = threading.Lock() - -_GMT_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" -_ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S.000Z" - - -def to_unixtime(time_string, format_string): - time_string = time_string.decode("ascii") - with _STRPTIME_LOCK: - return int(calendar.timegm(time.strptime(time_string, format_string))) - - -def http_date(timeval=None): - """返回符合HTTP标准的GMT时间字符串,用strftime的格式表示就是"%a, %d %b %Y %H:%M:%S GMT"。 - 但不能使用strftime,因为strftime的结果是和locale相关的。 - """ - return formatdate(timeval, usegmt=True) - - -def http_to_unixtime(time_string): - """把HTTP Date格式的字符串转换为UNIX时间(自1970年1月1日UTC零点的秒数)。 - - HTTP Date形如 `Sat, 05 Dec 2015 11:10:29 GMT` 。 - """ - return to_unixtime(time_string, _GMT_FORMAT) - - -def iso8601_to_unixtime(time_string): - """把ISO8601时间字符串(形如,2012-02-24T06:07:48.000Z)转换为UNIX时间,精确到秒。""" - return to_unixtime(time_string, _ISO8601_FORMAT) - - -def make_signature(access_key_secret, date=None): - if isinstance(date, bytes): - date = bytes.decode(date) - if isinstance(date, int): - date_gmt = http_date(date) - elif date is None: - date_gmt = http_date(int(time.time())) - else: - date_gmt = date - - data = str(access_key_secret) + "\n" + date_gmt - return content_md5(data) - - -def encrypt_password(password, salt=None): - from passlib.hash import sha512_crypt - if password: - return sha512_crypt.using(rounds=5000).hash(password, salt=salt) - return None - - def capacity_convert(size, expect='auto', rate=1000): """ :param size: '100MB', '1G' @@ -374,11 +142,6 @@ def is_uuid(seq): return True -def get_signer(): - signer = Signer(settings.SECRET_KEY) - return signer - - def get_request_ip(request): x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',') if x_forwarded_for and x_forwarded_for[0]: @@ -388,22 +151,13 @@ def get_request_ip(request): return login_ip -def get_command_storage_setting(): - default = settings.DEFAULT_TERMINAL_COMMAND_STORAGE - value = settings.TERMINAL_COMMAND_STORAGE - if not value: - return default - value.update(default) - return value - - -def get_replay_storage_setting(): - default = settings.DEFAULT_TERMINAL_REPLAY_STORAGE - value = settings.TERMINAL_REPLAY_STORAGE - if not value: - return default - value.update(default) - return value +def validate_ip(ip): + try: + ipaddress.ip_address(ip) + return True + except ValueError: + pass + return False def with_cache(func): @@ -537,4 +291,4 @@ class LocalProxy(object): __rmod__ = lambda x, o: o % x._get_current_object() __rdivmod__ = lambda x, o: x._get_current_object().__rdivmod__(o) __copy__ = lambda x: copy.copy(x._get_current_object()) - __deepcopy__ = lambda x, memo: copy.deepcopy(x._get_current_object(), memo) \ No newline at end of file + __deepcopy__ = lambda x, memo: copy.deepcopy(x._get_current_object(), memo) diff --git a/apps/common/utils/django.py b/apps/common/utils/django.py new file mode 100644 index 0000000000000000000000000000000000000000..9a0af8f7dab0dd554433259b7b52d9cb384d6852 --- /dev/null +++ b/apps/common/utils/django.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# +import re +from django.shortcuts import reverse as dj_reverse +from django.conf import settings +from django.utils import timezone + + +UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}') + + +def reverse(view_name, urlconf=None, args=None, kwargs=None, + current_app=None, external=False): + url = dj_reverse(view_name, urlconf=urlconf, args=args, + kwargs=kwargs, current_app=current_app) + + if external: + site_url = settings.SITE_URL + url = site_url.strip('/') + url + return url + + +def get_object_or_none(model, **kwargs): + try: + obj = model.objects.get(**kwargs) + except model.DoesNotExist: + return None + return obj + + +def date_expired_default(): + try: + years = int(settings.DEFAULT_EXPIRED_YEARS) + except TypeError: + years = 70 + return timezone.now() + timezone.timedelta(days=365*years) + + +def get_command_storage_setting(): + default = settings.DEFAULT_TERMINAL_COMMAND_STORAGE + value = settings.TERMINAL_COMMAND_STORAGE + if not value: + return default + value.update(default) + return value + + +def get_replay_storage_setting(): + default = settings.DEFAULT_TERMINAL_REPLAY_STORAGE + value = settings.TERMINAL_REPLAY_STORAGE + if not value: + return default + value.update(default) + return value diff --git a/apps/common/utils/encode.py b/apps/common/utils/encode.py new file mode 100644 index 0000000000000000000000000000000000000000..aefa19238c5891bd6316ae8bc26c66ea8e1bb43f --- /dev/null +++ b/apps/common/utils/encode.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +# +import re +from six import string_types +import base64 +import os +import time +import hashlib +from io import StringIO + +import paramiko +import sshpubkeys +from itsdangerous import ( + TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer, + BadSignature, SignatureExpired +) +from django.conf import settings + +from .http import http_date + + +UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}') + + +class Singleton(type): + def __init__(cls, *args, **kwargs): + cls.__instance = None + super().__init__(*args, **kwargs) + + def __call__(cls, *args, **kwargs): + if cls.__instance is None: + cls.__instance = super().__call__(*args, **kwargs) + return cls.__instance + else: + return cls.__instance + + +class Signer(metaclass=Singleton): + """用来加密,解密,和基于时间戳的方式验证token""" + def __init__(self, secret_key=None): + self.secret_key = secret_key + + def sign(self, value): + s = JSONWebSignatureSerializer(self.secret_key, algorithm_name='HS256') + return s.dumps(value).decode() + + def unsign(self, value): + if value is None: + return value + s = JSONWebSignatureSerializer(self.secret_key, algorithm_name='HS256') + try: + return s.loads(value) + except BadSignature: + return {} + + def sign_t(self, value, expires_in=3600): + s = TimedJSONWebSignatureSerializer(self.secret_key, expires_in=expires_in) + return str(s.dumps(value), encoding="utf8") + + def unsign_t(self, value): + s = TimedJSONWebSignatureSerializer(self.secret_key) + try: + return s.loads(value) + except (BadSignature, SignatureExpired): + return {} + + +def ssh_key_string_to_obj(text, password=None): + key = None + try: + key = paramiko.RSAKey.from_private_key(StringIO(text), password=password) + except paramiko.SSHException: + pass + + try: + key = paramiko.DSSKey.from_private_key(StringIO(text), password=password) + except paramiko.SSHException: + pass + return key + + +def ssh_pubkey_gen(private_key=None, username='jumpserver', hostname='localhost', password=None): + if isinstance(private_key, bytes): + private_key = private_key.decode("utf-8") + if isinstance(private_key, string_types): + private_key = ssh_key_string_to_obj(private_key, password=password) + if not isinstance(private_key, (paramiko.RSAKey, paramiko.DSSKey)): + raise IOError('Invalid private key') + + public_key = "%(key_type)s %(key_content)s %(username)s@%(hostname)s" % { + 'key_type': private_key.get_name(), + 'key_content': private_key.get_base64(), + 'username': username, + 'hostname': hostname, + } + return public_key + + +def ssh_key_gen(length=2048, type='rsa', password=None, username='jumpserver', hostname=None): + """Generate user ssh private and public key + + Use paramiko RSAKey generate it. + :return private key str and public key str + """ + + if hostname is None: + hostname = os.uname()[1] + + f = StringIO() + try: + if type == 'rsa': + private_key_obj = paramiko.RSAKey.generate(length) + elif type == 'dsa': + private_key_obj = paramiko.DSSKey.generate(length) + else: + raise IOError('SSH private key must be `rsa` or `dsa`') + private_key_obj.write_private_key(f, password=password) + private_key = f.getvalue() + public_key = ssh_pubkey_gen(private_key_obj, username=username, hostname=hostname) + return private_key, public_key + except IOError: + raise IOError('These is error when generate ssh key.') + + +def validate_ssh_private_key(text, password=None): + if isinstance(text, bytes): + try: + text = text.decode("utf-8") + except UnicodeDecodeError: + return False + + key = ssh_key_string_to_obj(text, password=password) + if key is None: + return False + else: + return True + + +def validate_ssh_public_key(text): + ssh = sshpubkeys.SSHKey(text) + try: + ssh.parse() + except (sshpubkeys.InvalidKeyException, UnicodeDecodeError): + return False + except NotImplementedError as e: + return False + return True + + +def content_md5(data): + """计算data的MD5值,经过Base64编码并返回str类型。 + + 返回值可以直接作为HTTP Content-Type头部的值 + """ + if isinstance(data, str): + data = hashlib.md5(data.encode('utf-8')) + value = base64.b64encode(data.hexdigest().encode('utf-8')) + return value.decode('utf-8') + + +def make_signature(access_key_secret, date=None): + if isinstance(date, bytes): + date = bytes.decode(date) + if isinstance(date, int): + date_gmt = http_date(date) + elif date is None: + date_gmt = http_date(int(time.time())) + else: + date_gmt = date + + data = str(access_key_secret) + "\n" + date_gmt + return content_md5(data) + + +def encrypt_password(password, salt=None): + from passlib.hash import sha512_crypt + if password: + return sha512_crypt.using(rounds=5000).hash(password, salt=salt) + return None + + +def get_signer(): + signer = Signer(settings.SECRET_KEY) + return signer diff --git a/apps/common/utils/http.py b/apps/common/utils/http.py new file mode 100644 index 0000000000000000000000000000000000000000..185397881e1ec66c80411a0dece2df926895117c --- /dev/null +++ b/apps/common/utils/http.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# +import time +from email.utils import formatdate +import calendar +import threading + +_STRPTIME_LOCK = threading.Lock() + +_GMT_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" +_ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S.000Z" + + +def to_unixtime(time_string, format_string): + time_string = time_string.decode("ascii") + with _STRPTIME_LOCK: + return int(calendar.timegm(time.strptime(time_string, format_string))) + + +def http_date(timeval=None): + """返回符合HTTP标准的GMT时间字符串,用strftime的格式表示就是"%a, %d %b %Y %H:%M:%S GMT"。 + 但不能使用strftime,因为strftime的结果是和locale相关的。 + """ + return formatdate(timeval, usegmt=True) + + +def http_to_unixtime(time_string): + """把HTTP Date格式的字符串转换为UNIX时间(自1970年1月1日UTC零点的秒数)。 + + HTTP Date形如 `Sat, 05 Dec 2015 11:10:29 GMT` 。 + """ + return to_unixtime(time_string, _GMT_FORMAT) + + +def iso8601_to_unixtime(time_string): + """把ISO8601时间字符串(形如,2012-02-24T06:07:48.000Z)转换为UNIX时间,精确到秒。""" + return to_unixtime(time_string, _ISO8601_FORMAT) diff --git a/apps/common/utils/ipip/__init__.py b/apps/common/utils/ipip/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..864ac8d4424d7742eb21919793ed336f35a6bda2 --- /dev/null +++ b/apps/common/utils/ipip/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# +from .ipdb import * diff --git a/apps/common/utils/ipip/ipdb.py b/apps/common/utils/ipip/ipdb.py new file mode 100644 index 0000000000000000000000000000000000000000..e17fb523ce60ab8f01aabd24ec1b1e09e7d25874 --- /dev/null +++ b/apps/common/utils/ipip/ipdb.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# +import os + +import ipdb + +ipip_db = None + + +def get_ip_city(ip): + global ipip_db + if ipip_db is None: + ipip_db_path = os.path.join(os.path.dirname(__file__), 'ipipfree.ipdb') + ipip_db = ipdb.City(ipip_db_path) + info = list(set(ipip_db.find(ip, 'CN'))) + if '' in info: + info.remove('') + return ' '.join(info) \ No newline at end of file diff --git a/apps/common/utils/ipip/ipipfree.ipdb b/apps/common/utils/ipip/ipipfree.ipdb new file mode 100755 index 0000000000000000000000000000000000000000..911e1ac33317d0356338b60b2aceb1f08239373a Binary files /dev/null and b/apps/common/utils/ipip/ipipfree.ipdb differ diff --git a/apps/users/api/auth.py b/apps/users/api/auth.py index c4ed7ff3a826e8686a6074bc72e7ce895b577eac..25a7ee40a8620442898f8cedaf960a4c926f45de 100644 --- a/apps/users/api/auth.py +++ b/apps/users/api/auth.py @@ -14,8 +14,8 @@ from rest_framework.views import APIView from common.utils import get_logger, get_request_ip from common.permissions import IsOrgAdminOrAppUser from orgs.mixins import RootOrgViewMixin +from authentication.signals import post_auth_success, post_auth_failed from ..serializers import UserSerializer -from ..tasks import write_login_log_async from ..models import User, LoginLog from ..utils import check_user_valid, check_otp_code, \ increase_login_failed_count, is_block_login, \ @@ -46,37 +46,22 @@ class UserAuthApi(RootOrgViewMixin, APIView): username = request.data.get('username', '') exist = User.objects.filter(username=username).first() reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST - data = { - 'username': username, - 'mfa': LoginLog.MFA_UNKNOWN, - 'reason': reason, - 'status': False - } - self.write_login_log(request, data) + self.send_auth_signal(success=False, username=username, reason=reason) increase_login_failed_count(username, ip) return Response({'msg': msg}, status=401) if user.password_has_expired: - data = { - 'username': user.username, - 'mfa': int(user.otp_enabled), - 'reason': LoginLog.REASON_PASSWORD_EXPIRED, - 'status': False - } - self.write_login_log(request, data) + self.send_auth_signal( + success=False, username=username, + reason=LoginLog.REASON_PASSWORD_EXPIRED + ) msg = _("The user {} password has expired, please update.".format( user.username)) logger.info(msg) 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) + self.send_auth_signal(success=True, user=user) # 登陆成功,清除原来的缓存计数 clean_failed_count(username, ip) token = user.create_bearer_token(request) @@ -108,22 +93,14 @@ class UserAuthApi(RootOrgViewMixin, APIView): ) return user, msg - @staticmethod - 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', '') - - if not login_ip: - login_ip = get_request_ip(request) - - tmp_data = { - 'ip': login_ip, - 'type': login_type, - 'user_agent': user_agent, - } - data.update(tmp_data) - write_login_log_async.delay(**data) + def send_auth_signal(self, success=True, user=None, username='', reason=''): + if success: + post_auth_success.send(sender=self.__class__, user=user, request=self.request) + else: + post_auth_failed.send( + sender=self.__class__, username=username, + request=self.request, reason=reason + ) class UserConnectionTokenApi(RootOrgViewMixin, APIView): @@ -197,52 +174,25 @@ class UserOtpAuthApi(RootOrgViewMixin, APIView): def post(self, request): otp_code = request.data.get('otp_code', '') seed = request.data.get('seed', '') - user = cache.get(seed, None) if not user: return Response( {'msg': _('Please verify the user name and password first')}, 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) + self.send_auth_signal(success=False, username=user.username, reason=LoginLog.REASON_MFA) return Response({'msg': _('MFA certification failed')}, status=401) - - data = { - 'username': user.username, - 'mfa': int(user.otp_enabled), - 'reason': LoginLog.REASON_NOTHING, - 'status': True - } - self.write_login_log(request, data) + self.send_auth_signal(success=True, user=user) token = user.create_bearer_token(request) - return Response( - { - 'token': token, - 'user': self.serializer_class(user).data - } - ) + data = {'token': token, 'user': self.serializer_class(user).data} + return Response(data) - @staticmethod - 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', '') - - if not login_ip: - login_ip = get_request_ip(request) - - tmp_data = { - 'ip': login_ip, - 'type': login_type, - 'user_agent': user_agent - } - data.update(tmp_data) - write_login_log_async.delay(**data) + def send_auth_signal(self, success=True, user=None, username='', reason=''): + if success: + post_auth_success.send(sender=self.__class__, user=user, request=self.request) + else: + post_auth_failed.send( + sender=self.__class__, username=username, + request=self.request, reason=reason + ) diff --git a/apps/users/models/authentication.py b/apps/users/models/authentication.py index 9bf1fdcb628b4ab40d131445a82cb1edeedc39c8..6090dfc863af0c1f7282d9b261b566052b89fd58 100644 --- a/apps/users/models/authentication.py +++ b/apps/users/models/authentication.py @@ -4,6 +4,7 @@ import uuid from django.db import models +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from rest_framework.authtoken.models import Token from .user import User @@ -82,7 +83,7 @@ class LoginLog(models.Model): 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(default=timezone.now, verbose_name=_('Date login')) class Meta: ordering = ['-datetime', 'username'] diff --git a/apps/users/signals.py b/apps/users/signals.py index 324cd162741a97a4f3693f00ad4b89a72606e7fa..b9084b7dc64d0b0b465c4bc988a39e33e9146966 100644 --- a/apps/users/signals.py +++ b/apps/users/signals.py @@ -2,4 +2,3 @@ from django.dispatch import Signal post_user_create = Signal(providing_args=('user',)) - diff --git a/apps/users/tasks.py b/apps/users/tasks.py index 769d01ed7d328396b061f69be18588da4540afe9..78d9fa9223c409f6176b548063df73176eeb367b 100644 --- a/apps/users/tasks.py +++ b/apps/users/tasks.py @@ -7,17 +7,12 @@ from ops.celery.utils import create_or_update_celery_periodic_tasks from ops.celery.decorator import after_app_ready_start from .models import User from common.utils import get_logger -from .utils import write_login_log, send_password_expiration_reminder_mail +from .utils import send_password_expiration_reminder_mail logger = get_logger(__file__) -@shared_task -def write_login_log_async(*args, **kwargs): - write_login_log(*args, **kwargs) - - @shared_task def check_password_expired(): users = User.objects.exclude(role=User.ROLE_APP) diff --git a/apps/users/utils.py b/apps/users/utils.py index db69fd0933ef1913c8b505341f286c716033043c..aaa236bd81393592814bb2280e93a23f45edd84f 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -7,7 +7,6 @@ import pyotp import base64 import logging -import requests import ipaddress from django.http import Http404 from django.conf import settings @@ -18,7 +17,7 @@ from django.core.cache import cache from datetime import datetime from common.tasks import send_mail_async -from common.utils import reverse, get_object_or_none +from common.utils import reverse, get_object_or_none, get_ip_city from .models import User, LoginLog @@ -199,51 +198,6 @@ def check_user_valid(**kwargs): return None, _('Password or SSH public key invalid') -def validate_ip(ip): - try: - ipaddress.ip_address(ip) - return True - except ValueError: - pass - return False - - -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) - kwargs.update({'ip': ip, 'city': city}) - LoginLog.objects.create(**kwargs) - - -def get_ip_city(ip, timeout=10): - # Taobao ip api: http://ip.taobao.com/service/getIpInfo.php?ip=8.8.8.8 - # Sina ip api: http://int.dpool.sina.com.cn/iplookup/iplookup.php?ip=8.8.8.8&format=json - - url = 'http://ip.taobao.com/service/getIpInfo.php?ip=%s' % ip - try: - r = requests.get(url, timeout=timeout) - except: - r = None - city = 'Unknown' - if r and r.status_code == 200: - try: - data = r.json() - if not isinstance(data, int) and data['code'] == 0: - country = data['data']['country'] - _city = data['data']['city'] - if country == 'XX': - city = _city - else: - city = ' '.join([country, _city]) - except ValueError: - pass - return city - - def get_user_or_tmp_user(request): user = request.user tmp_user = get_tmp_user_from_cache(request) diff --git a/apps/users/views/login.py b/apps/users/views/login.py index 3b960b7a54ffaf53e35c1b1f2dd1d6cc6b7053da..cafcf2b0d3d9307717e1472fc422dc31888fc3d7 100644 --- a/apps/users/views/login.py +++ b/apps/users/views/login.py @@ -1,244 +1,35 @@ # ~*~ coding: utf-8 ~*~ from __future__ import unicode_literals -import os -from django.core.cache import cache from django.shortcuts import render -from django.contrib.auth import login as auth_login, logout as auth_logout from django.contrib.auth.mixins import LoginRequiredMixin -from django.views.generic import ListView +from django.views.generic import RedirectView from django.core.files.storage import default_storage -from django.http import HttpResponseRedirect, HttpResponse +from django.http import HttpResponseRedirect from django.shortcuts import reverse, redirect -from django.utils.decorators import method_decorator from django.utils.translation import ugettext as _ -from django.views.decorators.cache import never_cache -from django.views.decorators.csrf import csrf_protect -from django.views.decorators.debug import sensitive_post_parameters from django.views.generic.base import TemplateView -from django.views.generic.edit import FormView from django.conf import settings +from django.urls import reverse_lazy from formtools.wizard.views import SessionWizardView -from common.utils import get_object_or_none, get_request_ip -from ..models import User, LoginLog -from ..utils import send_reset_password_mail, check_otp_code, \ - 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, increase_login_failed_count, clean_failed_count -from ..tasks import write_login_log_async +from common.utils import get_object_or_none +from ..models import User +from ..utils import ( + send_reset_password_mail, get_password_check_rules, check_password_rules +) from .. import forms __all__ = [ - 'UserLoginView', 'UserLoginOtpView', 'UserLogoutView', - 'UserForgotPasswordView', 'UserForgotPasswordSendmailSuccessView', - 'UserResetPasswordView', 'UserResetPasswordSuccessView', - 'UserFirstLoginView', 'LoginLogListView' + 'UserLoginView', 'UserForgotPasswordSendmailSuccessView', + 'UserResetPasswordSuccessView', 'UserResetPasswordSuccessView', + 'UserResetPasswordView', 'UserForgotPasswordView', ] -@method_decorator(sensitive_post_parameters(), name='dispatch') -@method_decorator(csrf_protect, name='dispatch') -@method_decorator(never_cache, name='dispatch') -class UserLoginView(FormView): - form_class = forms.UserLoginForm - form_class_captcha = forms.UserLoginCaptchaForm - redirect_field_name = 'next' - key_prefix_captcha = "_LOGIN_INVALID_{}" - - def get_template_names(self): - template_name = 'users/login.html' - if not settings.XPACK_ENABLED: - return template_name - - from xpack.plugins.license.models import License - if not License.has_valid_license(): - return template_name - - template_name = 'users/new_login.html' - return template_name - - def get(self, request, *args, **kwargs): - if request.user.is_staff: - return redirect(redirect_user_first_login_or_index( - request, self.redirect_field_name) - ) - request.session.set_test_cookie() - return super().get(request, *args, **kwargs) - - def post(self, request, *args, **kwargs): - # limit login authentication - ip = get_request_ip(request) - username = self.request.POST.get('username') - if is_block_login(username, ip): - 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.")) - - user = form.get_user() - - # user password expired - if user.password_has_expired: - data = { - 'username': user.username, - 'mfa': int(user.otp_enabled), - 'reason': LoginLog.REASON_PASSWORD_EXPIRED, - 'status': False - } - self.write_login_log(data) - return self.render_to_response(self.get_context_data(password_expired=True)) - - set_tmp_user_to_cache(self.request, user) - username = form.cleaned_data.get('username') - ip = get_request_ip(self.request) - # 登陆成功,清除缓存计数 - clean_failed_count(username, ip) - return redirect(self.get_success_url()) - - def form_invalid(self, form): - # write login failed log - username = form.cleaned_data.get('username') - exist = User.objects.filter(username=username).first() - reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST - data = { - 'username': username, - 'mfa': LoginLog.MFA_UNKNOWN, - 'reason': reason, - 'status': False - } - self.write_login_log(data) - - # limit user login failed count - ip = get_request_ip(self.request) - increase_login_failed_count(username, ip) - - # 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 - return super().form_invalid(form) - - def get_form_class(self): - ip = get_request_ip(self.request) - if cache.get(self.key_prefix_captcha.format(ip)): - return self.form_class_captcha - else: - return self.form_class - - def get_success_url(self): - user = get_user_or_tmp_user(self.request) - - if user.otp_enabled and user.otp_secret_key: - # 1,2,mfa_setting & T - return reverse('users:login-otp') - elif user.otp_enabled and not user.otp_secret_key: - # 1,2,mfa_setting & F - return reverse('users:user-otp-enable-authentication') - elif not user.otp_enabled: - # 0 & T,F - auth_login(self.request, user) - 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): - context = { - 'demo_mode': os.environ.get("DEMO_MODE"), - 'AUTH_OPENID': settings.AUTH_OPENID, - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - def write_login_log(self, data): - login_ip = get_request_ip(self.request) - user_agent = self.request.META.get('HTTP_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): - template_name = 'users/login_otp.html' - form_class = forms.UserCheckOtpCodeForm - redirect_field_name = 'next' - - def form_valid(self, form): - user = get_user_or_tmp_user(self.request) - otp_code = form.cleaned_data.get('otp_code') - otp_secret_key = user.otp_secret_key - - if check_otp_code(otp_secret_key, otp_code): - auth_login(self.request, user) - 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, or ntp sync server time')) - 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, data): - login_ip = get_request_ip(self.request) - user_agent = self.request.META.get('HTTP_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') -class UserLogoutView(TemplateView): - template_name = 'flash_message_standalone.html' - - def get(self, request, *args, **kwargs): - auth_logout(request) - next_uri = request.COOKIES.get("next") - if next_uri: - return redirect(next_uri) - response = super().get(request, *args, **kwargs) - return response - - def get_context_data(self, **kwargs): - context = { - 'title': _('Logout success'), - 'messages': _('Logout success, return login page'), - 'interval': 1, - 'redirect_url': reverse('users:login'), - 'auto_redirect': True, - } - kwargs.update(context) - return super().get_context_data(**kwargs) +class UserLoginView(RedirectView): + urls = reverse_lazy('authentication:login') class UserForgotPasswordView(TemplateView): @@ -386,8 +177,3 @@ class UserFirstLoginView(LoginRequiredMixin, SessionWizardView): form.fields["otp_level"].initial = self.request.user.otp_level return form - - -class LoginLogListView(ListView): - def get(self, request, *args, **kwargs): - return redirect(reverse('audits:login-log-list')) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 1c677e94fe7555b497b6c5910170ca868014d285..3ed0c349701b254ce788611e64f7757a3561935a 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -17,7 +17,7 @@ decorator==4.1.2 Django==2.1.7 django-auth-ldap==1.7.0 django-bootstrap3==9.1.0 -django-celery-beat==1.1.1 +django-celery-beat==1.4.0 django-filter==2.0.0 django-formtools==2.1 django-ranged-response==0.2.0 @@ -79,3 +79,4 @@ rest_condition==1.0.3 python-ldap==3.1.0 tencentcloud-sdk-python==3.0.40 django-radius==1.3.3 +ipip-ipdb==1.2.1