From d0ba67ed503011a8cadbee60dddb138d85b2d8d5 Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 30 Oct 2019 13:18:11 +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=E4=BA=8C=E6=AC=A1=E5=AE=A1=E6=A0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/views/domain.py | 2 +- apps/authentication/api/auth.py | 30 +- apps/authentication/api/token.py | 7 +- apps/authentication/models.py | 11 +- .../authentication/login_wait_confirm.html | 114 ++++-- apps/authentication/urls/api_urls.py | 8 +- apps/authentication/urls/view_urls.py | 4 +- apps/authentication/utils.py | 7 +- apps/authentication/views/login.py | 52 ++- apps/jumpserver/urls.py | 28 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 80725 -> 81958 bytes apps/locale/zh/LC_MESSAGES/django.po | 375 +++++++++++++----- apps/orders/api.py | 38 ++ apps/orders/apps.py | 4 + apps/orders/models.py | 49 ++- apps/orders/serializers.py | 72 ++++ apps/orders/signals_handler.py | 59 +++ .../orders/login_confirm_order_detail.html | 137 +++++++ .../orders/login_confirm_order_list.html | 91 +++++ apps/orders/urls/__init__.py | 2 + apps/orders/urls/api_urls.py | 20 + apps/orders/urls/views_urls.py | 11 + apps/orders/utils.py | 2 + apps/orders/views.py | 35 +- apps/static/js/jumpserver.js | 2 +- apps/templates/_nav.html | 12 +- apps/users/utils.py | 5 +- 27 files changed, 972 insertions(+), 205 deletions(-) create mode 100644 apps/orders/api.py create mode 100644 apps/orders/serializers.py create mode 100644 apps/orders/signals_handler.py create mode 100644 apps/orders/templates/orders/login_confirm_order_detail.html create mode 100644 apps/orders/templates/orders/login_confirm_order_list.html create mode 100644 apps/orders/urls/__init__.py create mode 100644 apps/orders/urls/api_urls.py create mode 100644 apps/orders/urls/views_urls.py create mode 100644 apps/orders/utils.py diff --git a/apps/assets/views/domain.py b/apps/assets/views/domain.py index 7b4dcfcce..67626b094 100644 --- a/apps/assets/views/domain.py +++ b/apps/assets/views/domain.py @@ -7,7 +7,7 @@ from django.views.generic.detail import SingleObjectMixin from django.utils.translation import ugettext_lazy as _ from django.urls import reverse_lazy, reverse -from common.permissions import PermissionsMixin ,IsOrgAdmin +from common.permissions import PermissionsMixin, IsOrgAdmin from common.const import create_success_msg, update_success_msg from common.utils import get_object_or_none from ..models import Domain, Gateway diff --git a/apps/authentication/api/auth.py b/apps/authentication/api/auth.py index 101d6436e..8b1ab69c0 100644 --- a/apps/authentication/api/auth.py +++ b/apps/authentication/api/auth.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # - import uuid import time @@ -8,19 +7,17 @@ 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 +from common.utils import get_logger, get_request_ip, get_object_or_none from common.permissions import IsOrgAdminOrAppUser, IsValidUser from orgs.mixins.api import RootOrgViewMixin from users.serializers import UserSerializer from users.models import User from assets.models import Asset, SystemUser -from audits.models import UserLoginLog as LoginLog from users.utils import ( check_otp_code, increase_login_failed_count, is_block_login, clean_failed_count @@ -33,7 +30,7 @@ from ..signals import post_auth_success, post_auth_failed logger = get_logger(__name__) __all__ = [ 'UserAuthApi', 'UserConnectionTokenApi', 'UserOtpAuthApi', - 'UserOtpVerifyApi', + 'UserOtpVerifyApi', 'UserOrderAcceptAuthApi', ] @@ -209,3 +206,26 @@ class UserOtpVerifyApi(CreateAPIView): 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/token.py b/apps/authentication/api/token.py index f44e93609..8855ac1c9 100644 --- a/apps/authentication/api/token.py +++ b/apps/authentication/api/token.py @@ -71,7 +71,8 @@ class TokenCreateApi(CreateAPIView): raise MFARequiredError() self.send_auth_signal(success=True, user=user) clean_failed_count(username, ip) - return super().create(request, *args, **kwargs) + resp = super().create(request, *args, **kwargs) + return resp except AuthFailedError as e: increase_login_failed_count(username, ip) self.send_auth_signal(success=False, user=user, username=username, reason=str(e)) @@ -80,8 +81,8 @@ class TokenCreateApi(CreateAPIView): msg = _("MFA required") seed = uuid.uuid4().hex cache.set(seed, user.username, 300) - resp = {'msg': msg, "choices": ["otp"], "req": seed} - return Response(resp, status=300) + data = {'msg': msg, "choices": ["otp"], "req": seed} + return Response(data, status=300) def send_auth_signal(self, success=True, user=None, username='', reason=''): if success: diff --git a/apps/authentication/models.py b/apps/authentication/models.py index 21fb2aafd..bc92eb8b5 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -49,8 +49,8 @@ class LoginConfirmSetting(CommonModelMixin): return get_object_or_none(cls, user=user) def create_confirm_order(self, request=None): - from orders.models import Order - title = _('User login request confirm: {}'.format(self.user)) + from orders.models import LoginConfirmOrder + title = _('User login request: {}'.format(self.user)) if request: remote_addr = get_request_ip(request) city = get_ip_city(remote_addr) @@ -58,14 +58,17 @@ class LoginConfirmSetting(CommonModelMixin): self.user, remote_addr, city, timezone.now() ) else: + city = '' + remote_addr = '' body = '' reviewer = self.reviewers.all() reviewer_names = ','.join([u.name for u in reviewer]) - order = Order.objects.create( + order = LoginConfirmOrder.objects.create( user=self.user, user_display=str(self.user), title=title, body=body, + city=city, ip=remote_addr, assignees_display=reviewer_names, - type=Order.TYPE_LOGIN_REQUEST, + type=LoginConfirmOrder.TYPE_LOGIN_CONFIRM, ) order.assignees.set(reviewer) return order diff --git a/apps/authentication/templates/authentication/login_wait_confirm.html b/apps/authentication/templates/authentication/login_wait_confirm.html index 54526427e..0a14e8515 100644 --- a/apps/authentication/templates/authentication/login_wait_confirm.html +++ b/apps/authentication/templates/authentication/login_wait_confirm.html @@ -6,13 +6,11 @@ - + {{ title }} - {% include '_head_css_js.html' %} - + @@ -29,23 +27,29 @@

-
- Wait for Guanghongwei confirm, You also can copy link to her/his
- Don't close .... +
+ {{ msg|safe }}
- +{% include '_foot_js.html' %} + diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index 68dd8eeaa..a90b328cc 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -1,20 +1,15 @@ # coding:utf-8 # - -from __future__ import absolute_import - from django.urls import path from rest_framework.routers import DefaultRouter from .. import api +app_name = 'authentication' router = DefaultRouter() router.register('access-keys', api.AccessKeyViewSet, 'access-key') -app_name = 'authentication' - - urlpatterns = [ # path('token/', api.UserToken.as_view(), name='user-token'), path('auth/', api.UserAuthApi.as_view(), name='user-auth'), @@ -24,6 +19,7 @@ urlpatterns = [ 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') ] urlpatterns += router.urls diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index 0981f1b09..64d01ae34 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -16,7 +16,7 @@ urlpatterns = [ # login path('login/', views.UserLoginView.as_view(), name='login'), path('login/otp/', views.UserLoginOtpView.as_view(), name='login-otp'), - path('login/continue/', views.UserLoginContinueView.as_view(), name='login-continue'), - path('login/wait/', views.UserLoginWaitConfirmView.as_view(), name='login-wait'), + path('login/wait-confirm/', views.UserLoginWaitConfirmView.as_view(), name='login-wait-confirm'), + path('login/guard/', views.UserLoginGuardView.as_view(), name='login-guard'), path('logout/', views.UserLogoutView.as_view(), name='logout'), ] diff --git a/apps/authentication/utils.py b/apps/authentication/utils.py index 70c7e52fa..85b486bf3 100644 --- a/apps/authentication/utils.py +++ b/apps/authentication/utils.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext as _, ugettext_lazy 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 +from common.utils import ( + get_ip_city, get_object_or_none, validate_ip, get_request_ip +) from users.models import User from . import const diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index afff9dd45..646268eea 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import os +import datetime from django.core.cache import cache from django.contrib.auth import login as auth_login, logout as auth_logout from django.http import HttpResponse @@ -12,17 +13,18 @@ from django.utils.translation import ugettext as _ from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_protect from django.views.decorators.debug import sensitive_post_parameters -from django.views.generic.base import TemplateView, View, RedirectView +from django.views.generic.base import TemplateView, RedirectView from django.views.generic.edit import FormView from django.conf import settings -from common.utils import get_request_ip +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, - redirect_user_first_login_or_index, + redirect_user_first_login_or_index ) +from ..models import LoginConfirmSetting from ..signals import post_auth_success, post_auth_failed from .. import forms from .. import const @@ -30,7 +32,7 @@ from .. import const __all__ = [ 'UserLoginView', 'UserLoginOtpView', 'UserLogoutView', - 'UserLoginContinueView', 'UserLoginWaitConfirmView', + 'UserLoginGuardView', 'UserLoginWaitConfirmView', ] @@ -91,7 +93,7 @@ class UserLoginView(FormView): # 登陆成功,清除缓存计数 clean_failed_count(username, ip) self.request.session['auth_password'] = '1' - return self.redirect_to_continue_view() + return self.redirect_to_guard_view() def form_invalid(self, form): # write login failed log @@ -112,8 +114,8 @@ class UserLoginView(FormView): return super().form_invalid(form) @staticmethod - def redirect_to_continue_view(): - continue_url = reverse('authentication:login-continue') + def redirect_to_guard_view(): + continue_url = reverse('authentication:login-guard') return redirect(continue_url) def get_form_class(self): @@ -144,7 +146,7 @@ class UserLoginOtpView(FormView): if check_otp_code(otp_secret_key, otp_code): self.request.session['auth_otp'] = '1' - return UserLoginView.redirect_to_continue_view() + return UserLoginView.redirect_to_guard_view() else: self.send_auth_signal( success=False, username=user.username, @@ -165,7 +167,7 @@ class UserLoginOtpView(FormView): ) -class UserLoginContinueView(RedirectView): +class UserLoginGuardView(RedirectView): redirect_field_name = 'next' def get_redirect_url(self, *args, **kwargs): @@ -173,11 +175,18 @@ class UserLoginContinueView(RedirectView): return reverse('authentication:login') user = get_user_or_tmp_user(self.request) + # 启用并设置了otp if user.otp_enabled and user.otp_secret_key and \ not self.request.session.get('auth_otp'): return reverse('authentication:login-otp') - + confirm_setting = LoginConfirmSetting.get_user_confirm_setting(user) + 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') + return url self.login_success(user) + # 启用但是没有设置otp if user.otp_enabled and not user.otp_secret_key: # 1,2,mfa_setting & F return reverse('users:user-otp-enable-authentication') @@ -204,7 +213,28 @@ class UserLoginWaitConfirmView(TemplateView): template_name = 'authentication/login_wait_confirm.html' def get_context_data(self, **kwargs): - return super().get_context_data(**kwargs) + from orders.models import LoginConfirmOrder + order_id = self.request.session.get("auth_order_id") + if not order_id: + order = None + else: + order = get_object_or_none(LoginConfirmOrder, pk=order_id) + context = super().get_context_data(**kwargs) + if order: + order_detail_url = reverse('orders:login-confirm-order-detail', kwargs={'pk': order_id}) + timestamp_created = datetime.datetime.timestamp(order.date_created) + msg = _("""Wait for {} confirm, You also can copy link to her/him
+ Don't close this page""").format(order.assignees_display) + else: + timestamp_created = 0 + order_detail_url = '' + msg = _("No order found") + context.update({ + "msg": msg, + "timestamp": timestamp_created, + "order_detail_url": order_detail_url + }) + return context @method_decorator(never_cache, name='dispatch') diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 129368c47..b9bdb697f 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -13,22 +13,23 @@ from .celery_flower import celery_flower_view from .swagger import get_swagger_view api_v1 = [ - path('users/', include('users.urls.api_urls', namespace='api-users')), - path('assets/', include('assets.urls.api_urls', namespace='api-assets')), - path('perms/', include('perms.urls.api_urls', namespace='api-perms')), - path('terminal/', include('terminal.urls.api_urls', namespace='api-terminal')), - path('ops/', include('ops.urls.api_urls', namespace='api-ops')), - path('audits/', include('audits.urls.api_urls', namespace='api-audits')), - path('orgs/', include('orgs.urls.api_urls', namespace='api-orgs')), - path('settings/', include('settings.urls.api_urls', namespace='api-settings')), - path('authentication/', include('authentication.urls.api_urls', namespace='api-auth')), - path('common/', include('common.urls.api_urls', namespace='api-common')), - path('applications/', include('applications.urls.api_urls', namespace='api-applications')), + path('users/', include('users.urls.api_urls', namespace='api-users')), + path('assets/', include('assets.urls.api_urls', namespace='api-assets')), + path('perms/', include('perms.urls.api_urls', namespace='api-perms')), + path('terminal/', include('terminal.urls.api_urls', namespace='api-terminal')), + path('ops/', include('ops.urls.api_urls', namespace='api-ops')), + path('audits/', include('audits.urls.api_urls', namespace='api-audits')), + path('orgs/', include('orgs.urls.api_urls', namespace='api-orgs')), + path('settings/', include('settings.urls.api_urls', namespace='api-settings')), + path('authentication/', include('authentication.urls.api_urls', namespace='api-auth')), + path('common/', include('common.urls.api_urls', namespace='api-common')), + path('applications/', include('applications.urls.api_urls', namespace='api-applications')), + path('orders/', include('orders.urls.api_urls', namespace='api-orders')), ] api_v2 = [ - path('terminal/', include('terminal.urls.api_urls_v2', namespace='api-terminal-v2')), - path('users/', include('users.urls.api_urls_v2', namespace='api-users-v2')), + path('terminal/', include('terminal.urls.api_urls_v2', namespace='api-terminal-v2')), + path('users/', include('users.urls.api_urls_v2', namespace='api-users-v2')), ] @@ -42,6 +43,7 @@ app_view_patterns = [ path('orgs/', include('orgs.urls.views_urls', namespace='orgs')), path('auth/', include('authentication.urls.view_urls'), name='auth'), path('applications/', include('applications.urls.views_urls', namespace='applications')), + path('orders/', include('orders.urls.views_urls', namespace='orders')), re_path(r'flower/(?P.*)', celery_flower_view, name='flower-view'), ] diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index a6510ca5a41597f874d262fab0c92cdb24ef153b..6f724f936f9dee30ca50475f535c01280cfb31f7 100644 GIT binary patch delta 24950 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 z z9Z@^h3nOtT7QkB=fyocM{VE~z^Y_10G+}crj*+N`aUN>Ib*Ov09d(7rQ4i@=)DHZO zTBzRtZ1k8r%PP!kdrBM@&!ZTR>6pKQ?pB?8@9Ez23HzvY&*cp9KyF1?XH2Yth#6%KW z@kP{DKR|7r|1a*q%%}y0p%$DQv-ofyQJ;{tID26nirUE^Fbp?h5_*_X4P$XyLiQ34GZi98BWcuYAo?!zKRu1h3#eCYbvh-(2zYD|}j#Z}Pi)1%cPx z6KBNK8+;9e+UW;8Tp#X1u17oW%N@?pE$LkE8DSuc#}#hT7_< zsQiBx``vQ$smxHbfLR9Bzq(luwSXqxb`Rf9ycPb71nPrjpvBWr0~eweupITQY_s;G z<^|N2-m>@~)9Jys(;l#cs{T`=EeT#X+dQ^mG<}?D`C?+uHTp!%(U_D=Rs4{0;ixRo@Mc3%tO4= zJZ<@x7)t&>)LW1xf&XBI-4nR~YFI%+pKymzSM-}1Z$2~MqxvPg>l%)Fc1mCXRxxXv z4N>#8Fgu(5%n^6lf8CpjBy=V7QCqXZI-EAITK=BJsqVQGhoUZ^fW>uC&rl20t>|mU zT7I_08_a_qDq7)1^EPThf1)OSZ-(A?`&B?)X&uyp8=?BQM$OX$b>cyo2q$6^oQk^l z^HBZvqUQ0Ow8S~{hBZ7wP4wLI0T0~I{xqlwN}?861vOCzvzxW|F^5@xygALB3S;n$wO4=WE}$`LfnQ)!?2mys9Q8VmH5Z^3ycv_=KJ&OY&;377 zMHAgb-Mi-&Cwt_^VW@?aK}}HJtYJ1ZTbZ3vJJQ?Y0a$|AWBDI38}Tl+>;1n*MfdQo zb@&I95x+w%Eacny8PE@}GsQFG>{x^$np!fIxM^tjt z;W_rkOi$ba<54G^ZSiWdK#>`n@vWTRz~ayTFuB*?)D&Nf=DlX#!Fj|FEgtiNdG!8IQU&Ls2CPJ#aGk|_to@LA7IjOmo3Bw{(?eeJ zS8~`8HQz(je6P&+mJj&Hoj(nF)G-GY{X8y;>97YXKMJ*_Q?Mv5wfLOH@u(AoymHNI z=EPLw3!pyfDxl{10yS@c)F-Ov7585g#F9vf6HpJ+d{lk~YQS2HcVG_U{g%Ij-iHqx zkbi@DvF^X_m(>{5_{HX0)Vw>){rbmXn&7B)IBQqSZ zd={6oxB_baI%ZRAZ)Zk&tkM%R&@tNLNvH{DnoBSb@jBFJ{zc2bHWR;b?`c}pxSXhk zMOa)0HGg%~t!QU?Pd}@Su!f1K@Ar#P19qb(x`_Glchm_Izjc4?rbV@fq4K#gGZsgE zA~vvmZ`4CQ6xDws@@#mV8C0~QA5jw=MV`O2K-~bv%LR*?u5bUT>$EPKyK9d z2=v7|W_>J7+|cx3Th8ySr&67UWbgPkgN?BZ&O$BhE$Rfp@7*{Awa|R1D=KCA8Wy*- z_9*j9oJYS{?1fn!AMaZ>5d%CVma2lQ%njyNtW16n>V)qtpUlUN)1bzMTb#!%Vfo5t zEi6I51{QyX`e2)g-uHhV6`f$E8Hbv9KWYb#qTb^xsMqf;s(-kz+piF+UkRLuHL*FK zN4@9y6Zv>=O<}Vv>Mg34$j5#Eo0HIlZO!he2?t_EoM8FomfvLYA=JG-i(2pli~q9r zq<(HbJ?dEsMcuNzW*I+^doOB{&?jFz)QMwI6Mt`sYU7%Ce{}Z(w#aN15MZ zGQIzCR5WnEH^EbjTH#I9NA6wJP6Q`$&1mMp5b}jkJ6H{MrL|C>Bh64B)pISs2X&qk zs0+A;8T9@?wuXSD?tqM_l~+V9qy>5(PSgovEI-N|hnjdQ>J~1uI1cL&A3=>v733Of z7C=vK8p>GW3)DnCQ7iOVJk{ElnH#VY?YmJ6d~5kc$=nm9MD18si}RrRl|cP$D2F;v zi)6h2T3L4zI^jU`YgB%Yx!l}@nrN@Zhb=y5UN&!@%Pf|h_g@_< zktU8|ngwA`h#_8Ep+SQ4_61 z4cLhK1l(i!BbGmdTJSZC-=Zc=lFB_nO8lHS2kHZ87-q*6sQHhf=0E3^_wNQ3P4LJ% zJh%8i)IIl0?Y3t{T|rLN0!mxl95rDSYUjR0%{SKK$rdj}^;>T7CaL#-yEW`HPnnld zCrmK^HIt=r^I>K&)UBzGT5xwW7PZg?sDKn!g_EA&f-LGZfW-bUJ(gr&@;v=4R`7%>2dt z)x3tZ{Xi%!emYUou~q zPI@;EGDA>1lMS`72-MqC&f*Sc4|9Nrinc^%#jzGIM_s`hbGvyMwa{}G$6Nf&^v~dq z%Ya%)F4X)bEnnX9wJr9vv`QB<8gq<>Pm}STmf~$IuqjpljOPQ5gP!`&0-MeSU5)Q&VpEjZHR81&AE`iiy@!|;HIiaw)n zp{~ry=5|bpI&oQx+h7giF<2H)VHpg_?%twGs0GZy*|^;D<#MjomD`Q;quMK= z=Bbahuru;ox5rsWMc+i8qaL2jdE9%H3w1>Wu@9ET!?+rCulwZn@&2XMBrHh02lci4 z0cxiT<@53W!=)H(M*IU-KqtSu0~N8U-v5hKIx1hl$NRtWo{X)C@1l03NVqkd#l=u3EQ{K?dN>4|BY(B(oJF1Rnzi3CpPDsfTw*~wbe-v6>B)S-si1oiZ{w>Z|E zW-c^;Ft?!=bO`nEU9$XBi~qCskO=oY`A`pIsR)mosBaCeP*>d5;(_K@s4bpu&c{!Q zSD>~y0h{1s)Ius3bMsBj)~E~Yh-&YHI?reil|U*pP$!s=y7wzk@8=pEfxA)fd%5DS zF{laGnLAMv9>q`a66!>$OStpqLbVq$E1_`oLL?I^ktA0rhq~ zM{Q-clJ4h58C1Vk=-o-wC*MHS371%YtEo`NDscpA z%Lkyw4MV*pV^RGlSbh$w-%`u3GPhX%pjY0%_{7JE?RW}p@}8?~_S&=carMMiCz)QOIpzoRDp6SZS+aV{pSE$R3L>W9>Q@k9{uA8sZt0-)SF>Rq3ODO;v<(Nr^AguR5M3_XyvT|G~y4r?0+$ zc#lam>WIh6bUsbdk&#X>M_we)bWl<7E{_$Uq+cs{}|Lqu@2t6 z{~gWfH=Fo6^BktG_+eoQv zZCU91lDk6YpHj4swLTrl zcO{>hlAgAa>c~-x_6u&+sl`0GKbou1NA(r-+s@~YbC|(}Y*LMDL(xwx9sUgX6nD`_ zM;`j|li0as{WEa#A(W@qu0DyW>+}5=^1smUKT3J)|DLv;e!my(sX?u^sdGC%6x=YI;7E+op^ z0IJSRa(pp%){{F#NlZCGPDhOOOUfeV+X9pir~VUuP5YOq&-#LlIZvKnBfP&6og_X$ zdvoF+6LJ2_RCK&1dD80V?F6-n_2pv%>w z_>wWuW0T+#VSZ{gXKmsCdBF=DCfs7Ufmen zk&gPp&;c*e@d|2}{-6w?9H-B2@~KE3rEQ{e#5(?TaVpYBpQbNp|M-ZQxwKIlPbyj# zQJPSm(Q|=y@hATH_{QS?%yN!;dKy2cl;O;`u^6@`U!Ss#c^1>J61g;#bd-Yh@gWyL zDMrahj>F^3V!&tAS~Ez;bqpg{fKJnh|DxX9+SZcOv6NghCh#Sn$#P-tgibi^xv0Ow zP}*P7UJW~AHpXqD=!heiDiM#_6B7Kw=RJng=})UG@guRn+vIG=ZnRyc)FH2<73CLi zjaP%*73wXSc&g>9vyel?`hq{5_Qu4W@H29u%u`x7<~4ziyx58ON9u#{C*sc;P=x{i zQs01+C?OObUC4EzK8Jcz%e5f4jruCaPsAFG*U`dMy&rwbP?C}Jw4w6xahE~mt@Bqno4l%nL8ppLGl>SHK}D6Polpe&|zr2OhGnE%YpKpo?l zFh6A+jXDluL$~Vvb1DOFQM*TVB(4W$tK4a*xhB|-W=WL?hR|7bn(3YFB zmi%Aj?vab2gb=?Zuj3B+;ky6LKU#5Pk`1iGYV@(T;E(!f@FMzMr2I`ONtwqycgZiM zevI-ZV}Hec6dj*3ewEdq+WZrUJzw%ib0zEq8JTDzr6{@Wl&3V!r_WW*^3hR~KBI|) zX?w2m#CxeNuahTPB}9Iw0+^2%CUJLX77y##Hws3)==Zi6G~+nfAD4;|`J z`I|%oiVtxLN+bhH&@mK099_v*_z+++`VYka=u?rt*{~CR67UplGpLV29c^iE>Q=pf z=H^d+xph2$(5WV63WM@fF4}-K)DKe{QPR`?;do4?200xfJ7puKCZ)3VTTDFzWiK6m zXTA~`hnvWqrWB>#gT9UQYff>3o)jHB=var9er<6QIvu2>CU>3ODEd^!jr3bb{uuQ{ z`0R)k)0;leiQ~O5y>&Q;4H=k+qGJV}bu?qZ-*}n04RsyY zDO-qpQ9hwuvqkvX*gfh?(Qy&ayH%%}wU;E`ME;z<=9adOJ~nw7;%SyUMaN8(pE<#P zaskx;!ASZJx08}`exP2Pl9qZ~+Lu!b6PK|$)u*=p;8^m}I8Ugps4xwF47yEYE(Z0Y z+@=12oQ|24D4Q%Jxrwx`w25ZZc7XclTB`2~}g#-4bDemdf?Dn{XF%=0DX zGZwd%_$NwTN+$i9qa!OpG#x%ZVu@oY1#O4EpwCrG*hiiIqW-&G*@yH=`lO?LIKHJl znqZ41eqqk-hr%34e9M9KQQnTB?FD)DeGywk2;#tX9N9fP#;L2wpfCArJXF$ z%x-GT9oly?eh(!Ry&mCM%1v?yh*wi|b-!A<_+XvIorsFvM zINUo`h~TU>Ts75Nn@PT4(u350#=q%z8v9Umd`rCF`aiIo@REg4o3I4;hOxl`$^N6cZUqzn_Y^xuAe5ms28WLBlV$_7>bTGv?r#Xky3+t z5PcHJXQ7l+2ae0M6|nwqX|F?l0)3mH=X)xh>6D3}6{}A|hnD1?6Ys&Cwtxq2C#DX)JzcX3!^vj%SvrYVNd2yWnAO4<1A& z(NW*}46{C+&3)ugP;L|7q~9CL4dSwttSqboB4BW}^qv>|1CcZurX%c<9=YgF&>PJO&lc%S~ABm0MUj*RIL)${+`)H5nN zCT{ig?S2K^u3h>J=-t`*tdEWKhSV*L9Mm_ee`M#lwzFUPWYx?AJ4A=~kL(`VDJHUW zc*nuvLx(#Jbjs+s#&ZJ_2e=J!%@!BQ8aHD1`NXMudRNy!vfqHn=$KFFKC|wj8lkEr zt=-T|)u@=k-b%F&F_GR%+@wPupR(aySVj4c6^9NlU!-Hj4;NT4yk(yO;T?KL_X+RR zL6_ZkaJY6PJf=^0x5)lQx<&Pxd8=*u^x@&v`t;5d6W*z3pXkW&m~K(g;e9)Fjf@L8 z^3LD4Yo9pZQ^$Rideh!*n0ey#Qvc#b!y_W%`u?&rq-^~1ukKD8duQiP=YO^MX_N1M zztM@`JLZ3>|0i(U&EoolRRNe);5tCEK05^LEAWndJuY+g8L+ zo^@}-%7oZ4|DT5VadYl1UA658AD=CL5%E)}-<`729pNNwUYf9Ae8Tv>@#8G@=k7@f z<7VF*IVOJF=6n0TPFS#b+vfp3nS8_S9Lv@uOq_9d*FxPWH+6UYQZ8*Hq)$ delta 23844 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\n" "Language-Team: Jumpserver team\n" @@ -144,7 +144,7 @@ msgstr "资产" #: settings/templates/settings/terminal_setting.html:105 terminal/models.py:23 #: 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:373 users/templates/users/_select_user_modal.html:13 +#: users/models/user.py:375 users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_detail.html:63 #: users/templates/users/user_group_detail.html:55 #: users/templates/users/user_group_list.html:35 @@ -197,7 +197,7 @@ msgstr "参数" #: orgs/models.py:16 perms/models/base.py:54 #: perms/templates/perms/asset_permission_detail.html:98 #: perms/templates/perms/remote_app_permission_detail.html:90 -#: users/models/user.py:414 users/serializers/v1.py:143 +#: users/models/user.py:416 users/serializers/group.py:32 #: users/templates/users/user_detail.html:111 #: xpack/plugins/change_auth_plan/models.py:108 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:113 @@ -219,7 +219,8 @@ msgstr "创建者" #: assets/templates/assets/system_user_detail.html:96 #: common/mixins/models.py:51 ops/models/adhoc.py:45 #: ops/templates/ops/adhoc_detail.html:90 ops/templates/ops/task_detail.html:64 -#: orgs/models.py:17 perms/models/base.py:55 +#: orders/templates/orders/login_confirm_order_detail.html:60 orgs/models.py:17 +#: perms/models/base.py:55 #: perms/templates/perms/asset_permission_detail.html:94 #: perms/templates/perms/remote_app_permission_detail.html:86 #: terminal/templates/terminal/terminal_detail.html:59 users/models/group.py:17 @@ -253,12 +254,14 @@ msgstr "创建日期" #: assets/templates/assets/domain_list.html:28 #: assets/templates/assets/system_user_detail.html:104 #: assets/templates/assets/system_user_list.html:55 ops/models/adhoc.py:43 -#: orgs/models.py:18 perms/models/base.py:56 +#: orders/serializers.py:23 +#: orders/templates/orders/login_confirm_order_detail.html:96 orgs/models.py:18 +#: perms/models/base.py:56 #: perms/templates/perms/asset_permission_detail.html:102 #: 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:406 users/templates/users/user_detail.html:129 +#: users/models/user.py:408 users/templates/users/user_detail.html:129 #: users/templates/users/user_group_detail.html:67 #: users/templates/users/user_group_list.html:37 #: users/templates/users/user_profile.html:138 @@ -516,6 +519,7 @@ msgstr "创建远程应用" #: authentication/templates/authentication/_access_key_modal.html:34 #: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:64 #: ops/templates/ops/task_history.html:65 ops/templates/ops/task_list.html:18 +#: orders/templates/orders/login_confirm_order_list.html:19 #: perms/forms/asset_permission.py:21 #: perms/templates/perms/asset_permission_create_update.html:50 #: perms/templates/perms/asset_permission_list.html:56 @@ -699,12 +703,12 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC" #: assets/templates/assets/system_user_list.html:48 audits/models.py:80 #: audits/templates/audits/login_log_list.html:57 authentication/forms.py:13 #: authentication/templates/authentication/login.html:65 -#: authentication/templates/authentication/new_login.html:92 +#: authentication/templates/authentication/xpack_login.html:92 #: 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:371 users/templates/users/_select_user_modal.html:14 +#: users/models/user.py:373 users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:67 #: users/templates/users/user_list.html:36 #: users/templates/users/user_profile.html:47 @@ -729,7 +733,7 @@ msgstr "密码或密钥密码" #: assets/templates/assets/_asset_user_auth_view_modal.html:27 #: authentication/forms.py:15 #: authentication/templates/authentication/login.html:68 -#: authentication/templates/authentication/new_login.html:95 +#: authentication/templates/authentication/xpack_login.html:95 #: 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 @@ -744,7 +748,7 @@ msgstr "密码" #: assets/forms/user.py:30 assets/serializers/asset_user.py:71 #: assets/templates/assets/_asset_user_auth_update_modal.html:27 -#: users/models/user.py:400 +#: users/models/user.py:402 msgid "Private key" msgstr "ssh私钥" @@ -793,6 +797,8 @@ msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig" #: assets/templates/assets/domain_gateway_list.html:68 #: assets/templates/assets/user_asset_list.html:76 #: audits/templates/audits/login_log_list.html:60 +#: orders/templates/orders/login_confirm_order_detail.html:33 +#: orders/templates/orders/login_confirm_order_list.html:15 #: perms/templates/perms/asset_permission_asset.html:58 settings/forms.py:144 #: users/templates/users/_granted_assets.html:31 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:54 @@ -842,6 +848,7 @@ msgstr "系统平台" #: assets/models/asset.py:146 assets/models/authbook.py:27 #: assets/models/cmd_filter.py:22 assets/models/domain.py:54 #: assets/models/label.py:22 assets/templates/assets/asset_detail.html:110 +#: authentication/models.py:45 msgid "Is active" msgstr "激活" @@ -957,7 +964,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:392 +#: assets/models/cluster.py:22 users/models/user.py:394 #: users/templates/users/user_detail.html:76 msgid "Phone" msgstr "手机" @@ -983,7 +990,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:512 +#: users/models/user.py:514 msgid "System" msgstr "系统" @@ -1011,7 +1018,7 @@ msgstr "BGP全网通" msgid "Regex" msgstr "正则表达式" -#: assets/models/cmd_filter.py:40 ops/models/command.py:21 +#: assets/models/cmd_filter.py:40 ops/models/command.py:22 #: ops/templates/ops/command_execution_list.html:64 terminal/models.py:163 #: terminal/templates/terminal/command_list.html:28 #: terminal/templates/terminal/command_list.html:68 @@ -1034,7 +1041,7 @@ msgstr "过滤器" #: assets/models/cmd_filter.py:51 #: assets/templates/assets/cmd_filter_rule_list.html:58 -#: audits/templates/audits/login_log_list.html:58 +#: audits/templates/audits/login_log_list.html:58 orders/models.py:41 #: perms/templates/perms/remote_app_permission_remote_app.html:54 #: settings/templates/settings/command_storage_create.html:31 #: settings/templates/settings/replay_storage_create.html:31 @@ -1097,8 +1104,11 @@ msgstr "默认资产组" #: audits/templates/audits/operate_log_list.html:72 #: audits/templates/audits/password_change_log_list.html:39 #: audits/templates/audits/password_change_log_list.html:56 -#: ops/templates/ops/command_execution_list.html:38 -#: ops/templates/ops/command_execution_list.html:63 +#: authentication/models.py:43 ops/templates/ops/command_execution_list.html:38 +#: ops/templates/ops/command_execution_list.html:63 orders/models.py:11 +#: orders/models.py:32 +#: orders/templates/orders/login_confirm_order_detail.html:32 +#: orders/templates/orders/login_confirm_order_list.html:14 #: perms/forms/asset_permission.py:78 perms/forms/remote_app_permission.py:34 #: perms/models/base.py:49 #: perms/templates/perms/asset_permission_create_update.html:41 @@ -1111,8 +1121,9 @@ 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:127 users/models/user.py:143 users/models/user.py:500 -#: users/serializers/v1.py:132 users/templates/users/user_group_detail.html:78 +#: users/models/user.py:129 users/models/user.py:145 users/models/user.py:502 +#: 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 #: xpack/plugins/orgs/forms.py:28 #: xpack/plugins/orgs/templates/orgs/org_detail.html:113 @@ -1235,7 +1246,7 @@ msgid "Reachable" msgstr "可连接" #: assets/models/utils.py:45 assets/tasks/const.py:86 -#: authentication/utils.py:13 xpack/plugins/license/models.py:78 +#: authentication/utils.py:16 xpack/plugins/license/models.py:78 msgid "Unknown" msgstr "未知" @@ -1266,7 +1277,7 @@ msgid "Backend" msgstr "后端" #: assets/serializers/asset_user.py:67 users/forms.py:262 -#: users/models/user.py:403 users/templates/users/first_login.html:42 +#: users/models/user.py:405 users/templates/users/first_login.html:42 #: users/templates/users/user_password_update.html:49 #: users/templates/users/user_profile.html:69 #: users/templates/users/user_profile_update.html:46 @@ -1470,6 +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 msgid "Copy success" msgstr "复制成功" @@ -1490,6 +1502,7 @@ msgstr "关闭" #: audits/templates/audits/operate_log_list.html:77 #: audits/templates/audits/password_change_log_list.html:59 #: ops/templates/ops/task_adhoc.html:63 +#: orders/templates/orders/login_confirm_order_list.html:18 #: terminal/templates/terminal/command_list.html:33 #: terminal/templates/terminal/session_detail.html:50 msgid "Datetime" @@ -1768,7 +1781,7 @@ msgstr "硬盘" msgid "Date joined" msgstr "创建日期" -#: assets/templates/assets/asset_detail.html:148 authentication/models.py:15 +#: assets/templates/assets/asset_detail.html:148 authentication/models.py:19 #: authentication/templates/authentication/_access_key_modal.html:32 #: perms/models/base.py:51 #: perms/templates/perms/asset_permission_create_update.html:55 @@ -1787,6 +1800,7 @@ msgid "Refresh hardware" msgstr "更新硬件信息" #: assets/templates/assets/asset_detail.html:168 +#: authentication/templates/authentication/login_wait_confirm.html:42 msgid "Refresh" msgstr "刷新" @@ -2264,7 +2278,7 @@ msgstr "Agent" #: audits/models.py:85 audits/templates/audits/login_log_list.html:62 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms.py:174 users/models/user.py:395 +#: users/forms.py:174 users/models/user.py:397 #: users/templates/users/first_login.html:45 msgid "MFA" msgstr "MFA" @@ -2278,6 +2292,8 @@ msgid "Reason" msgstr "原因" #: audits/models.py:87 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 #: xpack/plugins/cloud/models.py:275 xpack/plugins/cloud/models.py:310 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:70 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:65 @@ -2338,6 +2354,8 @@ msgid "UA" msgstr "Agent" #: audits/templates/audits/login_log_list.html:61 +#: orders/templates/orders/login_confirm_order_detail.html:58 +#: orders/templates/orders/login_confirm_order_list.html:16 msgid "City" msgstr "城市" @@ -2348,23 +2366,23 @@ msgid "Date" msgstr "日期" #: audits/views.py:86 audits/views.py:130 audits/views.py:167 -#: audits/views.py:212 audits/views.py:244 templates/_nav.html:129 +#: audits/views.py:212 audits/views.py:244 templates/_nav.html:139 msgid "Audits" msgstr "日志审计" -#: audits/views.py:87 templates/_nav.html:133 +#: audits/views.py:87 templates/_nav.html:143 msgid "FTP log" msgstr "FTP日志" -#: audits/views.py:131 templates/_nav.html:134 +#: audits/views.py:131 templates/_nav.html:144 msgid "Operate log" msgstr "操作日志" -#: audits/views.py:168 templates/_nav.html:135 +#: audits/views.py:168 templates/_nav.html:145 msgid "Password change log" msgstr "改密日志" -#: audits/views.py:213 templates/_nav.html:132 +#: audits/views.py:213 templates/_nav.html:142 msgid "Login log" msgstr "登录日志" @@ -2372,25 +2390,33 @@ msgstr "登录日志" msgid "Command execution log" msgstr "命令执行" -#: authentication/api/auth.py:61 authentication/api/token.py:45 +#: authentication/api/auth.py:58 authentication/api/token.py:45 #: authentication/templates/authentication/login.html:52 -#: authentication/templates/authentication/new_login.html:77 +#: authentication/templates/authentication/xpack_login.html:77 msgid "Log in frequently and try again later" msgstr "登录频繁, 稍后重试" -#: authentication/api/auth.py:86 +#: authentication/api/auth.py:83 msgid "Please carry seed value and conduct MFA secondary certification" msgstr "请携带seed值, 进行MFA二次认证" -#: authentication/api/auth.py:176 +#: authentication/api/auth.py:173 msgid "Please verify the user name and password first" msgstr "请先进行用户名和密码验证" -#: authentication/api/auth.py:181 +#: authentication/api/auth.py:178 msgid "MFA certification failed" msgstr "MFA认证失败" -#: authentication/api/token.py:80 +#: 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 "" @@ -2491,10 +2517,38 @@ msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)" msgid "MFA code" msgstr "MFA 验证码" -#: authentication/models.py:35 +#: authentication/models.py:39 msgid "Private Token" msgstr "ssh密钥" +#: authentication/models.py:43 +msgid "login_confirmation_setting" +msgstr "" + +#: authentication/models.py:44 +msgid "Reviewers" +msgstr "" + +#: authentication/models.py:44 +msgid "review_login_confirmation_settings" +msgstr "" + +#: authentication/models.py:53 +msgid "User login request: {}" +msgstr "用户登录请求: {}" + +#: authentication/models.py:57 +msgid "" +"User: {}\n" +"IP: {}\n" +"City: {}\n" +"Date: {}\n" +msgstr "" +"用户: {}\n" +"IP: {}\n" +"城市: {}\n" +"日期: {}\n" + #: authentication/templates/authentication/_access_key_modal.html:6 msgid "API key list" msgstr "API Key列表" @@ -2517,14 +2571,14 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:330 users/templates/users/user_profile.html:94 +#: users/models/user.py:332 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:331 users/templates/users/user_profile.html:92 +#: users/models/user.py:333 users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:170 msgid "Enable" msgstr "启用" @@ -2586,23 +2640,23 @@ msgstr "改变世界,从一点点开始。" #: authentication/templates/authentication/login.html:46 #: authentication/templates/authentication/login.html:73 -#: authentication/templates/authentication/new_login.html:101 +#: authentication/templates/authentication/xpack_login.html:101 #: templates/_header_bar.html:83 msgid "Login" msgstr "登录" #: authentication/templates/authentication/login.html:54 -#: authentication/templates/authentication/new_login.html:80 +#: authentication/templates/authentication/xpack_login.html:80 msgid "The user password has expired" msgstr "用户密码已过期" #: authentication/templates/authentication/login.html:57 -#: authentication/templates/authentication/new_login.html:83 +#: authentication/templates/authentication/xpack_login.html:83 msgid "Captcha invalid" msgstr "验证码错误" #: authentication/templates/authentication/login.html:84 -#: authentication/templates/authentication/new_login.html:105 +#: authentication/templates/authentication/xpack_login.html:105 #: users/templates/users/forgot_password.html:10 #: users/templates/users/forgot_password.html:25 msgid "Forgot password" @@ -2653,24 +2707,45 @@ msgstr "下一步" msgid "Can't provide security? Please contact the administrator!" msgstr "如果不能提供MFA验证码,请联系管理员!" -#: authentication/templates/authentication/new_login.html:67 +#: authentication/templates/authentication/login_wait_confirm.html:47 +msgid "Copy link" +msgstr "复制链接" + +#: authentication/templates/authentication/login_wait_confirm.html:52 +#: templates/flash_message_standalone.html:47 +msgid "Return" +msgstr "返回" + +#: authentication/templates/authentication/xpack_login.html:67 msgid "Welcome back, please enter username and password to login" msgstr "欢迎回来,请输入用户名和密码登录" -#: authentication/views/login.py:81 +#: authentication/views/login.py:82 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:174 users/views/user.py:393 +#: 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:205 +#: authentication/views/login.py:226 +msgid "" +"Wait for {} confirm, You also can copy link to her/him
\n" +" Don't close this page" +msgstr "" +"等待 {} 确认, 你也可以复制链接发给他/她
\n" +" 不要关闭本页面" + +#: authentication/views/login.py:231 +msgid "No order found" +msgstr "没有发现工单" + +#: authentication/views/login.py:254 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:206 +#: authentication/views/login.py:255 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" @@ -2757,6 +2832,21 @@ msgstr "" msgid "Websocket server run on port: {}, you should proxy it on nginx" msgstr "" +#: jumpserver/views.py:241 +#, fuzzy +#| msgid "" +#| "
Luna is a separately deployed program, you need to deploy Luna, " +#| "koko, configure nginx for url distribution,
If you see this " +#| "page, prove that you are not accessing the nginx listening port. Good " +#| "luck." +msgid "" +"
Koko is a separately deployed program, you need to deploy Koko, " +"configure nginx for url distribution,
If you see this page, " +"prove that you are not accessing the nginx listening port. Good luck." +msgstr "" +"
Luna是单独部署的一个程序,你需要部署luna,koko,
如果你看到了" +"这个页面,证明你访问的不是nginx监听的端口,祝你好运
" + #: ops/api/celery.py:54 msgid "Waiting task start" msgstr "等待任务开始" @@ -2872,21 +2962,21 @@ msgstr "结果" msgid "Adhoc result summary" msgstr "汇总" -#: ops/models/command.py:22 +#: ops/models/command.py:23 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:56 #: xpack/plugins/cloud/models.py:273 msgid "Result" msgstr "结果" -#: ops/models/command.py:57 +#: ops/models/command.py:58 msgid "Task start" msgstr "任务开始" -#: ops/models/command.py:71 +#: ops/models/command.py:75 msgid "Command `{}` is forbidden ........" msgstr "命令 `{}` 不允许被执行 ......." -#: ops/models/command.py:77 +#: ops/models/command.py:81 msgid "Task end" msgstr "任务结束" @@ -3028,7 +3118,7 @@ msgstr "没有输入命令" msgid "No system user was selected" msgstr "没有选择系统用户" -#: ops/templates/ops/command_execution_create.html:296 +#: ops/templates/ops/command_execution_create.html:296 orders/models.py:26 msgid "Pending" msgstr "等待" @@ -3112,7 +3202,93 @@ msgstr "命令执行列表" msgid "Command execution" msgstr "命令执行" -#: orgs/mixins/models.py:58 orgs/mixins/serializers.py:26 orgs/models.py:31 +#: orders/models.py:12 orders/models.py:33 +#, fuzzy +#| msgid "User is inactive" +msgid "User display name" +msgstr "用户已禁用" + +#: orders/models.py:13 orders/models.py:36 +msgid "Body" +msgstr "" + +#: orders/models.py:24 +#, fuzzy +#| msgid "Accept" +msgid "Accepted" +msgstr "接受" + +#: orders/models.py:25 +msgid "Rejected" +msgstr "拒绝" + +#: orders/models.py:35 orders/templates/orders/login_confirm_order_list.html:13 +msgid "Title" +msgstr "标题" + +#: orders/models.py:37 +#: orders/templates/orders/login_confirm_order_detail.html:59 +msgid "Assignee" +msgstr "处理人" + +#: orders/models.py:38 +msgid "Assignee display name" +msgstr "处理人名称" + +#: orders/models.py:39 +#: orders/templates/orders/login_confirm_order_detail.html:34 +msgid "Assignees" +msgstr "待处理人" + +#: orders/models.py:40 +msgid "Assignees display name" +msgstr "待处理人名称" + +#: orders/serializers.py:21 +#: orders/templates/orders/login_confirm_order_detail.html:94 +#: orders/templates/orders/login_confirm_order_list.html:53 +#: terminal/templates/terminal/terminal_list.html:78 +msgid "Accept" +msgstr "接受" + +#: orders/serializers.py:22 +#: orders/templates/orders/login_confirm_order_detail.html:95 +#: orders/templates/orders/login_confirm_order_list.html:54 +#: terminal/templates/terminal/terminal_list.html:80 +msgid "Reject" +msgstr "拒绝" + +#: orders/serializers.py:43 +msgid "this order" +msgstr "这个工单" + +#: orders/templates/orders/login_confirm_order_detail.html:75 +msgid "ago" +msgstr "前" + +#: orders/templates/orders/login_confirm_order_list.html:83 +#: users/templates/users/user_list.html:327 +msgid "User is expired" +msgstr "用户已失效" + +#: orders/templates/orders/login_confirm_order_list.html:86 +#: users/templates/users/user_list.html:330 +msgid "User is inactive" +msgstr "用户已禁用" + +#: orders/views.py:15 orders/views.py:31 templates/_nav.html:127 +msgid "Orders" +msgstr "工单管理" + +#: orders/views.py:16 +msgid "Login confirm order list" +msgstr "登录复核工单列表" + +#: orders/views.py:32 +msgid "Login confirm order detail" +msgstr "登录复核工单详情" + +#: orgs/mixins/models.py:44 orgs/mixins/serializers.py:26 orgs/models.py:31 msgid "Organization" msgstr "组织" @@ -3136,7 +3312,7 @@ msgstr "提示:RDP 协议不支持单独控制上传或下载文件" #: perms/templates/perms/asset_permission_list.html:118 #: perms/templates/perms/remote_app_permission_list.html:16 #: templates/_nav.html:21 users/forms.py:293 users/models/group.py:26 -#: users/models/user.py:379 users/templates/users/_select_user_modal.html:16 +#: users/models/user.py:381 users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_detail.html:218 #: users/templates/users/user_list.html:38 #: xpack/plugins/orgs/templates/orgs/org_list.html:16 @@ -3178,7 +3354,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:411 users/templates/users/user_detail.html:107 +#: users/models/user.py:413 users/templates/users/user_detail.html:107 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" @@ -3742,7 +3918,7 @@ msgid "Please submit the LDAP configuration before import" msgstr "请先提交LDAP配置再进行导入" #: settings/templates/settings/_ldap_list_users_modal.html:32 -#: users/models/user.py:375 users/templates/users/user_detail.html:71 +#: users/models/user.py:377 users/templates/users/user_detail.html:71 #: users/templates/users/user_profile.html:59 msgid "Email" msgstr "邮件" @@ -3946,7 +4122,7 @@ msgstr "用户来源不是LDAP" #: settings/views.py:19 settings/views.py:46 settings/views.py:73 #: settings/views.py:103 settings/views.py:131 settings/views.py:144 -#: settings/views.py:158 settings/views.py:185 templates/_nav.html:170 +#: settings/views.py:158 settings/views.py:185 templates/_nav.html:180 msgid "Settings" msgstr "系统设置" @@ -4139,7 +4315,7 @@ msgstr "终端管理" msgid "Job Center" msgstr "作业中心" -#: templates/_nav.html:116 templates/_nav.html:136 +#: templates/_nav.html:116 templates/_nav.html:146 msgid "Batch command" msgstr "批量命令" @@ -4147,15 +4323,19 @@ msgstr "批量命令" msgid "Task monitor" msgstr "任务监控" -#: templates/_nav.html:146 +#: templates/_nav.html:130 +msgid "Login confirm" +msgstr "登录复核" + +#: templates/_nav.html:156 msgid "XPack" msgstr "" -#: templates/_nav.html:154 xpack/plugins/cloud/views.py:28 +#: templates/_nav.html:164 xpack/plugins/cloud/views.py:28 msgid "Account list" msgstr "账户列表" -#: templates/_nav.html:155 +#: templates/_nav.html:165 msgid "Sync instance" msgstr "同步实例" @@ -4184,10 +4364,6 @@ msgstr "语言播放验证码" msgid "Captcha" msgstr "验证码" -#: templates/flash_message_standalone.html:47 -msgid "Return" -msgstr "返回" - #: templates/index.html:11 msgid "Total users" msgstr "用户总数" @@ -4496,14 +4672,6 @@ msgstr "地址" msgid "Alive" msgstr "在线" -#: terminal/templates/terminal/terminal_list.html:78 -msgid "Accept" -msgstr "接受" - -#: terminal/templates/terminal/terminal_list.html:80 -msgid "Reject" -msgstr "拒绝" - #: terminal/templates/terminal/terminal_modal_accept.html:5 msgid "Accept terminal registration" msgstr "接受终端注册" @@ -4541,7 +4709,7 @@ msgstr "你可以使用ssh客户端工具连接终端" msgid "Could not reset self otp, use profile reset instead" msgstr "不能再该页面重置MFA, 请去个人信息页面重置" -#: users/forms.py:32 users/models/user.py:383 +#: users/forms.py:32 users/models/user.py:385 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:87 #: users/templates/users/user_list.html:37 @@ -4570,7 +4738,7 @@ msgstr "添加到用户组" msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" -#: users/forms.py:90 users/forms.py:251 users/serializers/v1.py:116 +#: users/forms.py:90 users/forms.py:251 users/serializers/user.py:110 msgid "Not a valid ssh public key" msgstr "ssh密钥不合法" @@ -4655,98 +4823,98 @@ msgstr "复制你的公钥到这里" msgid "Select users" msgstr "选择用户" -#: users/models/user.py:50 users/templates/users/user_update.html:22 +#: users/models/user.py:52 users/templates/users/user_update.html:22 #: users/views/login.py:46 users/views/login.py:107 msgid "User auth from {}, go there change password" msgstr "用户认证源来自 {}, 请去相应系统修改密码" -#: users/models/user.py:126 users/models/user.py:508 +#: users/models/user.py:128 users/models/user.py:510 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:128 +#: users/models/user.py:130 msgid "Application" msgstr "应用程序" -#: users/models/user.py:129 xpack/plugins/orgs/forms.py:30 +#: users/models/user.py:131 xpack/plugins/orgs/forms.py:30 #: xpack/plugins/orgs/templates/orgs/org_list.html:14 msgid "Auditor" msgstr "审计员" -#: users/models/user.py:139 +#: users/models/user.py:141 msgid "Org admin" msgstr "组织管理员" -#: users/models/user.py:141 +#: users/models/user.py:143 msgid "Org auditor" msgstr "组织审计员" -#: users/models/user.py:332 users/templates/users/user_profile.html:90 +#: users/models/user.py:334 users/templates/users/user_profile.html:90 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:386 +#: users/models/user.py:388 msgid "Avatar" msgstr "头像" -#: users/models/user.py:389 users/templates/users/user_detail.html:82 +#: users/models/user.py:391 users/templates/users/user_detail.html:82 msgid "Wechat" msgstr "微信" -#: users/models/user.py:418 users/templates/users/user_detail.html:103 +#: users/models/user.py:420 users/templates/users/user_detail.html:103 #: users/templates/users/user_list.html:39 #: users/templates/users/user_profile.html:102 msgid "Source" msgstr "用户来源" -#: users/models/user.py:422 +#: users/models/user.py:424 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:511 +#: users/models/user.py:513 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/serializers/v1.py:45 +#: users/serializers/group.py:46 +msgid "Auditors cannot be join in the user group" +msgstr "审计员不能被加入到用户组" + +#: users/serializers/user.py:39 msgid "Groups name" msgstr "用户组名" -#: users/serializers/v1.py:46 +#: users/serializers/user.py:40 msgid "Source name" msgstr "用户来源名" -#: users/serializers/v1.py:47 +#: users/serializers/user.py:41 msgid "Is first login" msgstr "首次登录" -#: users/serializers/v1.py:48 +#: users/serializers/user.py:42 msgid "Role name" msgstr "角色名" -#: users/serializers/v1.py:49 +#: users/serializers/user.py:43 msgid "Is valid" msgstr "账户是否有效" -#: users/serializers/v1.py:50 +#: users/serializers/user.py:44 msgid "Is expired" msgstr " 是否过期" -#: users/serializers/v1.py:51 +#: users/serializers/user.py:45 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/v1.py:72 +#: users/serializers/user.py:66 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/v1.py:84 +#: users/serializers/user.py:78 msgid "Password does not match security rules" msgstr "密码不满足安全规则" -#: users/serializers/v1.py:157 -msgid "Auditors cannot be join in the user group" -msgstr "审计员不能被加入到用户组" - #: users/serializers_v2/user.py:36 msgid "name not unique" msgstr "名称重复" @@ -5092,14 +5260,6 @@ msgstr "删除" msgid "User Deleting failed." msgstr "用户删除失败" -#: users/templates/users/user_list.html:327 -msgid "User is expired" -msgstr "用户已失效" - -#: users/templates/users/user_list.html:330 -msgid "User is inactive" -msgstr "用户已禁用" - #: users/templates/users/user_otp_authentication.html:6 #: users/templates/users/user_password_authentication.html:6 msgid "Authenticate" @@ -6391,9 +6551,6 @@ msgstr "创建" #~ msgid "Start" #~ msgstr "开始" -#~ msgid "User login settings" -#~ msgstr "用户登录设置" - #~ msgid "Bit" #~ msgstr " 位" diff --git a/apps/orders/api.py b/apps/orders/api.py new file mode 100644 index 000000000..a588dd684 --- /dev/null +++ b/apps/orders/api.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import viewsets, generics +from django.shortcuts import get_object_or_404 + +from common.permissions import IsValidUser +from common.mixins import CommonApiMixin +from . import serializers +from .models import LoginConfirmOrder + + +class LoginConfirmOrderViewSet(CommonApiMixin, viewsets.ModelViewSet): + serializer_class = serializers.LoginConfirmOrderSerializer + permission_classes = (IsValidUser,) + search_fields = ['user_display', 'title', 'ip', 'city'] + + def get_queryset(self): + queryset = LoginConfirmOrder.objects.all()\ + .filter(assignees=self.request.user) + return queryset + + +class LoginConfirmOrderCreateActionApi(generics.CreateAPIView): + permission_classes = (IsValidUser,) + serializer_class = serializers.LoginConfirmOrderActionSerializer + + def get_order(self): + order_id = self.kwargs.get('pk') + queryset = LoginConfirmOrder.objects.all()\ + .filter(assignees=self.request.user) + order = get_object_or_404(queryset, id=order_id) + return order + + def get_serializer_context(self): + context = super().get_serializer_context() + order = self.get_order() + context['order'] = order + return context diff --git a/apps/orders/apps.py b/apps/orders/apps.py index 384ab4368..3e58af6ea 100644 --- a/apps/orders/apps.py +++ b/apps/orders/apps.py @@ -3,3 +3,7 @@ from django.apps import AppConfig class OrdersConfig(AppConfig): name = 'orders' + + def ready(self): + from . import signals_handler + return super().ready() diff --git a/apps/orders/models.py b/apps/orders/models.py index b2614bd17..c574cacb9 100644 --- a/apps/orders/models.py +++ b/apps/orders/models.py @@ -3,31 +3,56 @@ from django.utils.translation import ugettext_lazy as _ from common.mixins.models import CommonModelMixin +__all__ = ['LoginConfirmOrder', 'Comment'] -class Order(CommonModelMixin): + +class Comment(CommonModelMixin): + order_id = models.UUIDField() + user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, verbose_name=_("User"), related_name='comments') + user_display = models.CharField(max_length=128, verbose_name=_("User display name")) + body = models.TextField(verbose_name=_("Body")) + + class Meta: + ordering = ('date_created', ) + + +class BaseOrder(CommonModelMixin): + STATUS_ACCEPTED = 'accepted' + STATUS_REJECTED = 'rejected' + STATUS_PENDING = 'pending' STATUS_CHOICES = ( - ('accepted', _("Accepted")), - ('rejected', _("Rejected")), - ('pending', _("Pending")) + (STATUS_ACCEPTED, _("Accepted")), + (STATUS_REJECTED, _("Rejected")), + (STATUS_PENDING, _("Pending")) ) - TYPE_LOGIN_REQUEST = 'login_request' + TYPE_LOGIN_CONFIRM = 'login_confirm' TYPE_CHOICES = ( - (TYPE_LOGIN_REQUEST, _("Login request")), + (TYPE_LOGIN_CONFIRM, 'Login confirm'), ) - user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='orders', verbose_name=_("User")) + user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_requested', verbose_name=_("User")) user_display = models.CharField(max_length=128, verbose_name=_("User display name")) title = models.CharField(max_length=256, verbose_name=_("Title")) body = models.TextField(verbose_name=_("Body")) - assignees = models.ManyToManyField('users.User', related_name='assign_orders', verbose_name=_("Assignees")) + assignee = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_handled', verbose_name=_("Assignee")) + assignee_display = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Assignee display name")) + assignees = models.ManyToManyField('users.User', related_name='%(class)s_assigned', verbose_name=_("Assignees")) assignees_display = models.CharField(max_length=128, verbose_name=_("Assignees display name"), blank=True) - - type = models.CharField(choices=TYPE_CHOICES, max_length=64) + type = models.CharField(choices=TYPE_CHOICES, max_length=16, verbose_name=_('Type')) status = models.CharField(choices=STATUS_CHOICES, max_length=16, default='pending') def __str__(self): return '{}: {}'.format(self.user_display, self.title) - class Meta: - ordering = ('date_created',) + @property + def comments(self): + return Comment.objects.filter(order_id=self.id) + class Meta: + abstract = True + ordering = ('-date_created',) + + +class LoginConfirmOrder(BaseOrder): + ip = models.GenericIPAddressField(blank=True, null=True) + city = models.CharField(max_length=16, blank=True, default='') diff --git a/apps/orders/serializers.py b/apps/orders/serializers.py new file mode 100644 index 000000000..d74e33208 --- /dev/null +++ b/apps/orders/serializers.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + +from .models import LoginConfirmOrder, Comment + + +class LoginConfirmOrderSerializer(serializers.ModelSerializer): + class Meta: + model = LoginConfirmOrder + fields = [ + 'id', 'user', 'user_display', 'title', 'body', + 'ip', 'city', 'assignees', 'assignees_display', + 'type', 'status', 'date_created', 'date_updated', + ] + + +class LoginConfirmOrderActionSerializer(serializers.Serializer): + ACTION_CHOICES = ( + ('accept', _('Accept')), + ('reject', _('Reject')), + ('comment', _('Comment')) + ) + action = serializers.ChoiceField(choices=ACTION_CHOICES) + comment = serializers.CharField(allow_blank=True) + + def update(self, instance, validated_data): + pass + + def create_comments(self, order, user, validated_data): + comment_data = validated_data.get('comment') + action = validated_data.get('action') + comments_data = [] + if comment_data: + comments_data.append(comment_data) + Comment.objects.create( + order_id=order.id, body=comment_data, user=user, + user_display=str(user) + ) + if action != "comment": + action_display = dict(self.ACTION_CHOICES).get(action) + comment_data = '{} {} {}'.format(user, action_display, _("this order")) + comments_data.append(comment_data) + comments = [ + Comment(order_id=order.id, body=data, user=user, user_display=str(user)) + for data in comments_data + ] + Comment.objects.bulk_create(comments) + + @staticmethod + def perform_action(order, user, validated_data): + action = validated_data.get('action') + if action == "accept": + status = "accepted" + elif action == "reject": + status = "rejected" + else: + status = None + + if status: + order.status = status + order.assignee = user + order.assignee_display = str(user) + order.save() + + def create(self, validated_data): + order = self.context['order'] + user = self.context['request'].user + self.create_comments(order, user, validated_data) + self.perform_action(order, user, validated_data) + return validated_data diff --git a/apps/orders/signals_handler.py b/apps/orders/signals_handler.py new file mode 100644 index 000000000..9e2cdd2e7 --- /dev/null +++ b/apps/orders/signals_handler.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# +from django.utils.translation import ugettext as _ +from django.dispatch import receiver +from django.db.models.signals import m2m_changed +from django.conf import settings + +from common.tasks import send_mail_async +from common.utils import get_logger, reverse +from .models import LoginConfirmOrder + +logger = get_logger(__name__) + + +def send_mail(order, assignees): + recipient_list = [user.email for user in assignees] + user = order.user + if not recipient_list: + logger.error("Order not has assignees: {}".format(order.id)) + return + subject = '{}: {}'.format(_("New order"), order.title) + detail_url = reverse('orders:login-confirm-order-detail', + kwargs={'pk': order.id}, external=True) + message = _(""" +
+

Your has a new order

+
+ Title: {order.title} +
+ User: {user} +
+ City: {order.city} +
+ IP: {order.ip} +
+ click here to review +
+
+ """).format(order=order, user=user, url=detail_url) + if settings.DEBUG: + try: + print(message) + except OSError: + pass + + send_mail_async.delay(subject, message, recipient_list, html_message=message) + + +@receiver(m2m_changed, sender=LoginConfirmOrder.assignees.through) +def on_login_confirm_order_assignee_set(sender, instance=None, action=None, + model=None, pk_set=None, **kwargs): + print(">>>>>>>>>>>>>>>>>>>>>>>.") + print(action) + if action == 'post_add': + print("<<<<<<<<<<<<<<<<<<<<") + logger.debug('New order create, send mail: {}'.format(instance.id)) + assignees = model.objects.filter(pk__in=pk_set) + send_mail(instance, assignees) + diff --git a/apps/orders/templates/orders/login_confirm_order_detail.html b/apps/orders/templates/orders/login_confirm_order_detail.html new file mode 100644 index 000000000..55a86e009 --- /dev/null +++ b/apps/orders/templates/orders/login_confirm_order_detail.html @@ -0,0 +1,137 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block content %} +
+
+
+
+
+
+ {{ object.title }} +
+ +
+
+
+
+
+
+
+
{% trans 'User' %}:
{{ object.user_display }}
+
{% trans 'IP' %}:
{{ object.ip }}
+
{% trans 'Assignees' %}:
{{ object.assignees_display }}
+
{% trans 'Status' %}:
+
+ {% if object.status == "accpeted" %} + + {{ object.get_status_display }} + + {% endif %} + {% if object.status == "rejected" %} + + {{ object.get_status_display }} + + {% endif %} + {% if object.status == "pending" %} + + {{ object.get_status_display }} + + {% endif %} +
+
+
+
+
+

+
{% trans 'City' %}:
{{ object.city }}
+
{% trans 'Assignee' %}:
{{ object.assignee_display | default_if_none:"" }}
+
{% trans 'Date created' %}:
{{ object.date_created }}
+
+
+
+
+
+
+
+
+ {% for comment in object.comments %} +
+ + image + +
+ {{ comment.user_display }} {{ comment.date_created|timesince}} {% trans 'ago' %} +
+ {{ comment.date_created }} +
+ {{ comment.body }} +
+
+
+ {% endfor %} +
+
+ + image + +
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/orders/templates/orders/login_confirm_order_list.html b/apps/orders/templates/orders/login_confirm_order_list.html new file mode 100644 index 000000000..e21fb8c9f --- /dev/null +++ b/apps/orders/templates/orders/login_confirm_order_list.html @@ -0,0 +1,91 @@ +{% extends '_base_list.html' %} +{% load i18n static %} +{% block table_search %} + +{% endblock %} +{% block table_container %} + + + + + + + + + + + + + + + +
+ + {% trans 'Title' %}{% trans 'User' %}{% trans 'IP' %}{% trans 'City' %}{% trans 'Status' %}{% trans 'Datetime' %}{% trans 'Action' %}
+{% endblock %} +{% block content_bottom_left %}{% endblock %} +{% block custom_foot_js %} + +{% endblock %} + diff --git a/apps/orders/urls/__init__.py b/apps/orders/urls/__init__.py new file mode 100644 index 000000000..ec51c5a2b --- /dev/null +++ b/apps/orders/urls/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# diff --git a/apps/orders/urls/api_urls.py b/apps/orders/urls/api_urls.py new file mode 100644 index 000000000..81828d3fe --- /dev/null +++ b/apps/orders/urls/api_urls.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# +from django.urls import path +from rest_framework.routers import DefaultRouter + +from .. import api + +app_name = 'orders' +router = DefaultRouter() + +router.register('login-confirm-orders', api.LoginConfirmOrderViewSet, 'login-confirm-order') + +urlpatterns = [ + path('login-confirm-order//actions/', + api.LoginConfirmOrderCreateActionApi.as_view(), + name='login-confirm-order-create-action' + ), +] + +urlpatterns += router.urls diff --git a/apps/orders/urls/views_urls.py b/apps/orders/urls/views_urls.py new file mode 100644 index 000000000..f4fe0ba05 --- /dev/null +++ b/apps/orders/urls/views_urls.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# +from django.urls import path +from .. import views + +app_name = 'orders' + +urlpatterns = [ + path('login-confirm-orders/', views.LoginConfirmOrderListView.as_view(), name='login-confirm-order-list'), + path('login-confirm-orders//', views.LoginConfirmOrderDetailView.as_view(), name='login-confirm-order-detail') +] diff --git a/apps/orders/utils.py b/apps/orders/utils.py new file mode 100644 index 000000000..ec51c5a2b --- /dev/null +++ b/apps/orders/utils.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# diff --git a/apps/orders/views.py b/apps/orders/views.py index 91ea44a21..93fb5abde 100644 --- a/apps/orders/views.py +++ b/apps/orders/views.py @@ -1,3 +1,34 @@ -from django.shortcuts import render +from django.views.generic import TemplateView, DetailView +from django.utils.translation import ugettext as _ -# Create your views here. +from common.permissions import PermissionsMixin, IsOrgAdmin +from .models import LoginConfirmOrder + + +class LoginConfirmOrderListView(PermissionsMixin, TemplateView): + template_name = 'orders/login_confirm_order_list.html' + permission_classes = (IsOrgAdmin,) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + 'app': _("Orders"), + 'action': _("Login confirm order list") + }) + return context + + +class LoginConfirmOrderDetailView(PermissionsMixin, DetailView): + template_name = 'orders/login_confirm_order_detail.html' + permission_classes = (IsOrgAdmin,) + + def get_queryset(self): + return LoginConfirmOrder.objects.filter(assignees=self.request.user) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + 'app': _("Orders"), + 'action': _("Login confirm order detail") + }) + return context diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index 33be3ba61..edea12c5b 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -307,7 +307,7 @@ function requestApi(props) { toastr.error(msg); } if (typeof props.error === 'function') { - return props.error(jqXHR.responseText, jqXHR.status); + return props.error(jqXHR.responseText, jqXHR.responseJSON, jqXHR.status); } }); // return true; diff --git a/apps/templates/_nav.html b/apps/templates/_nav.html index 5690c961d..36a2cb9ed 100644 --- a/apps/templates/_nav.html +++ b/apps/templates/_nav.html @@ -121,6 +121,16 @@ {% endif %} +{% if request.user.can_admin_current_org %} +
  • + + {% trans 'Orders' %} + + +
  • +{% endif %} {# Audits #} {% if request.user.can_admin_or_audit_current_org %} @@ -175,4 +185,4 @@ \ No newline at end of file + diff --git a/apps/users/utils.py b/apps/users/utils.py index 30868358c..9ab2c914e 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -193,7 +193,6 @@ def send_reset_ssh_key_mail(user): send_mail_async.delay(subject, message, recipient_list, html_message=message) - def get_user_or_tmp_user(request): user = request.user tmp_user = get_tmp_user_from_cache(request) @@ -212,8 +211,8 @@ def get_tmp_user_from_cache(request): return user -def set_tmp_user_to_cache(request, user): - cache.set(request.session.session_key+'user', user, 600) +def set_tmp_user_to_cache(request, user, ttl=3600): + cache.set(request.session.session_key+'user', user, ttl) def redirect_user_first_login_or_index(request, redirect_field_name):