From 6ce9815d51613302b06122681e83b66992c6c3f8 Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 5 Nov 2019 18:46:29 +0800 Subject: [PATCH] =?UTF-8?q?[Update]=20=E5=9F=BA=E6=9C=AC=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/audits/models.py | 9 + apps/audits/signals_handler.py | 43 +- apps/audits/tasks.py | 6 + .../templates/audits/login_log_list.html | 2 +- apps/audits/utils.py | 18 +- apps/authentication/api/auth.py | 172 +------- apps/authentication/api/login_confirm.py | 33 +- apps/authentication/api/mfa.py | 47 ++- apps/authentication/api/token.py | 128 +----- apps/authentication/errors.py | 189 +++++++-- apps/authentication/forms.py | 38 +- apps/authentication/mixins.py | 127 ++++++ apps/authentication/serializers.py | 67 ++- apps/authentication/signals_handlers.py | 37 +- apps/authentication/tasks.py | 9 - .../templates/authentication/login.html | 27 +- .../authentication/login_wait_confirm.html | 4 +- .../templates/authentication/xpack_login.html | 39 +- apps/authentication/urls/api_urls.py | 3 +- apps/authentication/utils.py | 47 +-- apps/authentication/views/__init__.py | 2 +- apps/authentication/views/login.py | 124 ++---- apps/authentication/views/mfa.py | 25 ++ apps/authentication/views/utils.py | 8 + apps/common/utils/common.py | 8 + apps/locale/zh/LC_MESSAGES/django.mo | Bin 84548 -> 84603 bytes apps/locale/zh/LC_MESSAGES/django.po | 399 +++++++++--------- .../perms/asset_permission_create_update.html | 14 +- .../remote_app_permission_create_update.html | 4 +- apps/static/js/jumpserver.js | 28 ++ apps/templates/_head_css_js.html | 2 +- apps/users/models/user.py | 8 +- apps/users/templates/users/_user.html | 13 +- apps/users/templates/users/user_detail.html | 2 +- apps/users/urls/api_urls.py | 3 - apps/users/utils.py | 8 +- 36 files changed, 874 insertions(+), 819 deletions(-) create mode 100644 apps/authentication/mixins.py create mode 100644 apps/authentication/views/mfa.py create mode 100644 apps/authentication/views/utils.py diff --git a/apps/audits/models.py b/apps/audits/models.py index 5b53d1c85..eb3489a7c 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -110,5 +110,14 @@ class UserLoginLog(models.Model): login_logs = login_logs.filter(username__in=username_list) return login_logs + @property + def reason_display(self): + from authentication.errors import reason_choices, old_reason_choices + reason = reason_choices.get(self.reason) + if reason: + return reason + reason = old_reason_choices.get(self.reason, self.reason) + return reason + class Meta: ordering = ['-datetime', 'username'] diff --git a/apps/audits/signals_handler.py b/apps/audits/signals_handler.py index 0f201b464..d2a728f95 100644 --- a/apps/audits/signals_handler.py +++ b/apps/audits/signals_handler.py @@ -4,15 +4,18 @@ from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from django.db import transaction +from django.utils import timezone from rest_framework.renderers import JSONRenderer +from rest_framework.request import Request from jumpserver.utils import current_request from common.utils import get_request_ip, get_logger, get_syslogger from users.models import User +from authentication.signals import post_auth_failed, post_auth_success from terminal.models import Session, Command from terminal.backends.command.serializers import SessionCommandSerializer -from . import models -from . import serializers +from . import models, serializers +from .tasks import write_login_log_async logger = get_logger(__name__) sys_logger = get_syslogger("audits") @@ -99,3 +102,39 @@ def on_audits_log_create(sender, instance=None, **kwargs): data = json_render.render(s.data).decode(errors='ignore') msg = "{} - {}".format(category, data) sys_logger.info(msg) + + +def generate_data(username, request): + user_agent = request.META.get('HTTP_USER_AGENT', '') + + if isinstance(request, Request): + login_ip = request.data.get('remote_addr', None) + login_type = request.data.get('login_type', '') + else: + login_ip = get_request_ip(request) + 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): + logger.debug('User login success: {}'.format(user.username)) + 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): + logger.debug('User login failed: {}'.format(username)) + data = generate_data(username, request) + data.update({'reason': reason, 'status': False}) + write_login_log_async.delay(**data) diff --git a/apps/audits/tasks.py b/apps/audits/tasks.py index 90d8f47db..5f8da0bc0 100644 --- a/apps/audits/tasks.py +++ b/apps/audits/tasks.py @@ -7,6 +7,7 @@ from celery import shared_task from ops.celery.decorator import register_as_period_task from .models import UserLoginLog +from .utils import write_login_log @register_as_period_task(interval=3600*24) @@ -19,3 +20,8 @@ def clean_login_log_period(): days = 90 expired_day = now - datetime.timedelta(days=days) UserLoginLog.objects.filter(datetime__lt=expired_day).delete() + + +@shared_task +def write_login_log_async(*args, **kwargs): + write_login_log(*args, **kwargs) diff --git a/apps/audits/templates/audits/login_log_list.html b/apps/audits/templates/audits/login_log_list.html index 151fccb13..0533f8aef 100644 --- a/apps/audits/templates/audits/login_log_list.html +++ b/apps/audits/templates/audits/login_log_list.html @@ -78,7 +78,7 @@ {{ login_log.ip }} {{ login_log.city }} {{ login_log.get_mfa_display }} - {% trans login_log.reason %} + {{ login_log.reason_display }} {{ login_log.get_status_display }} {{ login_log.datetime }} diff --git a/apps/audits/utils.py b/apps/audits/utils.py index 36c54e81b..8710bfc0f 100644 --- a/apps/audits/utils.py +++ b/apps/audits/utils.py @@ -1,6 +1,9 @@ import csv import codecs from django.http import HttpResponse +from django.utils.translation import ugettext as _ + +from common.utils import validate_ip, get_ip_city def get_excel_response(filename): @@ -19,4 +22,17 @@ def write_content_to_excel(response, header=None, login_logs=None, fields=None): for log in login_logs: data = [getattr(log, field.name) for field in fields] writer.writerow(data) - return response \ No newline at end of file + return response + + +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 + else: + city = get_ip_city(ip) or default_city + kwargs.update({'ip': ip, 'city': city}) + UserLoginLog.objects.create(**kwargs) diff --git a/apps/authentication/api/auth.py b/apps/authentication/api/auth.py index c7c78422a..cc77058ee 100644 --- a/apps/authentication/api/auth.py +++ b/apps/authentication/api/auth.py @@ -1,114 +1,25 @@ # -*- coding: utf-8 -*- # import uuid -import time from django.core.cache import cache -from django.urls import reverse from django.shortcuts import get_object_or_404 -from django.utils.translation import ugettext as _ from rest_framework.permissions import AllowAny from rest_framework.response import Response -from rest_framework.generics import CreateAPIView from rest_framework.views import APIView -from common.utils import get_logger, get_request_ip, get_object_or_none -from common.permissions import IsOrgAdminOrAppUser, IsValidUser +from common.utils import get_logger +from common.permissions import IsOrgAdminOrAppUser from orgs.mixins.api import RootOrgViewMixin -from users.serializers import UserSerializer from users.models import User from assets.models import Asset, SystemUser -from users.utils import ( - check_otp_code, increase_login_failed_count, - is_block_login, clean_failed_count -) -from .. import errors -from ..utils import check_user_valid -from ..serializers import OtpVerifySerializer -from ..signals import post_auth_success, post_auth_failed logger = get_logger(__name__) __all__ = [ - 'UserAuthApi', 'UserConnectionTokenApi', 'UserOtpAuthApi', - 'UserOtpVerifyApi', 'UserOrderAcceptAuthApi', + 'UserConnectionTokenApi', ] -class UserAuthApi(RootOrgViewMixin, APIView): - permission_classes = (AllowAny,) - serializer_class = UserSerializer - - def get_serializer_context(self): - return { - 'request': self.request, - 'view': self - } - - def get_serializer(self, *args, **kwargs): - kwargs['context'] = self.get_serializer_context() - return self.serializer_class(*args, **kwargs) - - def post(self, request): - # limit login - username = request.data.get('username') - ip = request.data.get('remote_addr', None) - ip = ip or get_request_ip(request) - - if is_block_login(username, ip): - msg = _("Log in frequently and try again later") - logger.warn(msg + ': ' + username + ':' + ip) - return Response({'msg': msg}, status=401) - - user, msg = self.check_user_valid(request) - if not user: - username = request.data.get('username', '') - self.send_auth_signal(success=False, username=username, reason=msg) - increase_login_failed_count(username, ip) - return Response({'msg': msg}, status=401) - - if not user.otp_enabled: - self.send_auth_signal(success=True, user=user) - # 登陆成功,清除原来的缓存计数 - clean_failed_count(username, ip) - token, expired_at = user.create_bearer_token(request) - return Response( - {'token': token, 'user': self.get_serializer(user).data} - ) - - seed = uuid.uuid4().hex - cache.set(seed, user, 300) - return Response( - { - 'code': 101, - 'msg': _('Please carry seed value and ' - 'conduct MFA secondary certification'), - 'otp_url': reverse('api-auth:user-otp-auth'), - 'seed': seed, - 'user': self.get_serializer(user).data - }, status=300 - ) - - @staticmethod - def check_user_valid(request): - username = request.data.get('username', '') - password = request.data.get('password', '') - public_key = request.data.get('public_key', '') - user, msg = check_user_valid( - username=username, password=password, - public_key=public_key - ) - return user, msg - - 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): permission_classes = (IsOrgAdminOrAppUser,) @@ -150,82 +61,5 @@ class UserConnectionTokenApi(RootOrgViewMixin, APIView): return super().get_permissions() -class UserOtpAuthApi(RootOrgViewMixin, APIView): - permission_classes = (AllowAny,) - serializer_class = UserSerializer - - def get_serializer_context(self): - return { - 'request': self.request, - 'view': self - } - - def get_serializer(self, *args, **kwargs): - kwargs['context'] = self.get_serializer_context() - return self.serializer_class(*args, **kwargs) - - 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): - self.send_auth_signal(success=False, username=user.username, reason=errors.mfa_failed) - return Response({'msg': _('MFA certification failed')}, status=401) - self.send_auth_signal(success=True, user=user) - token, expired_at = user.create_bearer_token(request) - data = {'token': token, 'user': self.get_serializer(user).data} - return Response(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 UserOtpVerifyApi(CreateAPIView): - permission_classes = (IsValidUser,) - serializer_class = OtpVerifySerializer - - def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - code = serializer.validated_data["code"] - - if request.user.check_otp(code): - request.session["MFA_VERIFY_TIME"] = int(time.time()) - return Response({"ok": "1"}) - else: - return Response({"error": "Code not valid"}, status=400) - - -class UserOrderAcceptAuthApi(APIView): - permission_classes = () - - def get(self, request, *args, **kwargs): - from orders.models import LoginConfirmOrder - order_id = self.request.session.get("auth_order_id") - logger.debug('Login confirm order id: {}'.format(order_id)) - if not order_id: - order = None - else: - order = get_object_or_none(LoginConfirmOrder, pk=order_id) - if not order: - error = _("No order found or order expired") - return Response({"error": error, "status": "not found"}, status=404) - if order.status == order.STATUS_ACCEPTED: - self.request.session["auth_confirm"] = "1" - return Response({"msg": "ok"}) - elif order.status == order.STATUS_REJECTED: - error = _("Order was rejected by {}").format(order.assignee_display) - else: - error = "Order status: {}".format(order.status) - return Response({"error": error, "status": order.status}, status=400) diff --git a/apps/authentication/api/login_confirm.py b/apps/authentication/api/login_confirm.py index 3ce26f84d..45faddac6 100644 --- a/apps/authentication/api/login_confirm.py +++ b/apps/authentication/api/login_confirm.py @@ -1,13 +1,18 @@ # -*- coding: utf-8 -*- # from rest_framework.generics import UpdateAPIView +from rest_framework.response import Response +from rest_framework.views import APIView from django.shortcuts import get_object_or_404 +from common.utils import get_logger, get_object_or_none from common.permissions import IsOrgAdmin from ..models import LoginConfirmSetting from ..serializers import LoginConfirmSettingSerializer +from .. import errors -__all__ = ['LoginConfirmSettingUpdateApi'] +__all__ = ['LoginConfirmSettingUpdateApi', 'UserOrderAcceptAuthApi'] +logger = get_logger(__name__) class LoginConfirmSettingUpdateApi(UpdateAPIView): @@ -23,3 +28,29 @@ class LoginConfirmSettingUpdateApi(UpdateAPIView): defaults, user=user, ) return s + + +class UserOrderAcceptAuthApi(APIView): + permission_classes = () + + def get(self, request, *args, **kwargs): + from orders.models import LoginConfirmOrder + order_id = self.request.session.get("auth_order_id") + logger.debug('Login confirm order id: {}'.format(order_id)) + if not order_id: + order = None + else: + order = get_object_or_none(LoginConfirmOrder, pk=order_id) + try: + if not order: + raise errors.LoginConfirmOrderNotFound(order_id) + if order.status == order.STATUS_ACCEPTED: + self.request.session["auth_confirm"] = "1" + return Response({"msg": "ok"}) + elif order.status == order.STATUS_REJECTED: + raise errors.LoginConfirmRejectedError(order_id) + else: + return errors.LoginConfirmWaitError(order_id) + except errors.AuthFailedError as e: + data = e.as_data() + return Response(data, status=400) diff --git a/apps/authentication/api/mfa.py b/apps/authentication/api/mfa.py index 859106e95..3d7cde6ad 100644 --- a/apps/authentication/api/mfa.py +++ b/apps/authentication/api/mfa.py @@ -1,11 +1,56 @@ # -*- coding: utf-8 -*- # +import time from rest_framework.permissions import AllowAny from rest_framework.generics import CreateAPIView +from rest_framework.serializers import ValidationError +from rest_framework.response import Response +from common.permissions import IsValidUser +from ..serializers import OtpVerifySerializer from .. import serializers +from .. import errors +from ..mixins import AuthMixin -class MFAChallengeApi(CreateAPIView): +__all__ = ['MFAChallengeApi', 'UserOtpVerifyApi'] + + +class MFAChallengeApi(AuthMixin, CreateAPIView): permission_classes = (AllowAny,) serializer_class = serializers.MFAChallengeSerializer + + def perform_create(self, serializer): + try: + user = self.get_user_from_session() + code = serializer.validated_data.get('code') + valid = user.check_otp(code) + if not valid: + self.request.session['auth_mfa'] = '' + raise errors.MFAFailedError( + username=user.username, request=self.request + ) + + except errors.AuthFailedError as e: + data = {"error": e.error, "msg": e.reason} + raise ValidationError(data) + + def create(self, request, *args, **kwargs): + super().create(request, *args, **kwargs) + return Response({'msg': 'ok'}) + + +class UserOtpVerifyApi(CreateAPIView): + permission_classes = (IsValidUser,) + serializer_class = OtpVerifySerializer + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + code = serializer.validated_data["code"] + + if request.user.check_otp(code): + request.session["MFA_VERIFY_TIME"] = int(time.time()) + return Response({"ok": "1"}) + else: + return Response({"error": "Code not valid"}, status=400) diff --git a/apps/authentication/api/token.py b/apps/authentication/api/token.py index e2d5b2a58..e0db1bcc3 100644 --- a/apps/authentication/api/token.py +++ b/apps/authentication/api/token.py @@ -1,20 +1,14 @@ # -*- coding: utf-8 -*- # -from django.utils.translation import ugettext as _ from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.generics import CreateAPIView -from common.utils import get_request_ip, get_logger, get_object_or_none -from users.utils import ( - check_otp_code, increase_login_failed_count, - is_block_login, clean_failed_count -) -from users.models import User -from ..utils import check_user_valid -from ..signals import post_auth_success, post_auth_failed +from common.utils import get_logger + from .. import serializers, errors +from ..mixins import AuthMixin logger = get_logger(__name__) @@ -22,126 +16,24 @@ logger = get_logger(__name__) __all__ = ['TokenCreateApi'] -class TokenCreateApi(CreateAPIView): +class TokenCreateApi(AuthMixin, CreateAPIView): permission_classes = (AllowAny,) serializer_class = serializers.BearerTokenSerializer - def check_session(self): - pass - - def get_request_ip(self): - ip = self.request.data.get('remote_addr', None) - ip = ip or get_request_ip(self.request) - return ip - - def check_is_block(self): - username = self.request.data.get("username") - ip = self.get_request_ip() - if is_block_login(username, ip): - msg = errors.ip_blocked - logger.warn(msg + ': ' + username + ':' + ip) - raise errors.AuthFailedError(msg, 'blocked') - - def get_user_from_session(self): - user_id = self.request.session["user_id"] - user = get_object_or_none(User, pk=user_id) - if not user: - error = "Not user in session: {}".format(user_id) - raise errors.AuthFailedError(error, 'session_error') - return user - - def check_user_auth(self): - request = self.request - if request.session.get("auth_password") and \ - request.session.get('user_id'): - user = self.get_user_from_session() - return user - self.check_is_block() - username = request.data.get('username', '') - password = request.data.get('password', '') - public_key = request.data.get('public_key', '') - user, msg = check_user_valid( - username=username, password=password, - public_key=public_key - ) - ip = self.get_request_ip() - if not user: - raise errors.AuthFailedError(msg, error='auth_failed', username=username) - clean_failed_count(username, ip) - request.session['auth_password'] = 1 - request.session['user_id'] = str(user.id) - return user - - def check_user_mfa_if_need(self, user): - if self.request.session.get('auth_mfa'): - return True - if not user.otp_enabled or not user.otp_secret_key: - return True - otp_code = self.request.data.get("otp_code") - if not otp_code: - raise errors.MFARequiredError() - if not check_otp_code(user.otp_secret_key, otp_code): - raise errors.AuthFailedError( - errors.mfa_failed, error='mfa_failed', - username=user.username, - ) - return True - - def check_user_login_confirm_if_need(self, user): - from orders.models import LoginConfirmOrder - confirm_setting = user.get_login_confirm_setting() - if self.request.session.get('auth_confirm') or not confirm_setting: - return - order = None - if self.request.session.get('auth_order_id'): - order_id = self.request.session['auth_order_id'] - order = get_object_or_none(LoginConfirmOrder, pk=order_id) - if not order: - order = confirm_setting.create_confirm_order(self.request) - self.request.session['auth_order_id'] = str(order.id) - - if order.status == "accepted": - return - elif order.status == "rejected": - raise errors.LoginConfirmRejectedError() - else: - raise errors.LoginConfirmWaitError() + def create_session_if_need(self): + if self.request.session.is_empty(): + self.request.session.create() def create(self, request, *args, **kwargs): - self.check_session() + self.create_session_if_need() # 如果认证没有过,检查账号密码 try: user = self.check_user_auth() self.check_user_mfa_if_need(user) self.check_user_login_confirm_if_need(user) self.send_auth_signal(success=True, user=user) + self.clear_auth_mark() resp = super().create(request, *args, **kwargs) return resp except errors.AuthFailedError as e: - if e.username: - increase_login_failed_count(e.username, self.get_request_ip()) - self.send_auth_signal( - success=False, username=e.username, reason=e.reason - ) - return Response({'msg': e.reason, 'error': e.error}, status=401) - except errors.MFARequiredError: - msg = _("MFA required") - data = {'msg': msg, "choices": ["otp"], "error": 'mfa_required'} - return Response(data, status=300) - except errors.LoginConfirmRejectedError as e: - pass - except errors.LoginConfirmWaitError as e: - pass - except errors.LoginConfirmRequiredError as e: - pass - - 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 - ) + return Response(e.as_data(), status=401) diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index 4b104625d..adafd05b1 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -1,41 +1,180 @@ # -*- coding: utf-8 -*- # from django.utils.translation import ugettext_lazy as _ +from django.urls import reverse +from django.conf import settings -password_failed = _('Username/password check failed') -mfa_failed = _('MFA authentication failed') -user_not_exist = _("Username does not exist") -password_expired = _("Password expired") -user_invalid = _('Disabled or expired') -ip_blocked = _("Log in frequently and try again later") +from .signals import post_auth_failed +from users.utils import ( + increase_login_failed_count, get_login_failed_count +) -mfa_required = _("MFA required") -login_confirm_required = _("Login confirm required") -login_confirm_wait = _("Wait login confirm") +reason_password_failed = 'password_failed' +reason_mfa_failed = 'mfa_failed' +reason_user_not_exist = 'user_not_exist' +reason_password_expired = 'password_expired' +reason_user_invalid = 'user_invalid' +reason_user_inactive = 'user_inactive' + +reason_choices = { + reason_password_failed: _('Username/password check failed'), + reason_mfa_failed: _('MFA authentication failed'), + reason_user_not_exist: _("Username does not exist"), + reason_password_expired: _("Password expired"), + reason_user_invalid: _('Disabled or expired'), + reason_user_inactive: _("This account is inactive.") +} +old_reason_choices = { + '0': '-', + '1': reason_choices[reason_password_failed], + '2': reason_choices[reason_mfa_failed], + '3': reason_choices[reason_user_not_exist], + '4': reason_choices[reason_password_expired], +} + +session_empty_msg = _("No session found, check your cookie") +invalid_login_msg = _( + "The username or password you entered is incorrect, " + "please enter it again. " + "You can also try {times_try} times " + "(The account will be temporarily locked for {block_time} minutes)" +) +block_login_msg = _( + "The account has been locked " + "(please contact admin to unlock it or try again after {} minutes)" +) +mfa_failed_msg = _("MFA code invalid, or ntp sync server time") + +mfa_required_msg = _("MFA required") +login_confirm_required_msg = _("Login confirm required") +login_confirm_wait_msg = _("Wait login confirm order for accept") +login_confirm_rejected_msg = _("Login confirm order was rejected") +login_confirm_order_not_found_msg = _("Order not found") + + +class AuthFailedNeedLogMixin: + username = '' + request = None + error = '' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + post_auth_failed.send( + sender=self.__class__, username=self.username, + request=self.request, reason=self.error + ) + + +class AuthFailedNeedBlockMixin: + username = '' + ip = '' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + increase_login_failed_count(self.username, self.ip) class AuthFailedError(Exception): - def __init__(self, reason, error=None, username=None): - self.reason = reason - self.error = error - self.username = username + username = '' + msg = '' + error = '' + request = None + ip = '' + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + def as_data(self): + return { + 'error': self.error, + 'msg': self.msg, + } -class MFARequiredError(Exception): - reason = mfa_required - error = 'mfa_required' +class CredentialError(AuthFailedNeedLogMixin, AuthFailedNeedBlockMixin, AuthFailedError): + def __init__(self, error, username, ip, request): + super().__init__(error=error, username=username, ip=ip, request=request) + times_up = settings.SECURITY_LOGIN_LIMIT_COUNT + times_failed = get_login_failed_count(username, ip) + times_try = int(times_up) - int(times_failed) + block_time = settings.SECURITY_LOGIN_LIMIT_TIME + + default_msg = invalid_login_msg.format( + times_try=times_try, block_time=block_time + ) + if error == reason_password_failed: + self.msg = default_msg + else: + self.msg = reason_choices.get(error, default_msg) -class LoginConfirmRequiredError(Exception): - reason = login_confirm_required - error = 'login_confirm_required' +class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError): + reason = reason_mfa_failed + error = 'mfa_failed' + msg = mfa_failed_msg + + def __init__(self, username, request): + super().__init__(username=username, request=request) -class LoginConfirmWaitError(Exception): - reason = login_confirm_wait - error = 'login_confirm_wait' +class BlockLoginError(AuthFailedNeedBlockMixin, AuthFailedError): + error = 'block_login' + msg = block_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME) + + def __init__(self, username, ip): + super().__init__(username=username, ip=ip) -class LoginConfirmRejectedError(Exception): - reason = login_confirm_wait - error = 'login_confirm_rejected' +class SessionEmptyError(AuthFailedError): + msg = session_empty_msg + error = 'session_empty_msg' + + +class MFARequiredError(AuthFailedError): + msg = mfa_required_msg + error = 'mfa_required_msg' + + def as_data(self): + return { + 'error': self.error, + 'msg': self.msg, + 'choices': ['otp'], + 'url': reverse('api-auth:mfa-challenge') + } + + +class LoginConfirmRequiredError(AuthFailedError): + msg = login_confirm_required_msg + error = 'login_confirm_required_msg' + + +class LoginConfirmError(AuthFailedError): + msg = login_confirm_wait_msg + error = 'login_confirm_wait_msg' + + def __init__(self, order_id, **kwargs): + self.order_id = order_id + super().__init__(**kwargs) + + def as_data(self): + return { + "error": self.error, + "msg": self.msg, + "order_id": self.order_id + } + + +class LoginConfirmWaitError(LoginConfirmError): + msg = login_confirm_wait_msg + error = 'login_confirm_wait_msg' + + +class LoginConfirmRejectedError(LoginConfirmError): + msg = login_confirm_rejected_msg + error = 'login_confirm_rejected_msg' + + +class LoginConfirmOrderNotFound(LoginConfirmError): + msg = login_confirm_order_not_found_msg + error = 'login_confirm_order_not_found_msg' diff --git a/apps/authentication/forms.py b/apps/authentication/forms.py index 5316e0d79..1b83a3a55 100644 --- a/apps/authentication/forms.py +++ b/apps/authentication/forms.py @@ -9,53 +9,19 @@ from django.conf import settings from users.utils import get_login_failed_count -class UserLoginForm(AuthenticationForm): +class UserLoginForm(forms.Form): username = forms.CharField(label=_('Username'), max_length=100) password = forms.CharField( label=_('Password'), widget=forms.PasswordInput, max_length=128, strip=False ) - error_messages = { - 'invalid_login': _( - "The username or password you entered is incorrect, " - "please enter it again." - ), - 'inactive': _("This account is inactive."), - 'limit_login': _( - "You can also try {times_try} times " - "(The account will be temporarily locked for {block_time} minutes)" - ), - 'block_login': _( - "The account has been locked " - "(please contact admin to unlock it or try again after {} minutes)" - ) - } - def confirm_login_allowed(self, user): if not user.is_staff: raise forms.ValidationError( self.error_messages['inactive'], - code='inactive',) - - def get_limit_login_error_message(self, username, ip): - times_up = settings.SECURITY_LOGIN_LIMIT_COUNT - times_failed = get_login_failed_count(username, ip) - times_try = int(times_up) - int(times_failed) - block_time = settings.SECURITY_LOGIN_LIMIT_TIME - if times_try <= 0: - error_message = self.error_messages['block_login'] - error_message = error_message.format(block_time) - else: - error_message = self.error_messages['limit_login'] - error_message = error_message.format( - times_try=times_try, block_time=block_time, + code='inactive', ) - return error_message - - def add_limit_login_error(self, username, ip): - error = self.get_limit_login_error_message(username, ip) - self.add_error('password', error) class UserLoginCaptchaForm(UserLoginForm): diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py new file mode 100644 index 000000000..02d728b2d --- /dev/null +++ b/apps/authentication/mixins.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# +import time + +from common.utils import get_object_or_none, get_request_ip, get_logger +from users.models import User +from users.utils import ( + is_block_login, clean_failed_count, increase_login_failed_count +) +from . import errors +from .utils import check_user_valid +from .signals import post_auth_success, post_auth_failed + +logger = get_logger(__name__) + + +class AuthMixin: + request = None + + def get_user_from_session(self): + if self.request.session.is_empty(): + raise errors.SessionEmptyError() + if self.request.user and not self.request.user.is_anonymous: + return self.request.user + user_id = self.request.session.get('user_id') + if not user_id: + user = None + else: + user = get_object_or_none(User, pk=user_id) + if not user: + raise errors.SessionEmptyError() + return user + + def get_request_ip(self): + ip = '' + if hasattr(self.request, 'data'): + ip = self.request.data.get('remote_addr', '') + ip = ip or get_request_ip(self.request) + return ip + + def check_is_block(self): + if hasattr(self.request, 'data'): + username = self.request.data.get("username") + else: + username = self.request.POST.get("username") + ip = self.get_request_ip() + if is_block_login(username, ip): + logger.warn('Ip was blocked' + ': ' + username + ':' + ip) + raise errors.BlockLoginError(username=username, ip=ip) + + def check_user_auth(self): + request = self.request + self.check_is_block() + if hasattr(request, 'data'): + username = request.data.get('username', '') + password = request.data.get('password', '') + public_key = request.data.get('public_key', '') + else: + username = request.POST.get('username', '') + password = request.POST.get('password', '') + public_key = request.POST.get('public_key', '') + user, error = check_user_valid( + username=username, password=password, + public_key=public_key + ) + ip = self.get_request_ip() + if not user: + raise errors.CredentialError( + username=username, error=error, ip=ip, request=request + ) + clean_failed_count(username, ip) + request.session['auth_password'] = 1 + request.session['user_id'] = str(user.id) + return user + + def check_user_mfa_if_need(self, user): + if self.request.session.get('auth_mfa'): + return True + if not user.otp_enabled or not user.otp_secret_key: + return True + raise errors.MFARequiredError() + + def check_user_mfa(self, code): + user = self.get_user_from_session() + ok = user.check_otp(code) + if ok: + self.request.session['auth_mfa'] = 1 + self.request.session['auth_mfa_time'] = time.time() + self.request.session['auth_mfa_type'] = 'otp' + return + raise errors.MFAFailedError(username=user.username, request=self.request) + + def check_user_login_confirm_if_need(self, user): + from orders.models import LoginConfirmOrder + confirm_setting = user.get_login_confirm_setting() + if self.request.session.get('auth_confirm') or not confirm_setting: + return + order = None + if self.request.session.get('auth_order_id'): + order_id = self.request.session['auth_order_id'] + order = get_object_or_none(LoginConfirmOrder, pk=order_id) + if not order: + order = confirm_setting.create_confirm_order(self.request) + self.request.session['auth_order_id'] = str(order.id) + + if order.status == "accepted": + return + elif order.status == "rejected": + raise errors.LoginConfirmRejectedError(order.id) + else: + raise errors.LoginConfirmWaitError(order.id) + + def clear_auth_mark(self): + self.request.session['auth_password'] = '' + self.request.session['auth_mfa'] = '' + self.request.session['auth_confirm'] = '' + + 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/authentication/serializers.py b/apps/authentication/serializers.py index 7463d30ca..0a2f70dda 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -3,6 +3,7 @@ from django.core.cache import cache from rest_framework import serializers +from common.utils import get_object_or_none from users.models import User from .models import AccessKey, LoginConfirmSetting @@ -24,7 +25,12 @@ class OtpVerifySerializer(serializers.Serializer): code = serializers.CharField(max_length=6, min_length=6) -class BearerTokenMixin(serializers.Serializer): +class BearerTokenSerializer(serializers.Serializer): + username = serializers.CharField(allow_null=True, required=False) + password = serializers.CharField(write_only=True, allow_null=True, + required=False) + public_key = serializers.CharField(write_only=True, allow_null=True, + required=False) token = serializers.CharField(read_only=True) keyword = serializers.SerializerMethodField() date_expired = serializers.DateTimeField(read_only=True) @@ -33,58 +39,35 @@ class BearerTokenMixin(serializers.Serializer): def get_keyword(obj): return 'Bearer' - def create_response(self, username): - request = self.context.get("request") - try: - user = User.objects.get(username=username) - except User.DoesNotExist: - raise serializers.ValidationError("username %s not exist" % username) + def create(self, validated_data): + request = self.context.get('request') + if request.user and not request.user.is_anonymous: + user = request.user + else: + user_id = request.session.get('user_id') + user = get_object_or_none(User, pk=user_id) + if not user: + raise serializers.ValidationError( + "user id {} not exist".format(user_id) + ) token, date_expired = user.create_bearer_token(request) instance = { - "username": username, + "username": user.username, "token": token, "date_expired": date_expired, } return instance - def update(self, instance, validated_data): - pass - -class BearerTokenSerializer(BearerTokenMixin, serializers.Serializer): - username = serializers.CharField() - password = serializers.CharField(write_only=True, allow_null=True, - required=False) - public_key = serializers.CharField(write_only=True, allow_null=True, - required=False) - - def create(self, validated_data): - username = validated_data.get("username") - return self.create_response(username) - - -class MFAChallengeSerializer(BearerTokenMixin, serializers.Serializer): - req = serializers.CharField(write_only=True) - auth_type = serializers.CharField(write_only=True) +class MFAChallengeSerializer(serializers.Serializer): + auth_type = serializers.CharField(write_only=True, required=False, allow_blank=True) code = serializers.CharField(write_only=True) - def validate_req(self, attr): - username = cache.get(attr) - if not username: - raise serializers.ValidationError("Not valid, may be expired") - self.context["username"] = username - - def validate_code(self, code): - username = self.context["username"] - user = User.objects.get(username=username) - ok = user.check_otp(code) - if not ok: - msg = "Otp code not valid, may be expired" - raise serializers.ValidationError(msg) - def create(self, validated_data): - username = self.context["username"] - return self.create_response(username) + pass + + def update(self, instance, validated_data): + pass class LoginConfirmSettingSerializer(serializers.ModelSerializer): diff --git a/apps/authentication/signals_handlers.py b/apps/authentication/signals_handlers.py index c0b48c61d..538bdd869 100644 --- a/apps/authentication/signals_handlers.py +++ b/apps/authentication/signals_handlers.py @@ -1,18 +1,14 @@ -from rest_framework.request import Request 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 .backends.openid import new_client from .backends.openid.signals import ( post_create_openid_user, post_openid_login_success ) -from .tasks import write_login_log_async -from .signals import post_auth_success, post_auth_failed +from .signals import post_auth_success @receiver(user_logged_out) @@ -52,35 +48,4 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs): user.save() -def generate_data(username, request): - user_agent = request.META.get('HTTP_USER_AGENT', '') - if isinstance(request, Request): - login_ip = request.data.get('remote_addr', None) - login_type = request.data.get('login_type', '') - else: - login_ip = get_request_ip(request) - 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 index d64d92992..08472e931 100644 --- a/apps/authentication/tasks.py +++ b/apps/authentication/tasks.py @@ -6,17 +6,8 @@ from ops.celery.decorator import register_as_period_task from django.contrib.sessions.models import Session from django.utils import timezone -from .utils import write_login_log - - -@shared_task -def write_login_log_async(*args, **kwargs): - write_login_log(*args, **kwargs) - @register_as_period_task(interval=3600*24) @shared_task def clean_django_sessions(): Session.objects.filter(expire_date__lt=timezone.now()).delete() - - diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html index b31a716b8..5f1c68c5f 100644 --- a/apps/authentication/templates/authentication/login.html +++ b/apps/authentication/templates/authentication/login.html @@ -37,7 +37,6 @@

{% trans "Changes the world, starting with a little bit." %}

-
@@ -47,25 +46,29 @@
{% csrf_token %} - - {% if block_login %} -

{% trans 'Log in frequently and try again later' %}

- {% elif password_expired %} -

{% trans 'The user password has expired' %}

- {% elif form.errors %} - {% if 'captcha' in form.errors %} -

{% trans 'Captcha invalid' %}

- {% else %} + {% if form.non_field_errors %} +

{{ form.non_field_errors.as_text }}

- {% endif %} -

{{ form.errors.password.as_text }}

+
+ {% elif form.errors.captcha %} +

{% trans 'Captcha invalid' %}

{% endif %}
+ {% if form.errors.username %} +
+

{{ form.errors.username.as_text }}

+
+ {% endif %}
+ {% if form.errors.password %} +
+

{{ form.errors.password.as_text }}

+
+ {% endif %}
{{ form.captcha }} diff --git a/apps/authentication/templates/authentication/login_wait_confirm.html b/apps/authentication/templates/authentication/login_wait_confirm.html index 0167236df..0b22cbd1c 100644 --- a/apps/authentication/templates/authentication/login_wait_confirm.html +++ b/apps/authentication/templates/authentication/login_wait_confirm.html @@ -86,7 +86,7 @@ function doRequestAuth() { window.location = successUrl; }, error: function (text, data) { - if (data.status !== "pending") { + if (data.error !== "login_confirm_wait") { if (!errorMsgShow) { infoMsgRef.hide(); errorMsgRef.show(); @@ -97,7 +97,7 @@ function doRequestAuth() { clearInterval(checkInterval); $(".copy-btn").attr('disabled', 'disabled') } - errorMsgRef.html(data.error) + errorMsgRef.html(data.msg) }, flash_message: false }) diff --git a/apps/authentication/templates/authentication/xpack_login.html b/apps/authentication/templates/authentication/xpack_login.html index ff61b981c..2035cc360 100644 --- a/apps/authentication/templates/authentication/xpack_login.html +++ b/apps/authentication/templates/authentication/xpack_login.html @@ -48,6 +48,13 @@ float: right; } + .red-fonts { + color: red; + } + + .field-error { + text-align: left; + } @@ -69,30 +76,32 @@
-
+
{% csrf_token %} + {% if form.non_field_errors %}
- {% if block_login %} -

{% trans 'Log in frequently and try again later' %}

-

{{ form.errors.password.as_text }}

- {% elif password_expired %} -

{% trans 'The user password has expired' %}

- {% elif form.errors %} - {% if 'captcha' in form.errors %} -

{% trans 'Captcha invalid' %}

- {% else %} -

{{ form.non_field_errors.as_text }}

- {% endif %} -

{{ form.errors.password.as_text }}

- {% endif %} +

{{ form.non_field_errors.as_text }}

+ {% elif form.errors.captcha %} +

{% trans 'Captcha invalid' %}

+ {% endif %}
+ {% if form.errors.username %} +
+

{{ form.errors.username.as_text }}

+
+ {% endif %}
+ {% if form.errors.password %} +
+

{{ form.errors.password.as_text }}

+
+ {% endif %}
{{ form.captcha }} @@ -116,4 +125,4 @@
- \ No newline at end of file + diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index b47e5eb72..57e238192 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -12,12 +12,11 @@ router.register('access-keys', api.AccessKeyViewSet, 'access-key') urlpatterns = [ # path('token/', api.UserToken.as_view(), name='user-token'), - path('auth/', api.UserAuthApi.as_view(), name='user-auth'), + path('auth/', api.TokenCreateApi.as_view(), name='user-auth'), path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'), path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'), path('connection-token/', api.UserConnectionTokenApi.as_view(), name='connection-token'), - path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'), path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'), path('order/auth/', api.UserOrderAcceptAuthApi.as_view(), name='user-order-auth'), path('login-confirm-settings//', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update') diff --git a/apps/authentication/utils.py b/apps/authentication/utils.py index c96878e38..f06b6ef15 100644 --- a/apps/authentication/utils.py +++ b/apps/authentication/utils.py @@ -1,34 +1,21 @@ # -*- coding: utf-8 -*- # -from django.utils.translation import ugettext as _, ugettext_lazy as __ +from django.utils.translation import ugettext as _ from django.contrib.auth import authenticate -from django.utils import timezone from common.utils import ( - get_ip_city, get_object_or_none, validate_ip, get_request_ip + get_ip_city, get_object_or_none, validate_ip ) from users.models import User from . import errors -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 - else: - city = get_ip_city(ip) or default_city - kwargs.update({'ip': ip, 'city': city}) - UserLoginLog.objects.create(**kwargs) - - def check_user_valid(**kwargs): password = kwargs.pop('password', None) public_key = kwargs.pop('public_key', None) email = kwargs.pop('email', None) username = kwargs.pop('username', None) + request = kwargs.get('request') if username: user = get_object_or_none(User, username=username) @@ -38,21 +25,25 @@ def check_user_valid(**kwargs): user = None if user is None: - return None, errors.user_not_exist - elif not user.is_valid: - return None, errors.user_invalid + return None, errors.reason_user_not_exist + elif user.is_expired: + return None, errors.reason_password_expired + elif not user.is_active: + return None, errors.reason_user_inactive elif user.password_has_expired: - return None, errors.password_expired + return None, errors.reason_password_expired - if password and authenticate(username=username, password=password): - return user, '' + if password: + user = authenticate(request, username=username, password=password) + if user: + return user, '' if public_key and user.public_key: public_key_saved = user.public_key.split() if len(public_key_saved) == 1: - if public_key == public_key_saved[0]: - return user, '' - elif len(public_key_saved) > 1: - if public_key == public_key_saved[1]: - return user, '' - return None, errors.password_failed + public_key_saved = public_key_saved[0] + else: + public_key_saved = public_key_saved[1] + if public_key == public_key_saved: + return user, '' + return None, errors.reason_password_failed diff --git a/apps/authentication/views/__init__.py b/apps/authentication/views/__init__.py index 5e7732adc..5a1a40f7a 100644 --- a/apps/authentication/views/__init__.py +++ b/apps/authentication/views/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # - from .login import * +from .mfa import * diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index f8e5984b1..282ec8501 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -16,22 +16,20 @@ from django.views.decorators.debug import sensitive_post_parameters from django.views.generic.base import TemplateView, RedirectView from django.views.generic.edit import FormView from django.conf import settings +from django.urls import reverse_lazy from common.utils import get_request_ip, get_object_or_none from users.models import User from users.utils import ( - check_otp_code, is_block_login, clean_failed_count, get_user_or_tmp_user, - set_tmp_user_to_cache, increase_login_failed_count, + get_user_or_tmp_user, increase_login_failed_count, redirect_user_first_login_or_index ) -from ..models import LoginConfirmSetting from ..signals import post_auth_success, post_auth_failed -from .. import forms -from .. import errors +from .. import forms, mixins, errors __all__ = [ - 'UserLoginView', 'UserLoginOtpView', 'UserLogoutView', + 'UserLoginView', 'UserLogoutView', 'UserLoginGuardView', 'UserLoginWaitConfirmView', ] @@ -39,10 +37,11 @@ __all__ = [ @method_decorator(sensitive_post_parameters(), name='dispatch') @method_decorator(csrf_protect, name='dispatch') @method_decorator(never_cache, name='dispatch') -class UserLoginView(FormView): +class UserLoginView(mixins.AuthMixin, FormView): form_class = forms.UserLoginForm form_class_captcha = forms.UserLoginCaptchaForm key_prefix_captcha = "_LOGIN_INVALID_{}" + redirect_field_name = 'next' def get_template_names(self): template_name = 'authentication/login.html' @@ -69,54 +68,25 @@ class UserLoginView(FormView): request.session.set_test_cookie() return super().get(request, *args, **kwargs) - def post(self, request, *args, **kwargs): - # limit login authentication - ip = get_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 = errors.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) - self.request.session['auth_password'] = '1' + try: + self.check_user_auth() + except errors.AuthFailedError as e: + form.add_error(None, e.msg) + ip = self.get_request_ip() + cache.set(self.key_prefix_captcha.format(ip), 1, 3600) + context = self.get_context_data(form=form) + return self.render_to_response(context) return self.redirect_to_guard_view() - def form_invalid(self, form): - # write login failed log - username = form.cleaned_data.get('username') - exist = User.objects.filter(username=username).first() - reason = errors.password_failed if exist else errors.user_not_exist - # limit user login failed count - ip = get_request_ip(self.request) - increase_login_failed_count(username, ip) - form.add_limit_login_error(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) - - @staticmethod - def redirect_to_guard_view(): - continue_url = reverse('authentication:login-guard') - return redirect(continue_url) + def redirect_to_guard_view(self): + guard_url = reverse('authentication:login-guard') + args = self.request.META.get('QUERY_STRING', '') + if args and self.query_string: + guard_url = "%s?%s" % (guard_url, args) + return redirect(guard_url) def get_form_class(self): ip = get_request_ip(self.request) @@ -134,58 +104,34 @@ class UserLoginView(FormView): return super().get_context_data(**kwargs) -class UserLoginOtpView(FormView): - template_name = 'authentication/login_otp.html' - form_class = forms.UserCheckOtpCodeForm +class UserLoginGuardView(mixins.AuthMixin, RedirectView): redirect_field_name = 'next' + login_url = reverse_lazy('authentication:login') + login_otp_url = reverse_lazy('authentication:login-otp') + login_confirm_url = reverse_lazy('authentication:login-wait-confirm') - 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): - self.request.session['auth_otp'] = '1' - return UserLoginView.redirect_to_guard_view() - else: - self.send_auth_signal( - success=False, username=user.username, - reason=errors.mfa_failed - ) - form.add_error( - 'otp_code', _('MFA code invalid, or ntp sync server time') - ) - return super().form_invalid(form) - - 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 UserLoginGuardView(RedirectView): - redirect_field_name = 'next' + def format_redirect_url(self, url): + args = self.request.META.get('QUERY_STRING', '') + if args and self.query_string: + url = "%s?%s" % (url, args) + return url def get_redirect_url(self, *args, **kwargs): if not self.request.session.get('auth_password'): - return reverse('authentication:login') - - user = get_user_or_tmp_user(self.request) + return self.format_redirect_url(self.login_url) + user = self.get_user_from_session() # 启用并设置了otp if user.otp_enabled and user.otp_secret_key and \ - not self.request.session.get('auth_otp'): - return reverse('authentication:login-otp') + not self.request.session.get('auth_mfa'): + return self.format_redirect_url(self.login_otp_url) confirm_setting = user.get_login_confirm_setting() if confirm_setting and not self.request.session.get('auth_confirm'): order = confirm_setting.create_confirm_order(self.request) self.request.session['auth_order_id'] = str(order.id) - url = reverse('authentication:login-wait-confirm') + url = self.format_redirect_url(self.login_confirm_url) return url self.login_success(user) + self.clear_auth_mark() # 启用但是没有设置otp if user.otp_enabled and not user.otp_secret_key: # 1,2,mfa_setting & F diff --git a/apps/authentication/views/mfa.py b/apps/authentication/views/mfa.py new file mode 100644 index 000000000..c143601cb --- /dev/null +++ b/apps/authentication/views/mfa.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# + +from __future__ import unicode_literals +from django.views.generic.edit import FormView +from .. import forms, errors, mixins +from .utils import redirect_to_guard_view + +__all__ = ['UserLoginOtpView'] + + +class UserLoginOtpView(mixins.AuthMixin, FormView): + template_name = 'authentication/login_otp.html' + form_class = forms.UserCheckOtpCodeForm + redirect_field_name = 'next' + + def form_valid(self, form): + otp_code = form.cleaned_data.get('otp_code') + try: + self.check_user_mfa(otp_code) + return redirect_to_guard_view() + except errors.MFAFailedError as e: + form.add_error('otp_code', e.reason) + return super().form_invalid(form) + diff --git a/apps/authentication/views/utils.py b/apps/authentication/views/utils.py new file mode 100644 index 000000000..182d7390b --- /dev/null +++ b/apps/authentication/views/utils.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# +from django.shortcuts import reverse, redirect + + +def redirect_to_guard_view(): + continue_url = reverse('authentication:login-guard') + return redirect(continue_url) diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index 2f4ce784c..29f3b471c 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -153,6 +153,14 @@ def get_request_ip(request): return login_ip +def get_request_ip_or_data(request): + ip = '' + if hasattr(request, 'data'): + ip = request.data.get('remote_addr', '') + ip = ip or get_request_ip(request) + return ip + + def validate_ip(ip): try: ipaddress.ip_address(ip) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 7d21e0d682223b8118cb8487069700ed5d42e1ea..39d51220e02eca0be696c263006d7160b39b4f2a 100644 GIT binary patch delta 19389 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 delta 19350 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\n" "Language-Team: Jumpserver team\n" @@ -96,7 +96,7 @@ msgstr "运行参数" #: terminal/templates/terminal/session_list.html:28 #: terminal/templates/terminal/session_list.html:72 #: xpack/plugins/change_auth_plan/forms.py:73 -#: xpack/plugins/change_auth_plan/models.py:412 +#: xpack/plugins/change_auth_plan/models.py:419 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:46 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:54 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:13 @@ -145,14 +145,14 @@ msgstr "资产" #: terminal/models.py:260 terminal/templates/terminal/terminal_detail.html:43 #: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14 #: users/models/user.py:382 users/templates/users/_select_user_modal.html:13 -#: users/templates/users/user_detail.html:63 +#: users/templates/users/user_detail.html:64 #: users/templates/users/user_group_detail.html:55 #: users/templates/users/user_group_list.html:35 #: users/templates/users/user_list.html:35 #: users/templates/users/user_profile.html:51 #: users/templates/users/user_pubkey_update.html:57 #: xpack/plugins/change_auth_plan/forms.py:56 -#: xpack/plugins/change_auth_plan/models.py:63 +#: xpack/plugins/change_auth_plan/models.py:64 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:61 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:12 #: xpack/plugins/cloud/models.py:59 xpack/plugins/cloud/models.py:144 @@ -198,8 +198,8 @@ msgstr "参数" #: perms/templates/perms/asset_permission_detail.html:98 #: perms/templates/perms/remote_app_permission_detail.html:90 #: users/models/user.py:423 users/serializers/group.py:32 -#: users/templates/users/user_detail.html:111 -#: xpack/plugins/change_auth_plan/models.py:108 +#: users/templates/users/user_detail.html:112 +#: xpack/plugins/change_auth_plan/models.py:109 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:113 #: xpack/plugins/cloud/models.py:80 xpack/plugins/cloud/models.py:179 #: xpack/plugins/gathered_user/models.py:46 @@ -261,11 +261,11 @@ msgstr "创建日期" #: perms/templates/perms/remote_app_permission_detail.html:94 #: settings/models.py:34 terminal/models.py:33 #: terminal/templates/terminal/terminal_detail.html:63 users/models/group.py:15 -#: users/models/user.py:415 users/templates/users/user_detail.html:129 +#: users/models/user.py:415 users/templates/users/user_detail.html:130 #: users/templates/users/user_group_detail.html:67 #: users/templates/users/user_group_list.html:37 #: users/templates/users/user_profile.html:138 -#: xpack/plugins/change_auth_plan/models.py:104 +#: xpack/plugins/change_auth_plan/models.py:105 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:117 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:19 #: xpack/plugins/cloud/models.py:77 xpack/plugins/cloud/models.py:173 @@ -313,7 +313,7 @@ msgstr "远程应用" #: terminal/templates/terminal/terminal_update.html:45 #: users/templates/users/_user.html:50 #: users/templates/users/user_bulk_update.html:23 -#: users/templates/users/user_detail.html:178 +#: users/templates/users/user_detail.html:179 #: users/templates/users/user_group_create_update.html:31 #: users/templates/users/user_password_update.html:75 #: users/templates/users/user_profile.html:209 @@ -420,7 +420,7 @@ msgstr "详情" #: perms/templates/perms/remote_app_permission_list.html:64 #: terminal/templates/terminal/terminal_detail.html:16 #: terminal/templates/terminal/terminal_list.html:73 -#: users/templates/users/user_detail.html:25 +#: users/templates/users/user_detail.html:26 #: users/templates/users/user_group_detail.html:28 #: users/templates/users/user_group_list.html:20 #: users/templates/users/user_group_list.html:71 @@ -467,7 +467,7 @@ msgstr "更新" #: settings/templates/settings/terminal_setting.html:93 #: settings/templates/settings/terminal_setting.html:115 #: terminal/templates/terminal/terminal_list.html:75 -#: users/templates/users/user_detail.html:30 +#: users/templates/users/user_detail.html:31 #: users/templates/users/user_group_detail.html:32 #: users/templates/users/user_group_list.html:73 #: users/templates/users/user_list.html:111 @@ -606,7 +606,7 @@ msgstr "端口" #: assets/templates/assets/asset_detail.html:196 #: assets/templates/assets/system_user_assets.html:83 #: perms/models/asset_permission.py:81 -#: xpack/plugins/change_auth_plan/models.py:74 +#: xpack/plugins/change_auth_plan/models.py:75 #: xpack/plugins/gathered_user/models.py:31 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:17 msgid "Nodes" @@ -700,21 +700,21 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC" #: assets/templates/assets/admin_user_list.html:45 #: assets/templates/assets/domain_gateway_list.html:71 #: assets/templates/assets/system_user_detail.html:62 -#: assets/templates/assets/system_user_list.html:48 audits/models.py:81 +#: assets/templates/assets/system_user_list.html:48 audits/models.py:82 #: audits/templates/audits/login_log_list.html:57 authentication/forms.py:13 -#: authentication/templates/authentication/login.html:65 -#: authentication/templates/authentication/xpack_login.html:92 +#: authentication/templates/authentication/login.html:60 +#: authentication/templates/authentication/xpack_login.html:87 #: ops/models/adhoc.py:189 perms/templates/perms/asset_permission_list.html:70 #: perms/templates/perms/asset_permission_user.html:55 #: perms/templates/perms/remote_app_permission_user.html:54 #: settings/templates/settings/_ldap_list_users_modal.html:30 users/forms.py:13 #: users/models/user.py:380 users/templates/users/_select_user_modal.html:14 -#: users/templates/users/user_detail.html:67 +#: users/templates/users/user_detail.html:68 #: users/templates/users/user_list.html:36 #: users/templates/users/user_profile.html:47 #: xpack/plugins/change_auth_plan/forms.py:58 -#: xpack/plugins/change_auth_plan/models.py:65 -#: xpack/plugins/change_auth_plan/models.py:408 +#: xpack/plugins/change_auth_plan/models.py:66 +#: xpack/plugins/change_auth_plan/models.py:415 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:65 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:53 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:12 @@ -732,8 +732,8 @@ msgstr "密码或密钥密码" #: assets/templates/assets/_asset_user_auth_update_modal.html:21 #: assets/templates/assets/_asset_user_auth_view_modal.html:27 #: authentication/forms.py:15 -#: authentication/templates/authentication/login.html:68 -#: authentication/templates/authentication/xpack_login.html:95 +#: authentication/templates/authentication/login.html:63 +#: authentication/templates/authentication/xpack_login.html:90 #: settings/forms.py:114 users/forms.py:15 users/forms.py:27 #: users/templates/users/reset_password.html:53 #: users/templates/users/user_password_authentication.html:18 @@ -741,8 +741,8 @@ msgstr "密码或密钥密码" #: users/templates/users/user_profile_update.html:41 #: users/templates/users/user_pubkey_update.html:41 #: users/templates/users/user_update.html:20 -#: xpack/plugins/change_auth_plan/models.py:95 -#: xpack/plugins/change_auth_plan/models.py:263 +#: xpack/plugins/change_auth_plan/models.py:96 +#: xpack/plugins/change_auth_plan/models.py:264 msgid "Password" msgstr "密码" @@ -938,13 +938,13 @@ msgstr "版本" msgid "AuthBook" msgstr "" -#: assets/models/base.py:31 xpack/plugins/change_auth_plan/models.py:99 -#: xpack/plugins/change_auth_plan/models.py:270 +#: assets/models/base.py:31 xpack/plugins/change_auth_plan/models.py:100 +#: xpack/plugins/change_auth_plan/models.py:271 msgid "SSH private key" msgstr "ssh密钥" -#: assets/models/base.py:32 xpack/plugins/change_auth_plan/models.py:102 -#: xpack/plugins/change_auth_plan/models.py:266 +#: assets/models/base.py:32 xpack/plugins/change_auth_plan/models.py:103 +#: xpack/plugins/change_auth_plan/models.py:267 msgid "SSH public key" msgstr "ssh公钥" @@ -965,7 +965,7 @@ msgid "Contact" msgstr "联系人" #: assets/models/cluster.py:22 users/models/user.py:401 -#: users/templates/users/user_detail.html:76 +#: users/templates/users/user_detail.html:77 msgid "Phone" msgstr "手机" @@ -1121,7 +1121,7 @@ msgstr "默认资产组" #: terminal/templates/terminal/command_list.html:65 #: terminal/templates/terminal/session_list.html:27 #: terminal/templates/terminal/session_list.html:71 users/forms.py:319 -#: users/models/user.py:136 users/models/user.py:152 users/models/user.py:509 +#: users/models/user.py:132 users/models/user.py:148 users/models/user.py:509 #: users/serializers/group.py:21 #: users/templates/users/user_group_detail.html:78 #: users/templates/users/user_group_list.html:36 users/views/user.py:250 @@ -1187,7 +1187,7 @@ msgstr "手动登录" #: assets/views/label.py:27 assets/views/label.py:45 assets/views/label.py:73 #: assets/views/system_user.py:29 assets/views/system_user.py:46 #: assets/views/system_user.py:63 assets/views/system_user.py:79 -#: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:70 +#: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:71 msgid "Assets" msgstr "资产管理" @@ -1236,17 +1236,17 @@ msgstr "系统用户" msgid "%(value)s is not an even number" msgstr "%(value)s is not an even number" -#: assets/models/utils.py:43 assets/tasks/const.py:84 +#: assets/models/utils.py:43 assets/tasks/const.py:87 msgid "Unreachable" msgstr "不可达" -#: assets/models/utils.py:44 assets/tasks/const.py:85 +#: assets/models/utils.py:44 assets/tasks/const.py:88 #: assets/templates/assets/asset_list.html:99 msgid "Reachable" msgstr "可连接" -#: assets/models/utils.py:45 assets/tasks/const.py:86 -#: authentication/utils.py:16 xpack/plugins/license/models.py:78 +#: assets/models/utils.py:45 assets/tasks/const.py:89 audits/utils.py:29 +#: xpack/plugins/license/models.py:78 msgid "Unknown" msgstr "未知" @@ -1332,7 +1332,7 @@ msgstr "测试资产可连接性: {}" #: assets/tasks/asset_user_connectivity.py:27 #: assets/tasks/push_system_user.py:130 -#: xpack/plugins/change_auth_plan/models.py:521 +#: xpack/plugins/change_auth_plan/models.py:528 msgid "The asset {} system platform {} does not support run Ansible tasks" msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务" @@ -1470,8 +1470,8 @@ msgstr "请输入密码" #: assets/templates/assets/_asset_user_auth_update_modal.html:68 #: assets/templates/assets/asset_detail.html:302 -#: users/templates/users/user_detail.html:364 -#: users/templates/users/user_detail.html:391 +#: users/templates/users/user_detail.html:366 +#: users/templates/users/user_detail.html:393 #: xpack/plugins/interface/views.py:35 msgid "Update successfully!" msgstr "更新成功" @@ -1481,7 +1481,7 @@ msgid "Asset user auth" msgstr "资产用户信息" #: assets/templates/assets/_asset_user_auth_view_modal.html:54 -#: authentication/templates/authentication/login_wait_confirm.html:117 +#: authentication/templates/authentication/login_wait_confirm.html:114 msgid "Copy success" msgstr "复制成功" @@ -1669,10 +1669,10 @@ msgstr "选择节点" #: settings/templates/settings/terminal_setting.html:168 #: templates/_modal.html:23 terminal/templates/terminal/session_detail.html:112 #: users/templates/users/user_detail.html:271 -#: users/templates/users/user_detail.html:445 -#: users/templates/users/user_detail.html:471 -#: users/templates/users/user_detail.html:494 -#: users/templates/users/user_detail.html:539 +#: users/templates/users/user_detail.html:447 +#: users/templates/users/user_detail.html:473 +#: users/templates/users/user_detail.html:496 +#: users/templates/users/user_detail.html:541 #: users/templates/users/user_group_create_update.html:32 #: users/templates/users/user_group_list.html:120 #: users/templates/users/user_list.html:256 @@ -1750,7 +1750,7 @@ msgstr "资产用户" #: assets/templates/assets/asset_asset_user_list.html:47 #: assets/templates/assets/asset_detail.html:142 #: terminal/templates/terminal/session_detail.html:85 -#: users/templates/users/user_detail.html:140 +#: users/templates/users/user_detail.html:141 #: users/templates/users/user_profile.html:150 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:128 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:132 @@ -1777,7 +1777,7 @@ msgid "Disk" msgstr "硬盘" #: assets/templates/assets/asset_detail.html:126 -#: users/templates/users/user_detail.html:115 +#: users/templates/users/user_detail.html:116 #: users/templates/users/user_profile.html:106 msgid "Date joined" msgstr "创建日期" @@ -1791,7 +1791,7 @@ msgstr "创建日期" #: perms/templates/perms/remote_app_permission_detail.html:112 #: terminal/templates/terminal/terminal_list.html:34 #: users/templates/users/_select_user_modal.html:18 -#: users/templates/users/user_detail.html:146 +#: users/templates/users/user_detail.html:147 #: users/templates/users/user_profile.html:63 msgid "Active" msgstr "激活中" @@ -1872,9 +1872,9 @@ msgstr "显示所有子节点资产" #: assets/templates/assets/asset_list.html:417 #: assets/templates/assets/system_user_list.html:129 -#: users/templates/users/user_detail.html:439 -#: users/templates/users/user_detail.html:465 -#: users/templates/users/user_detail.html:533 +#: users/templates/users/user_detail.html:441 +#: users/templates/users/user_detail.html:467 +#: users/templates/users/user_detail.html:535 #: users/templates/users/user_group_list.html:114 #: users/templates/users/user_list.html:250 #: xpack/plugins/interface/templates/interface/interface.html:97 @@ -1888,9 +1888,9 @@ msgstr "删除选择资产" #: assets/templates/assets/asset_list.html:421 #: assets/templates/assets/system_user_list.html:133 #: settings/templates/settings/terminal_setting.html:166 -#: users/templates/users/user_detail.html:443 -#: users/templates/users/user_detail.html:469 -#: users/templates/users/user_detail.html:537 +#: users/templates/users/user_detail.html:445 +#: users/templates/users/user_detail.html:471 +#: users/templates/users/user_detail.html:539 #: users/templates/users/user_group_list.html:118 #: users/templates/users/user_list.html:254 #: xpack/plugins/interface/templates/interface/interface.html:101 @@ -2214,11 +2214,11 @@ msgstr "操作" msgid "Filename" msgstr "文件名" -#: audits/models.py:24 audits/models.py:77 +#: audits/models.py:24 audits/models.py:78 #: audits/templates/audits/ftp_log_list.html:79 #: ops/templates/ops/command_execution_list.html:68 #: ops/templates/ops/task_list.html:15 -#: users/templates/users/user_detail.html:515 +#: users/templates/users/user_detail.html:517 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:14 #: xpack/plugins/cloud/api.py:61 msgid "Success" @@ -2243,12 +2243,12 @@ msgstr "资源" msgid "Change by" msgstr "修改者" -#: audits/models.py:71 users/templates/users/user_detail.html:98 +#: audits/models.py:71 users/templates/users/user_detail.html:99 msgid "Disabled" msgstr "禁用" #: audits/models.py:72 settings/models.py:33 -#: users/templates/users/user_detail.html:96 +#: users/templates/users/user_detail.html:97 msgid "Enabled" msgstr "启用" @@ -2256,43 +2256,43 @@ msgstr "启用" msgid "-" msgstr "" -#: audits/models.py:78 xpack/plugins/cloud/models.py:264 +#: audits/models.py:79 xpack/plugins/cloud/models.py:264 #: xpack/plugins/cloud/models.py:287 msgid "Failed" msgstr "失败" -#: audits/models.py:82 +#: audits/models.py:83 msgid "Login type" msgstr "登录方式" -#: audits/models.py:83 +#: audits/models.py:84 msgid "Login ip" msgstr "登录IP" -#: audits/models.py:84 +#: audits/models.py:85 msgid "Login city" msgstr "登录城市" -#: audits/models.py:85 +#: audits/models.py:86 msgid "User agent" msgstr "Agent" -#: audits/models.py:86 audits/templates/audits/login_log_list.html:62 +#: audits/models.py:87 audits/templates/audits/login_log_list.html:62 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: users/forms.py:174 users/models/user.py:404 #: users/templates/users/first_login.html:45 msgid "MFA" msgstr "MFA" -#: audits/models.py:87 audits/templates/audits/login_log_list.html:63 -#: xpack/plugins/change_auth_plan/models.py:416 +#: audits/models.py:88 audits/templates/audits/login_log_list.html:63 +#: xpack/plugins/change_auth_plan/models.py:423 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:15 #: xpack/plugins/cloud/models.py:278 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:69 msgid "Reason" msgstr "原因" -#: audits/models.py:88 audits/templates/audits/login_log_list.html:64 +#: audits/models.py:89 audits/templates/audits/login_log_list.html:64 #: orders/templates/orders/login_confirm_order_detail.html:35 #: orders/templates/orders/login_confirm_order_list.html:17 #: orders/templates/orders/login_confirm_order_list.html:91 @@ -2302,7 +2302,7 @@ msgstr "原因" msgid "Status" msgstr "状态" -#: audits/models.py:89 +#: audits/models.py:90 msgid "Date login" msgstr "登录日期" @@ -2314,8 +2314,8 @@ msgstr "登录日期" #: perms/templates/perms/asset_permission_detail.html:86 #: perms/templates/perms/remote_app_permission_detail.html:78 #: terminal/models.py:167 terminal/templates/terminal/session_list.html:34 -#: xpack/plugins/change_auth_plan/models.py:249 -#: xpack/plugins/change_auth_plan/models.py:419 +#: xpack/plugins/change_auth_plan/models.py:250 +#: xpack/plugins/change_auth_plan/models.py:426 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:59 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:17 #: xpack/plugins/gathered_user/models.py:143 @@ -2391,9 +2391,7 @@ msgstr "登录日志" msgid "Command execution log" msgstr "命令执行" -#: authentication/api/auth.py:58 authentication/api/token.py:45 -#: authentication/templates/authentication/login.html:52 -#: authentication/templates/authentication/xpack_login.html:77 +#: authentication/api/auth.py:58 msgid "Log in frequently and try again later" msgstr "登录频繁, 稍后重试" @@ -2409,18 +2407,6 @@ msgstr "请先进行用户名和密码验证" msgid "MFA certification failed" msgstr "MFA认证失败" -#: authentication/api/auth.py:222 -msgid "No order found or order expired" -msgstr "没有找到工单,或者已过期" - -#: authentication/api/auth.py:228 -msgid "Order was rejected by {}" -msgstr "工单被拒绝 {}" - -#: authentication/api/token.py:81 -msgid "MFA required" -msgstr "" - #: authentication/backends/api.py:53 msgid "Invalid signature header. No credentials provided." msgstr "" @@ -2472,49 +2458,75 @@ msgstr "" msgid "Invalid token or cache refreshed." msgstr "" -#: authentication/const.py:6 +#: authentication/errors.py:20 msgid "Username/password check failed" msgstr "用户名/密码 校验失败" -#: authentication/const.py:7 +#: authentication/errors.py:21 msgid "MFA authentication failed" msgstr "MFA 认证失败" -#: authentication/const.py:8 +#: authentication/errors.py:22 msgid "Username does not exist" msgstr "用户名不存在" -#: authentication/const.py:9 +#: authentication/errors.py:23 msgid "Password expired" -msgstr "密码过期" +msgstr "密码已过期" -#: authentication/const.py:10 +#: authentication/errors.py:24 msgid "Disabled or expired" msgstr "禁用或失效" -#: authentication/forms.py:21 -msgid "" -"The username or password you entered is incorrect, please enter it again." -msgstr "您输入的用户名或密码不正确,请重新输入。" - -#: authentication/forms.py:24 +#: authentication/errors.py:25 msgid "This account is inactive." -msgstr "此账户无效" +msgstr "此账户已禁用" -#: authentication/forms.py:26 +#: authentication/errors.py:28 +msgid "No session found, check your cookie" +msgstr "会话已变更,刷新页面" + +#: authentication/errors.py:30 #, python-brace-format msgid "" +"The username or password you entered is incorrect, please enter it again. " "You can also try {times_try} times (The account will be temporarily locked " "for {block_time} minutes)" -msgstr "您还可以尝试 {times_try} 次(账号将被临时锁定 {block_time} 分钟)" +msgstr "" +"您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将" +"被临时 锁定 {block_time} 分钟)" -#: authentication/forms.py:30 +#: authentication/errors.py:36 msgid "" "The account has been locked (please contact admin to unlock it or try again " "after {} minutes)" msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)" -#: authentication/forms.py:66 users/forms.py:21 +#: authentication/errors.py:39 users/views/user.py:393 users/views/user.py:418 +msgid "MFA code invalid, or ntp sync server time" +msgstr "MFA验证码不正确,或者服务器端时间不对" + +#: authentication/errors.py:41 +msgid "MFA required" +msgstr "" + +#: authentication/errors.py:42 +msgid "Login confirm required" +msgstr "需要登录复核" + +#: authentication/errors.py:43 +msgid "Wait login confirm order for accept" +msgstr "等待登录复核处理" + +#: authentication/errors.py:44 +msgid "Login confirm order was rejected" +msgstr "登录已被拒绝" + +#: authentication/errors.py:45 +msgid "Order not found" +msgstr "没有发现工单" + +#: authentication/forms.py:32 users/forms.py:21 msgid "MFA code" msgstr "MFA 验证码" @@ -2522,18 +2534,10 @@ msgstr "MFA 验证码" msgid "Private Token" msgstr "ssh密钥" -#: authentication/models.py:43 -msgid "login_confirm_setting" -msgstr "登录复核设置" - #: authentication/models.py:44 users/templates/users/user_detail.html:265 msgid "Reviewers" msgstr "审批人" -#: authentication/models.py:44 -msgid "review_login_confirm_settings" -msgstr "" - #: authentication/models.py:53 msgid "User login confirm: {}" msgstr "用户登录复核: {}" @@ -2572,14 +2576,14 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:339 users/templates/users/user_profile.html:94 +#: users/models/user.py:335 users/templates/users/user_profile.html:94 #: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:166 msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:340 users/templates/users/user_profile.html:92 +#: users/models/user.py:336 users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:170 msgid "Enable" msgstr "启用" @@ -2640,39 +2644,34 @@ msgid "Changes the world, starting with a little bit." msgstr "改变世界,从一点点开始。" #: authentication/templates/authentication/login.html:46 -#: authentication/templates/authentication/login.html:73 -#: authentication/templates/authentication/xpack_login.html:101 +#: authentication/templates/authentication/login.html:68 +#: authentication/templates/authentication/xpack_login.html:96 #: templates/_header_bar.html:83 msgid "Login" msgstr "登录" -#: authentication/templates/authentication/login.html:54 -#: authentication/templates/authentication/xpack_login.html:80 -msgid "The user password has expired" -msgstr "用户密码已过期" - -#: authentication/templates/authentication/login.html:57 -#: authentication/templates/authentication/xpack_login.html:83 +#: authentication/templates/authentication/login.html:52 +#: authentication/templates/authentication/xpack_login.html:78 msgid "Captcha invalid" msgstr "验证码错误" -#: authentication/templates/authentication/login.html:84 -#: authentication/templates/authentication/xpack_login.html:105 +#: authentication/templates/authentication/login.html:79 +#: authentication/templates/authentication/xpack_login.html:100 #: users/templates/users/forgot_password.html:10 #: users/templates/users/forgot_password.html:25 msgid "Forgot password" msgstr "忘记密码" -#: authentication/templates/authentication/login.html:91 +#: authentication/templates/authentication/login.html:86 msgid "More login options" msgstr "更多登录方式" -#: authentication/templates/authentication/login.html:95 +#: authentication/templates/authentication/login.html:90 msgid "Keycloak" msgstr "" #: authentication/templates/authentication/login_otp.html:46 -#: users/templates/users/user_detail.html:91 +#: users/templates/users/user_detail.html:92 #: users/templates/users/user_profile.html:87 msgid "MFA certification" msgstr "MFA认证" @@ -2721,16 +2720,11 @@ msgstr "返回" msgid "Welcome back, please enter username and password to login" msgstr "欢迎回来,请输入用户名和密码登录" -#: authentication/views/login.py:82 +#: authentication/views/login.py:80 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:156 users/views/user.py:393 -#: users/views/user.py:418 -msgid "MFA code invalid, or ntp sync server time" -msgstr "MFA验证码不正确,或者服务器端时间不对" - -#: authentication/views/login.py:226 +#: authentication/views/login.py:192 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -2738,15 +2732,15 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\n" " 不要关闭本页面" -#: authentication/views/login.py:231 +#: authentication/views/login.py:197 msgid "No order found" msgstr "没有发现工单" -#: authentication/views/login.py:254 +#: authentication/views/login.py:220 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:255 +#: authentication/views/login.py:221 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" @@ -2930,8 +2924,8 @@ msgstr "完成时间" #: ops/models/adhoc.py:357 ops/templates/ops/adhoc_history.html:57 #: ops/templates/ops/task_history.html:63 ops/templates/ops/task_list.html:17 -#: xpack/plugins/change_auth_plan/models.py:252 -#: xpack/plugins/change_auth_plan/models.py:422 +#: xpack/plugins/change_auth_plan/models.py:253 +#: xpack/plugins/change_auth_plan/models.py:429 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:58 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:16 #: xpack/plugins/gathered_user/models.py:146 @@ -3305,11 +3299,11 @@ msgstr "" "
\n" " " -#: orders/utils.py:52 +#: orders/utils.py:48 msgid "Order has been reply" msgstr "工单已被回复" -#: orders/utils.py:53 +#: orders/utils.py:49 #, python-brace-format msgid "" "\n" @@ -3418,7 +3412,7 @@ msgstr "资产授权" #: perms/models/base.py:53 #: perms/templates/perms/asset_permission_detail.html:90 #: perms/templates/perms/remote_app_permission_detail.html:82 -#: users/models/user.py:420 users/templates/users/user_detail.html:107 +#: users/models/user.py:420 users/templates/users/user_detail.html:108 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" @@ -3982,7 +3976,7 @@ msgid "Please submit the LDAP configuration before import" msgstr "请先提交LDAP配置再进行导入" #: settings/templates/settings/_ldap_list_users_modal.html:32 -#: users/models/user.py:384 users/templates/users/user_detail.html:71 +#: users/models/user.py:384 users/templates/users/user_detail.html:72 #: users/templates/users/user_profile.html:59 msgid "Email" msgstr "邮件" @@ -4775,7 +4769,7 @@ msgstr "不能再该页面重置MFA, 请去个人信息页面重置" #: users/forms.py:32 users/models/user.py:392 #: users/templates/users/_select_user_modal.html:15 -#: users/templates/users/user_detail.html:87 +#: users/templates/users/user_detail.html:88 #: users/templates/users/user_list.html:37 #: users/templates/users/user_profile.html:55 msgid "Role" @@ -4818,7 +4812,7 @@ msgstr "生成重置密码链接,通过邮件发送给用户" msgid "Set password" msgstr "设置密码" -#: users/forms.py:132 xpack/plugins/change_auth_plan/models.py:88 +#: users/forms.py:132 xpack/plugins/change_auth_plan/models.py:89 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:51 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:69 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:57 @@ -4892,28 +4886,28 @@ msgstr "选择用户" msgid "User auth from {}, go there change password" msgstr "用户认证源来自 {}, 请去相应系统修改密码" -#: users/models/user.py:135 users/models/user.py:517 +#: users/models/user.py:131 users/models/user.py:517 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:137 +#: users/models/user.py:133 msgid "Application" msgstr "应用程序" -#: users/models/user.py:138 xpack/plugins/orgs/forms.py:30 +#: users/models/user.py:134 xpack/plugins/orgs/forms.py:30 #: xpack/plugins/orgs/templates/orgs/org_list.html:14 msgid "Auditor" msgstr "审计员" -#: users/models/user.py:148 +#: users/models/user.py:144 msgid "Org admin" msgstr "组织管理员" -#: users/models/user.py:150 +#: users/models/user.py:146 msgid "Org auditor" msgstr "组织审计员" -#: users/models/user.py:341 users/templates/users/user_profile.html:90 +#: users/models/user.py:337 users/templates/users/user_profile.html:90 msgid "Force enable" msgstr "强制启用" @@ -4921,11 +4915,11 @@ msgstr "强制启用" msgid "Avatar" msgstr "头像" -#: users/models/user.py:398 users/templates/users/user_detail.html:82 +#: users/models/user.py:398 users/templates/users/user_detail.html:83 msgid "Wechat" msgstr "微信" -#: users/models/user.py:427 users/templates/users/user_detail.html:103 +#: users/models/user.py:427 users/templates/users/user_detail.html:104 #: users/templates/users/user_list.html:39 #: users/templates/users/user_profile.html:102 msgid "Source" @@ -5107,7 +5101,7 @@ msgid "Always young, always with tears in my eyes. Stay foolish Stay hungry" msgstr "永远年轻,永远热泪盈眶 stay foolish stay hungry" #: users/templates/users/reset_password.html:46 -#: users/templates/users/user_detail.html:430 users/utils.py:83 +#: users/templates/users/user_detail.html:432 users/utils.py:83 msgid "Reset password" msgstr "重置密码" @@ -5176,102 +5170,102 @@ msgstr "很强" msgid "Create user" msgstr "创建用户" -#: users/templates/users/user_detail.html:19 +#: users/templates/users/user_detail.html:20 #: users/templates/users/user_granted_asset.html:18 users/views/user.py:190 msgid "User detail" msgstr "用户详情" -#: users/templates/users/user_detail.html:22 +#: users/templates/users/user_detail.html:23 #: users/templates/users/user_granted_asset.html:21 #: users/templates/users/user_group_detail.html:25 #: users/templates/users/user_group_granted_asset.html:21 msgid "Asset granted" msgstr "授权的资产" -#: users/templates/users/user_detail.html:94 +#: users/templates/users/user_detail.html:95 msgid "Force enabled" msgstr "强制启用" -#: users/templates/users/user_detail.html:119 +#: users/templates/users/user_detail.html:120 #: users/templates/users/user_profile.html:110 msgid "Last login" msgstr "最后登录" -#: users/templates/users/user_detail.html:124 +#: users/templates/users/user_detail.html:125 #: users/templates/users/user_profile.html:115 msgid "Last password updated" msgstr "最后更新密码" -#: users/templates/users/user_detail.html:160 +#: users/templates/users/user_detail.html:161 msgid "Force enabled MFA" msgstr "强制启用MFA" -#: users/templates/users/user_detail.html:175 +#: users/templates/users/user_detail.html:176 msgid "Reset MFA" msgstr "重置MFA" -#: users/templates/users/user_detail.html:184 +#: users/templates/users/user_detail.html:185 msgid "Send reset password mail" msgstr "发送重置密码邮件" -#: users/templates/users/user_detail.html:187 -#: users/templates/users/user_detail.html:197 +#: users/templates/users/user_detail.html:188 +#: users/templates/users/user_detail.html:198 msgid "Send" msgstr "发送" -#: users/templates/users/user_detail.html:194 +#: users/templates/users/user_detail.html:195 msgid "Send reset ssh key mail" msgstr "发送重置密钥邮件" -#: users/templates/users/user_detail.html:203 -#: users/templates/users/user_detail.html:518 +#: users/templates/users/user_detail.html:204 +#: users/templates/users/user_detail.html:520 msgid "Unblock user" msgstr "解除登录限制" -#: users/templates/users/user_detail.html:206 +#: users/templates/users/user_detail.html:207 msgid "Unblock" msgstr "解除" -#: users/templates/users/user_detail.html:373 +#: users/templates/users/user_detail.html:375 msgid "Goto profile page enable MFA" msgstr "请去个人信息页面启用自己的MFA" -#: users/templates/users/user_detail.html:429 +#: users/templates/users/user_detail.html:431 msgid "An e-mail has been sent to the user`s mailbox." msgstr "已发送邮件到用户邮箱" -#: users/templates/users/user_detail.html:440 +#: users/templates/users/user_detail.html:442 msgid "This will reset the user password and send a reset mail" msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" -#: users/templates/users/user_detail.html:455 +#: users/templates/users/user_detail.html:457 msgid "" "The reset-ssh-public-key E-mail has been sent successfully. Please inform " "the user to update his new ssh public key." msgstr "重设密钥邮件将会发送到用户邮箱" -#: users/templates/users/user_detail.html:456 +#: users/templates/users/user_detail.html:458 msgid "Reset SSH public key" msgstr "重置SSH密钥" -#: users/templates/users/user_detail.html:466 +#: users/templates/users/user_detail.html:468 msgid "This will reset the user public key and send a reset mail" msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱" -#: users/templates/users/user_detail.html:484 +#: users/templates/users/user_detail.html:486 msgid "Successfully updated the SSH public key." msgstr "更新ssh密钥成功" -#: users/templates/users/user_detail.html:485 -#: users/templates/users/user_detail.html:489 +#: users/templates/users/user_detail.html:487 +#: users/templates/users/user_detail.html:491 msgid "User SSH public key update" msgstr "ssh密钥" -#: users/templates/users/user_detail.html:534 +#: users/templates/users/user_detail.html:536 msgid "After unlocking the user, the user can log in normally." msgstr "解除用户登录限制后,此用户即可正常登录" -#: users/templates/users/user_detail.html:548 +#: users/templates/users/user_detail.html:550 msgid "Reset user MFA success" msgstr "重置用户MFA成功" @@ -5754,8 +5748,8 @@ msgstr "" "具)
注意: 如果同时设置了定期执行和周期执行,优先使用定期执行" #: xpack/plugins/change_auth_plan/meta.py:9 -#: xpack/plugins/change_auth_plan/models.py:116 -#: xpack/plugins/change_auth_plan/models.py:256 +#: xpack/plugins/change_auth_plan/models.py:117 +#: xpack/plugins/change_auth_plan/models.py:257 #: xpack/plugins/change_auth_plan/views.py:33 #: xpack/plugins/change_auth_plan/views.py:50 #: xpack/plugins/change_auth_plan/views.py:74 @@ -5766,20 +5760,20 @@ msgstr "" msgid "Change auth plan" msgstr "改密计划" -#: xpack/plugins/change_auth_plan/models.py:57 +#: xpack/plugins/change_auth_plan/models.py:58 msgid "Custom password" msgstr "自定义密码" -#: xpack/plugins/change_auth_plan/models.py:58 +#: xpack/plugins/change_auth_plan/models.py:59 msgid "All assets use the same random password" msgstr "所有资产使用相同的随机密码" -#: xpack/plugins/change_auth_plan/models.py:59 +#: xpack/plugins/change_auth_plan/models.py:60 msgid "All assets use different random password" msgstr "所有资产使用不同的随机密码" -#: xpack/plugins/change_auth_plan/models.py:78 -#: xpack/plugins/change_auth_plan/models.py:147 +#: xpack/plugins/change_auth_plan/models.py:79 +#: xpack/plugins/change_auth_plan/models.py:148 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:100 #: xpack/plugins/cloud/models.py:165 xpack/plugins/cloud/models.py:219 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:91 @@ -5788,8 +5782,8 @@ msgstr "所有资产使用不同的随机密码" msgid "Cycle perform" msgstr "周期执行" -#: xpack/plugins/change_auth_plan/models.py:83 -#: xpack/plugins/change_auth_plan/models.py:145 +#: xpack/plugins/change_auth_plan/models.py:84 +#: xpack/plugins/change_auth_plan/models.py:146 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:92 #: xpack/plugins/cloud/models.py:170 xpack/plugins/cloud/models.py:217 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:83 @@ -5798,37 +5792,37 @@ msgstr "周期执行" msgid "Regularly perform" msgstr "定期执行" -#: xpack/plugins/change_auth_plan/models.py:92 +#: xpack/plugins/change_auth_plan/models.py:93 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:74 msgid "Password rules" msgstr "密码规则" -#: xpack/plugins/change_auth_plan/models.py:212 +#: xpack/plugins/change_auth_plan/models.py:213 msgid "* For security, do not change {} user's password" msgstr "* 为了安全,禁止更改 {} 用户的密码" -#: xpack/plugins/change_auth_plan/models.py:216 +#: xpack/plugins/change_auth_plan/models.py:217 msgid "Assets is empty, please add the asset" msgstr "资产为空,请添加资产" -#: xpack/plugins/change_auth_plan/models.py:260 +#: xpack/plugins/change_auth_plan/models.py:261 msgid "Change auth plan snapshot" msgstr "改密计划快照" -#: xpack/plugins/change_auth_plan/models.py:275 -#: xpack/plugins/change_auth_plan/models.py:426 +#: xpack/plugins/change_auth_plan/models.py:276 +#: xpack/plugins/change_auth_plan/models.py:433 msgid "Change auth plan execution" msgstr "改密计划执行" -#: xpack/plugins/change_auth_plan/models.py:435 +#: xpack/plugins/change_auth_plan/models.py:442 msgid "Change auth plan execution subtask" msgstr "改密计划执行子任务" -#: xpack/plugins/change_auth_plan/models.py:453 +#: xpack/plugins/change_auth_plan/models.py:460 msgid "Authentication failed" msgstr "认证失败" -#: xpack/plugins/change_auth_plan/models.py:455 +#: xpack/plugins/change_auth_plan/models.py:462 msgid "Connection timeout" msgstr "连接超时" @@ -6438,6 +6432,27 @@ msgstr "密码匣子" msgid "vault create" msgstr "创建" +#~ msgid "" +#~ "The username or password you entered is incorrect, please enter it again." +#~ msgstr "您输入的用户名或密码不正确,请重新输入。" + +#~ msgid "" +#~ "You can also try {times_try} times (The account will be temporarily " +#~ "locked for {block_time} minutes)" +#~ msgstr "您还可以尝试 {times_try} 次(账号将被临时锁定 {block_time} 分钟)" + +#~ msgid "No order found or order expired" +#~ msgstr "没有找到工单,或者已过期" + +#~ msgid "Order was rejected by {}" +#~ msgstr "工单被拒绝 {}" + +#~ msgid "login_confirm_setting" +#~ msgstr "登录复核设置" + +#~ msgid "The user password has expired" +#~ msgstr "用户密码已过期" + #~ msgid "Recipient" #~ msgstr "收件人" diff --git a/apps/perms/templates/perms/asset_permission_create_update.html b/apps/perms/templates/perms/asset_permission_create_update.html index 5e4c650d0..0c05de831 100644 --- a/apps/perms/templates/perms/asset_permission_create_update.html +++ b/apps/perms/templates/perms/asset_permission_create_update.html @@ -100,16 +100,6 @@ - \ No newline at end of file + diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 36a60abc8..c987fbd91 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -62,10 +62,6 @@ class AuthMixin: def can_use_ssh_key_login(self): return settings.TERMINAL_PUBLIC_KEY_AUTH - def check_otp(self, code): - from ..utils import check_otp_code - return check_otp_code(self.otp_secret_key, code) - def is_public_key_valid(self): """ Check if the user's ssh public key is valid. @@ -362,6 +358,10 @@ class MFAMixin: self.otp_level = 0 self.otp_secret_key = None + def check_otp(self, code): + from ..utils import check_otp_code + return check_otp_code(self.otp_secret_key, code) + class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): SOURCE_LOCAL = 'local' diff --git a/apps/users/templates/users/_user.html b/apps/users/templates/users/_user.html index 192dbfb70..018a165fa 100644 --- a/apps/users/templates/users/_user.html +++ b/apps/users/templates/users/_user.html @@ -56,6 +56,7 @@ {% endblock %} {% block custom_foot_js %} + @@ -72,19 +73,9 @@ $(groups_id).closest('.form-group').removeClass('hidden'); }} - var dateOptions = { - singleDatePicker: true, - showDropdowns: true, - timePicker: true, - timePicker24Hour: true, - autoApply: true, - locale: { - format: 'YYYY-MM-DD HH:mm' - } - }; $(document).ready(function () { $('.select2').select2(); - $('#id_date_expired').daterangepicker(dateOptions); + initDateRangePicker('#id_date_expired'); var mfa_radio = $('#id_otp_level'); mfa_radio.addClass("form-inline"); mfa_radio.children().css("margin-right","15px"); diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html index eed7c8409..651763140 100644 --- a/apps/users/templates/users/user_detail.html +++ b/apps/users/templates/users/user_detail.html @@ -212,7 +212,7 @@
- {% if user_object.is_current_org_admin or user_object.is_superuser %} + {% if user.is_current_org_admin or user.is_superuser %}
{% trans 'User group' %} diff --git a/apps/users/urls/api_urls.py b/apps/users/urls/api_urls.py index e40f5d3a7..f0fe3db6f 100644 --- a/apps/users/urls/api_urls.py +++ b/apps/users/urls/api_urls.py @@ -20,9 +20,6 @@ router.register(r'users-groups-relations', api.UserUserGroupRelationViewSet, 'us urlpatterns = [ path('connection-token/', auth_api.UserConnectionTokenApi.as_view(), name='connection-token'), - path('auth/', auth_api.UserAuthApi.as_view(), name='user-auth'), - path('otp/auth/', auth_api.UserOtpAuthApi.as_view(), name='user-otp-auth'), - path('profile/', api.UserProfileApi.as_view(), name='user-profile'), path('otp/reset/', api.UserResetOTPApi.as_view(), name='my-otp-reset'), path('users//otp/reset/', api.UserResetOTPApi.as_view(), name='user-reset-otp'), diff --git a/apps/users/utils.py b/apps/users/utils.py index 9ab2c914e..a955350a9 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -218,9 +218,11 @@ def set_tmp_user_to_cache(request, user, ttl=3600): def redirect_user_first_login_or_index(request, redirect_field_name): if request.user.is_first_login: return reverse('users:user-first-login') - return request.POST.get( - redirect_field_name, - request.GET.get(redirect_field_name, reverse('index'))) + url_in_post = request.POST.get(redirect_field_name) + if url_in_post: + return url_in_post + url_in_get = request.GET.get(redirect_field_name, reverse('index')) + return url_in_get def generate_otp_uri(request, issuer="Jumpserver"):