[Update] 基本完成登录逻辑

pull/3428/head
ibuler 2019-11-05 18:46:29 +08:00
parent 9d201bbf98
commit 6ce9815d51
36 changed files with 874 additions and 819 deletions

View File

@ -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']

View File

@ -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)

View File

@ -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)

View File

@ -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>

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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'

View File

@ -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):

View File

@ -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
)

View File

@ -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):

View File

@ -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)

View File

@ -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()

View File

@ -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 }}

View File

@ -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
})

View File

@ -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>

View File

@ -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')

View File

@ -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

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
#
from .login import *
from .mfa import *

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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.

View File

@ -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 "收件人"

View File

@ -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'){

View File

@ -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();

View File

@ -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);
}

View File

@ -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>

View File

@ -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'

View File

@ -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");

View File

@ -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' %}

View File

@ -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'),

View File

@ -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"):