mirror of https://github.com/jumpserver/jumpserver
[Update] 基本完成登录逻辑
parent
9d201bbf98
commit
6ce9815d51
|
@ -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']
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
<td class="text-center">{{ login_log.ip }}</td>
|
||||
<td class="text-center">{{ login_log.city }}</td>
|
||||
<td class="text-center">{{ login_log.get_mfa_display }}</td>
|
||||
<td class="text-center">{% trans login_log.reason %}</td>
|
||||
<td class="text-center">{{ login_log.reason_display }}</td>
|
||||
<td class="text-center">{{ login_log.get_status_display }}</td>
|
||||
<td class="text-center">{{ login_log.datetime }}</td>
|
||||
</tr>
|
||||
|
|
|
@ -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
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -37,7 +37,6 @@
|
|||
<p>
|
||||
{% trans "Changes the world, starting with a little bit." %}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="ibox-content">
|
||||
|
@ -47,25 +46,29 @@
|
|||
</div>
|
||||
<form class="m-t" role="form" method="post" action="">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if block_login %}
|
||||
<p class="red-fonts">{% trans 'Log in frequently and try again later' %}</p>
|
||||
{% elif password_expired %}
|
||||
<p class="red-fonts">{% trans 'The user password has expired' %}</p>
|
||||
{% elif form.errors %}
|
||||
{% if 'captcha' in form.errors %}
|
||||
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
|
||||
{% else %}
|
||||
{% if form.non_field_errors %}
|
||||
<div style="line-height: 17px;">
|
||||
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
|
||||
{% endif %}
|
||||
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
|
||||
</div>
|
||||
{% elif form.errors.captcha %}
|
||||
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="{{ form.username.html_name }}" placeholder="{% trans 'Username' %}" required="" value="{% if form.username.value %}{{ form.username.value }}{% endif %}">
|
||||
{% if form.errors.username %}
|
||||
<div class="help-block field-error">
|
||||
<p class="red-fonts">{{ form.errors.username.as_text }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
|
||||
{% if form.errors.password %}
|
||||
<div class="help-block field-error">
|
||||
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.captcha }}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -48,6 +48,13 @@
|
|||
float: right;
|
||||
}
|
||||
|
||||
.red-fonts {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.field-error {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
@ -69,30 +76,32 @@
|
|||
<div style="margin-bottom: 10px">
|
||||
<div>
|
||||
<div class="col-md-1"></div>
|
||||
<div class="contact-form col-md-10" style="margin-top: 10px;height: 35px">
|
||||
<div class="contact-form col-md-10" style="margin-top: 20px;height: 35px">
|
||||
<form id="contact-form" action="" method="post" role="form" novalidate="novalidate">
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}
|
||||
<div style="height: 70px;color: red;line-height: 17px;">
|
||||
{% if block_login %}
|
||||
<p class="red-fonts">{% trans 'Log in frequently and try again later' %}</p>
|
||||
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
|
||||
{% elif password_expired %}
|
||||
<p class="red-fonts">{% trans 'The user password has expired' %}</p>
|
||||
{% elif form.errors %}
|
||||
{% if 'captcha' in form.errors %}
|
||||
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
|
||||
{% else %}
|
||||
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
|
||||
{% endif %}
|
||||
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
|
||||
{% endif %}
|
||||
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
|
||||
</div>
|
||||
{% elif form.errors.captcha %}
|
||||
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="{{ form.username.html_name }}" placeholder="{% trans 'Username' %}" required="" value="{% if form.username.value %}{{ form.username.value }}{% endif %}" style="height: 35px">
|
||||
{% if form.errors.username %}
|
||||
<div class="help-block field-error">
|
||||
<p class="red-fonts">{{ form.errors.username.as_text }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
|
||||
{% if form.errors.password %}
|
||||
<div class="help-block field-error">
|
||||
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group" style="height: 50px;margin-bottom: 0;font-size: 13px">
|
||||
{{ form.captcha }}
|
||||
|
@ -116,4 +125,4 @@
|
|||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
@ -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/<uuid:user_id>/', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .login import *
|
||||
from .mfa import *
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
Binary file not shown.
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: Jumpserver 0.3.3\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-31 16:57+0800\n"
|
||||
"POT-Creation-Date: 2019-11-05 15:00+0800\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: ibuler <ibuler@qq.com>\n"
|
||||
"Language-Team: Jumpserver team<ibuler@qq.com>\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 <b>{}</b> confirm, You also can copy link to her/him <br/>\n"
|
||||
" Don't close this page"
|
||||
|
@ -2738,15 +2732,15 @@ msgstr ""
|
|||
"等待 <b>{}</b> 确认, 你也可以复制链接发给他/她 <br/>\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 ""
|
|||
" </div>\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 ""
|
|||
"具</a>) <br>注意: 如果同时设置了定期执行和周期执行,优先使用定期执行"
|
||||
|
||||
#: 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 "收件人"
|
||||
|
||||
|
|
|
@ -100,16 +100,6 @@
|
|||
<link rel="stylesheet" type="text/css" href={% static "css/plugins/daterangepicker/daterangepicker.css" %} />
|
||||
|
||||
<script>
|
||||
var dateOptions = {
|
||||
singleDatePicker: true,
|
||||
showDropdowns: true,
|
||||
timePicker: true,
|
||||
timePicker24Hour: true,
|
||||
autoApply: true,
|
||||
locale: {
|
||||
format: 'YYYY-MM-DD HH:mm'
|
||||
}
|
||||
};
|
||||
var api_action = "{{ api_action }}";
|
||||
|
||||
$(document).ready(function () {
|
||||
|
@ -119,8 +109,8 @@ $(document).ready(function () {
|
|||
nodesSelect2Init(".nodes-select2");
|
||||
usersSelect2Init(".users-select2");
|
||||
|
||||
$('#date_start').daterangepicker(dateOptions);
|
||||
$('#date_expired').daterangepicker(dateOptions);
|
||||
initDateRangePicker('#date_start');
|
||||
initDateRangePicker('#date_expired');
|
||||
|
||||
$("#id_assets").parent().find(".select2-selection").on('click', function (e) {
|
||||
if ($(e.target).attr('class') !== 'select2-selection__choice__remove'){
|
||||
|
|
|
@ -115,8 +115,8 @@ $(document).ready(function () {
|
|||
closeOnSelect: false
|
||||
});
|
||||
usersSelect2Init('.users-select2');
|
||||
$('#date_start').daterangepicker(dateOptions);
|
||||
$('#date_expired').daterangepicker(dateOptions);
|
||||
initDateRangePicker('#date_start');
|
||||
initDateRangePicker('#date_expired');
|
||||
})
|
||||
.on("submit", "form", function (evt) {
|
||||
evt.preventDefault();
|
||||
|
|
|
@ -1289,3 +1289,31 @@ function showCeleryTaskLog(taskId) {
|
|||
var url = '/ops/celery/task/taskId/log/'.replace('taskId', taskId);
|
||||
window.open(url, '', 'width=900,height=600')
|
||||
}
|
||||
|
||||
function initDateRangePicker(selector, options) {
|
||||
if (!options) {
|
||||
options = {}
|
||||
}
|
||||
var zhLocale = {
|
||||
format: 'YYYY-MM-DD HH:mm',
|
||||
separator: ' ~ ',
|
||||
applyLabel: "应用",
|
||||
cancelLabel: "取消",
|
||||
resetLabel: "重置",
|
||||
daysOfWeek: ["日", "一", "二", "三", "四", "五", "六"],//汉化处理
|
||||
monthNames: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
|
||||
};
|
||||
var defaultOption = {
|
||||
singleDatePicker: true,
|
||||
showDropdowns: true,
|
||||
timePicker: true,
|
||||
timePicker24Hour: true,
|
||||
autoApply: true,
|
||||
};
|
||||
var userLang = navigator.language || navigator.userLanguage;;
|
||||
if (userLang.indexOf('zh') !== -1) {
|
||||
defaultOption.locale = zhLocale;
|
||||
}
|
||||
options = Object.assign(defaultOption, options);
|
||||
return $(selector).daterangepicker(options);
|
||||
}
|
||||
|
|
|
@ -12,4 +12,4 @@
|
|||
<script src="{% static 'js/jquery-3.1.1.min.js' %}"></script>
|
||||
<script src="{% static 'js/plugins/sweetalert/sweetalert.min.js' %}"></script>
|
||||
<script src="{% static 'js/bootstrap.min.js' %}"></script>
|
||||
<script src="{% static 'js/plugins/datatables/datatables.min.js' %}"></script>
|
||||
<script src="{% static 'js/plugins/datatables/datatables.min.js' %}"></script>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
|
||||
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.zh-CN.min.js' %}"></script>
|
||||
<script type="text/javascript" src='{% static "js/plugins/daterangepicker/moment.min.js" %}'></script>
|
||||
<script type="text/javascript" src='{% static "js/plugins/daterangepicker/daterangepicker.min.js" %}'></script>
|
||||
<link rel="stylesheet" type="text/css" href={% static "css/plugins/daterangepicker/daterangepicker.css" %} />
|
||||
|
@ -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");
|
||||
|
|
|
@ -212,7 +212,7 @@
|
|||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% if user_object.is_current_org_admin or user_object.is_superuser %}
|
||||
{% if user.is_current_org_admin or user.is_superuser %}
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'User group' %}
|
||||
|
|
|
@ -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/<uuid:pk>/otp/reset/', api.UserResetOTPApi.as_view(), name='user-reset-otp'),
|
||||
|
|
|
@ -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"):
|
||||
|
|
Loading…
Reference in New Issue