mirror of https://github.com/jumpserver/jumpserver
feat(authentication): 类似腾讯企业邮单点登录功能
parent
4e7a5d8d4f
commit
90f03dda62
|
@ -6,3 +6,4 @@ from .token import *
|
||||||
from .mfa import *
|
from .mfa import *
|
||||||
from .access_key import *
|
from .access_key import *
|
||||||
from .login_confirm import *
|
from .login_confirm import *
|
||||||
|
from .sso import *
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
from uuid import UUID
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
from django.contrib.auth import login
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http.response import HttpResponseRedirect
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.request import Request
|
||||||
|
|
||||||
|
from common.utils.timezone import utcnow
|
||||||
|
from common.const.http import POST, GET
|
||||||
|
from common.drf.api import JmsGenericViewSet
|
||||||
|
from common.drf.serializers import EmptySerializer
|
||||||
|
from common.permissions import IsSuperUser
|
||||||
|
from common.utils import reverse
|
||||||
|
from users.models import User
|
||||||
|
from ..serializers import SSOTokenSerializer
|
||||||
|
from ..models import SSOToken
|
||||||
|
from ..filters import AuthKeyQueryDeclaration
|
||||||
|
from ..mixins import AuthMixin
|
||||||
|
from ..errors import SSOAuthClosed
|
||||||
|
|
||||||
|
|
||||||
|
class SSOViewSet(AuthMixin, JmsGenericViewSet):
|
||||||
|
queryset = SSOToken.objects.all()
|
||||||
|
serializer_classes = {
|
||||||
|
'get_login_url': SSOTokenSerializer,
|
||||||
|
'login': EmptySerializer
|
||||||
|
}
|
||||||
|
|
||||||
|
@action(methods=[POST], detail=False, permission_classes=[IsSuperUser])
|
||||||
|
def get_login_url(self, request, *args, **kwargs):
|
||||||
|
if not settings.AUTH_SSO:
|
||||||
|
raise SSOAuthClosed()
|
||||||
|
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
username = serializer.validated_data['username']
|
||||||
|
user = User.objects.get(username=username)
|
||||||
|
|
||||||
|
operator = request.user.username
|
||||||
|
# TODO `created_by` 和 `created_by` 可以通过 `ThreadLocal` 统一处理
|
||||||
|
token = SSOToken.objects.create(user=user, created_by=operator, updated_by=operator)
|
||||||
|
query = {
|
||||||
|
'authkey': token.authkey
|
||||||
|
}
|
||||||
|
login_url = '%s?%s' % (reverse('api-auth:sso-login', external=True), urlencode(query))
|
||||||
|
return Response(data={'login_url': login_url})
|
||||||
|
|
||||||
|
@action(methods=[GET], detail=False, filter_backends=[AuthKeyQueryDeclaration], permission_classes=[])
|
||||||
|
def login(self, request: Request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
此接口违反了 `Restful` 的规范
|
||||||
|
`GET` 应该是安全的方法,但此接口是不安全的
|
||||||
|
"""
|
||||||
|
authkey = request.query_params.get('authkey')
|
||||||
|
try:
|
||||||
|
authkey = UUID(authkey)
|
||||||
|
token = SSOToken.objects.get(authkey=authkey, expired=False)
|
||||||
|
# 先过期,只能访问这一次
|
||||||
|
token.expired = True
|
||||||
|
token.save()
|
||||||
|
except (ValueError, SSOToken.DoesNotExist):
|
||||||
|
self.send_auth_signal(success=False, reason=f'authkey invalid: {authkey}')
|
||||||
|
return HttpResponseRedirect(reverse('authentication:login'))
|
||||||
|
|
||||||
|
# 判断是否过期
|
||||||
|
if (utcnow().timestamp() - token.date_created.timestamp()) > settings.AUTH_SSO_AUTHKEY_TTL:
|
||||||
|
self.send_auth_signal(success=False, reason=f'authkey timeout: {authkey}')
|
||||||
|
return HttpResponseRedirect(reverse('authentication:login'))
|
||||||
|
|
||||||
|
user = token.user
|
||||||
|
login(self.request, user, 'authentication.backends.api.SSOAuthentication')
|
||||||
|
self.send_auth_signal(success=True, user=user)
|
||||||
|
return HttpResponseRedirect(reverse('index'))
|
|
@ -5,14 +5,13 @@ import uuid
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils.six import text_type
|
from django.utils.six import text_type
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.backends import ModelBackend
|
||||||
from rest_framework import HTTP_HEADER_ENCODING
|
from rest_framework import HTTP_HEADER_ENCODING
|
||||||
from rest_framework import authentication, exceptions
|
from rest_framework import authentication, exceptions
|
||||||
from common.auth import signature
|
from common.auth import signature
|
||||||
from rest_framework.authentication import CSRFCheck
|
|
||||||
|
|
||||||
from common.utils import get_object_or_none, make_signature, http_to_unixtime
|
from common.utils import get_object_or_none, make_signature, http_to_unixtime
|
||||||
from ..models import AccessKey, PrivateToken
|
from ..models import AccessKey, PrivateToken
|
||||||
|
@ -197,3 +196,10 @@ class SignatureAuthentication(signature.SignatureAuthentication):
|
||||||
return user, secret
|
return user, secret
|
||||||
except AccessKey.DoesNotExist:
|
except AccessKey.DoesNotExist:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
class SSOAuthentication(ModelBackend):
|
||||||
|
"""
|
||||||
|
什么也不做呀😺
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
|
@ -4,6 +4,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
from common.exceptions import JMSException
|
||||||
from .signals import post_auth_failed
|
from .signals import post_auth_failed
|
||||||
from users.utils import (
|
from users.utils import (
|
||||||
increase_login_failed_count, get_login_failed_count
|
increase_login_failed_count, get_login_failed_count
|
||||||
|
@ -205,3 +206,8 @@ class LoginConfirmOtherError(LoginConfirmBaseError):
|
||||||
def __init__(self, ticket_id, status):
|
def __init__(self, ticket_id, status):
|
||||||
msg = login_confirm_error_msg.format(status)
|
msg = login_confirm_error_msg.format(status)
|
||||||
super().__init__(ticket_id=ticket_id, msg=msg)
|
super().__init__(ticket_id=ticket_id, msg=msg)
|
||||||
|
|
||||||
|
|
||||||
|
class SSOAuthClosed(JMSException):
|
||||||
|
default_code = 'sso_auth_closed'
|
||||||
|
default_detail = _('SSO auth closed')
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
from rest_framework import filters
|
||||||
|
from rest_framework.compat import coreapi, coreschema
|
||||||
|
|
||||||
|
|
||||||
|
class AuthKeyQueryDeclaration(filters.BaseFilterBackend):
|
||||||
|
def get_schema_fields(self, view):
|
||||||
|
return [
|
||||||
|
coreapi.Field(
|
||||||
|
name='authkey', location='query', required=True, type='string',
|
||||||
|
schema=coreschema.String(
|
||||||
|
title='authkey',
|
||||||
|
description='authkey'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 2.2.10 on 2020-07-31 08:36
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('authentication', '0003_loginconfirmsetting'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SSOToken',
|
||||||
|
fields=[
|
||||||
|
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||||
|
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
|
||||||
|
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||||
|
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||||
|
('authkey', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='Token')),
|
||||||
|
('expired', models.BooleanField(default=False, verbose_name='Expired')),
|
||||||
|
('user', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,10 +1,13 @@
|
||||||
import uuid
|
import uuid
|
||||||
from django.db import models
|
from functools import partial
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _, ugettext as __
|
from django.utils.translation import ugettext_lazy as _, ugettext as __
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
|
|
||||||
|
from common.db import models
|
||||||
from common.mixins.models import CommonModelMixin
|
from common.mixins.models import CommonModelMixin
|
||||||
from common.utils import get_object_or_none, get_request_ip, get_ip_city
|
from common.utils import get_object_or_none, get_request_ip, get_ip_city
|
||||||
|
|
||||||
|
@ -76,3 +79,12 @@ class LoginConfirmSetting(CommonModelMixin):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} confirm'.format(self.user.username)
|
return '{} confirm'.format(self.user.username)
|
||||||
|
|
||||||
|
|
||||||
|
class SSOToken(models.JMSBaseModel):
|
||||||
|
"""
|
||||||
|
类似腾讯企业邮的 [单点登录](https://exmail.qq.com/qy_mng_logic/doc#10036)
|
||||||
|
出于安全考虑,这里的 `token` 使用一次随即过期。但我们保留每一个生成过的 `token`。
|
||||||
|
"""
|
||||||
|
authkey = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name=_('Token'))
|
||||||
|
expired = models.BooleanField(default=False, verbose_name=_('Expired'))
|
||||||
|
user = models.ForeignKey('users.User', on_delete=models.PROTECT, verbose_name=_('User'), db_constraint=False)
|
||||||
|
|
|
@ -5,12 +5,12 @@ from rest_framework import serializers
|
||||||
from common.utils import get_object_or_none
|
from common.utils import get_object_or_none
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from users.serializers import UserProfileSerializer
|
from users.serializers import UserProfileSerializer
|
||||||
from .models import AccessKey, LoginConfirmSetting
|
from .models import AccessKey, LoginConfirmSetting, SSOToken
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
|
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
|
||||||
'MFAChallengeSerializer', 'LoginConfirmSettingSerializer',
|
'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,3 +76,8 @@ class LoginConfirmSettingSerializer(serializers.ModelSerializer):
|
||||||
model = LoginConfirmSetting
|
model = LoginConfirmSetting
|
||||||
fields = ['id', 'user', 'reviewers', 'date_created', 'date_updated']
|
fields = ['id', 'user', 'reviewers', 'date_created', 'date_updated']
|
||||||
read_only_fields = ['date_created', 'date_updated']
|
read_only_fields = ['date_created', 'date_updated']
|
||||||
|
|
||||||
|
|
||||||
|
class SSOTokenSerializer(serializers.Serializer):
|
||||||
|
username = serializers.CharField(write_only=True)
|
||||||
|
login_url = serializers.CharField(read_only=True)
|
||||||
|
|
|
@ -8,6 +8,7 @@ from .. import api
|
||||||
app_name = 'authentication'
|
app_name = 'authentication'
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register('access-keys', api.AccessKeyViewSet, 'access-key')
|
router.register('access-keys', api.AccessKeyViewSet, 'access-key')
|
||||||
|
router.register('sso', api.SSOViewSet, 'sso')
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
|
@ -1,4 +1,18 @@
|
||||||
from functools import partial
|
"""
|
||||||
|
此文件作为 `django.db.models` 的 shortcut
|
||||||
|
|
||||||
|
这样做的优点与缺点为:
|
||||||
|
优点:
|
||||||
|
- 包命名都统一为 `models`
|
||||||
|
- 用户在使用的时候只导入本文件即可
|
||||||
|
缺点:
|
||||||
|
- 此文件中添加代码的时候,注意不要跟 `django.db.models` 中的命名冲突
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.db.models import *
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class Choice(str):
|
class Choice(str):
|
||||||
|
@ -46,3 +60,20 @@ class ChoiceSetType(type):
|
||||||
|
|
||||||
class ChoiceSet(metaclass=ChoiceSetType):
|
class ChoiceSet(metaclass=ChoiceSetType):
|
||||||
choices = None # 用于 Django Model 中的 choices 配置, 为了代码提示在此声明
|
choices = None # 用于 Django Model 中的 choices 配置, 为了代码提示在此声明
|
||||||
|
|
||||||
|
|
||||||
|
class JMSBaseModel(Model):
|
||||||
|
created_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
|
||||||
|
updated_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Updated by'))
|
||||||
|
date_created = DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
|
||||||
|
date_updated = DateTimeField(auto_now=True, verbose_name=_('Date updated'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class JMSModel(JMSBaseModel):
|
||||||
|
id = UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist as DJObjectDoesNotExist
|
||||||
|
from django.http import Http404
|
||||||
|
from django.utils.translation import gettext
|
||||||
|
|
||||||
|
from rest_framework import exceptions
|
||||||
|
from rest_framework.views import set_rollback
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from common.exceptions import JMSObjectDoesNotExist
|
||||||
|
|
||||||
|
|
||||||
|
def extract_object_name(exc, index=0):
|
||||||
|
"""
|
||||||
|
`index` 是从 0 开始数的, 比如:
|
||||||
|
`No User matches the given query.`
|
||||||
|
提取 `User`,`index=1`
|
||||||
|
"""
|
||||||
|
(msg, *_) = exc.args
|
||||||
|
return gettext(msg.split(sep=' ', maxsplit=index + 1)[index])
|
||||||
|
|
||||||
|
|
||||||
|
def common_exception_handler(exc, context):
|
||||||
|
if isinstance(exc, Http404):
|
||||||
|
exc = JMSObjectDoesNotExist(object_name=extract_object_name(exc, 1))
|
||||||
|
elif isinstance(exc, PermissionDenied):
|
||||||
|
exc = exceptions.PermissionDenied()
|
||||||
|
elif isinstance(exc, DJObjectDoesNotExist):
|
||||||
|
exc = JMSObjectDoesNotExist(object_name=extract_object_name(exc, 0))
|
||||||
|
|
||||||
|
if isinstance(exc, exceptions.APIException):
|
||||||
|
headers = {}
|
||||||
|
if getattr(exc, 'auth_header', None):
|
||||||
|
headers['WWW-Authenticate'] = exc.auth_header
|
||||||
|
if getattr(exc, 'wait', None):
|
||||||
|
headers['Retry-After'] = '%d' % exc.wait
|
||||||
|
|
||||||
|
if isinstance(exc.detail, (list, dict)):
|
||||||
|
data = exc.detail
|
||||||
|
else:
|
||||||
|
data = {'detail': exc.detail}
|
||||||
|
|
||||||
|
set_rollback()
|
||||||
|
return Response(data, status=exc.status_code, headers=headers)
|
||||||
|
|
||||||
|
return None
|
|
@ -1,8 +1,20 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.exceptions import APIException
|
from rest_framework.exceptions import APIException
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
|
|
||||||
class JMSException(APIException):
|
class JMSException(APIException):
|
||||||
status_code = status.HTTP_400_BAD_REQUEST
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
|
||||||
|
|
||||||
|
class JMSObjectDoesNotExist(APIException):
|
||||||
|
status_code = status.HTTP_404_NOT_FOUND
|
||||||
|
default_code = 'object_does_not_exist'
|
||||||
|
default_detail = _('%s object does not exist.')
|
||||||
|
|
||||||
|
def __init__(self, detail=None, code=None, object_name=None):
|
||||||
|
if detail is None and object_name:
|
||||||
|
detail = self.default_detail % object_name
|
||||||
|
super(JMSObjectDoesNotExist, self).__init__(detail=detail, code=code)
|
||||||
|
|
|
@ -211,6 +211,9 @@ class Config(dict):
|
||||||
'CAS_LOGOUT_COMPLETELY': True,
|
'CAS_LOGOUT_COMPLETELY': True,
|
||||||
'CAS_VERSION': 3,
|
'CAS_VERSION': 3,
|
||||||
|
|
||||||
|
'AUTH_SSO': False,
|
||||||
|
'AUTH_SSO_AUTHKEY_TTL': 60 * 15,
|
||||||
|
|
||||||
'OTP_VALID_WINDOW': 2,
|
'OTP_VALID_WINDOW': 2,
|
||||||
'OTP_ISSUER_NAME': 'JumpServer',
|
'OTP_ISSUER_NAME': 'JumpServer',
|
||||||
'EMAIL_SUFFIX': 'jumpserver.org',
|
'EMAIL_SUFFIX': 'jumpserver.org',
|
||||||
|
@ -440,6 +443,8 @@ class DynamicConfig:
|
||||||
backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthCodeBackend')
|
backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthCodeBackend')
|
||||||
if self.static_config.get('AUTH_RADIUS'):
|
if self.static_config.get('AUTH_RADIUS'):
|
||||||
backends.insert(0, 'authentication.backends.radius.RadiusBackend')
|
backends.insert(0, 'authentication.backends.radius.RadiusBackend')
|
||||||
|
if self.static_config.get('AUTH_SSO'):
|
||||||
|
backends.insert(0, 'authentication.backends.api.SSOAuthentication')
|
||||||
return backends
|
return backends
|
||||||
|
|
||||||
def XPACK_LICENSE_IS_VALID(self):
|
def XPACK_LICENSE_IS_VALID(self):
|
||||||
|
|
|
@ -94,6 +94,9 @@ CAS_VERSION = CONFIG.CAS_VERSION
|
||||||
CAS_ROOT_PROXIED_AS = CONFIG.CAS_ROOT_PROXIED_AS
|
CAS_ROOT_PROXIED_AS = CONFIG.CAS_ROOT_PROXIED_AS
|
||||||
CAS_CHECK_NEXT = lambda: lambda _next_page: True
|
CAS_CHECK_NEXT = lambda: lambda _next_page: True
|
||||||
|
|
||||||
|
# SSO Auth
|
||||||
|
AUTH_SSO = CONFIG.AUTH_SSO
|
||||||
|
AUTH_SSO_AUTHKEY_TTL = CONFIG.AUTH_SSO_AUTHKEY_TTL
|
||||||
|
|
||||||
# Other setting
|
# Other setting
|
||||||
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
||||||
|
|
|
@ -40,6 +40,7 @@ REST_FRAMEWORK = {
|
||||||
'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S %z',
|
'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S %z',
|
||||||
'DATETIME_INPUT_FORMATS': ['iso-8601', '%Y-%m-%d %H:%M:%S %z'],
|
'DATETIME_INPUT_FORMATS': ['iso-8601', '%Y-%m-%d %H:%M:%S %z'],
|
||||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
||||||
|
'EXCEPTION_HANDLER': 'common.drf.exc_handlers.common_exception_handler',
|
||||||
# 'PAGE_SIZE': 100,
|
# 'PAGE_SIZE': 100,
|
||||||
# 'MAX_PAGE_SIZE': 5000
|
# 'MAX_PAGE_SIZE': 5000
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: JumpServer 0.3.3\n"
|
"Project-Id-Version: JumpServer 0.3.3\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2020-07-29 15:03+0800\n"
|
"POT-Creation-Date: 2020-07-31 19:20+0800\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: ibuler <ibuler@qq.com>\n"
|
"Last-Translator: ibuler <ibuler@qq.com>\n"
|
||||||
"Language-Team: JumpServer team<ibuler@qq.com>\n"
|
"Language-Team: JumpServer team<ibuler@qq.com>\n"
|
||||||
|
@ -131,9 +131,10 @@ msgstr "参数"
|
||||||
#: applications/models/remote_app.py:39 assets/models/asset.py:224
|
#: applications/models/remote_app.py:39 assets/models/asset.py:224
|
||||||
#: assets/models/base.py:240 assets/models/cluster.py:28
|
#: assets/models/base.py:240 assets/models/cluster.py:28
|
||||||
#: assets/models/cmd_filter.py:26 assets/models/cmd_filter.py:60
|
#: assets/models/cmd_filter.py:26 assets/models/cmd_filter.py:60
|
||||||
#: assets/models/group.py:21 common/mixins/models.py:49 orgs/models.py:23
|
#: assets/models/group.py:21 common/db/models.py:66 common/mixins/models.py:49
|
||||||
#: orgs/models.py:316 perms/models/base.py:54 users/models/user.py:530
|
#: orgs/models.py:23 orgs/models.py:316 perms/models/base.py:54
|
||||||
#: users/serializers/group.py:35 users/templates/users/user_detail.html:97
|
#: users/models/user.py:530 users/serializers/group.py:35
|
||||||
|
#: users/templates/users/user_detail.html:97
|
||||||
#: xpack/plugins/change_auth_plan/models.py:81 xpack/plugins/cloud/models.py:56
|
#: xpack/plugins/change_auth_plan/models.py:81 xpack/plugins/cloud/models.py:56
|
||||||
#: xpack/plugins/cloud/models.py:146 xpack/plugins/gathered_user/models.py:30
|
#: xpack/plugins/cloud/models.py:146 xpack/plugins/gathered_user/models.py:30
|
||||||
msgid "Created by"
|
msgid "Created by"
|
||||||
|
@ -144,7 +145,7 @@ msgstr "创建者"
|
||||||
#: applications/models/remote_app.py:42 assets/models/asset.py:225
|
#: applications/models/remote_app.py:42 assets/models/asset.py:225
|
||||||
#: assets/models/base.py:238 assets/models/cluster.py:26
|
#: assets/models/base.py:238 assets/models/cluster.py:26
|
||||||
#: assets/models/domain.py:23 assets/models/gathered_user.py:19
|
#: assets/models/domain.py:23 assets/models/gathered_user.py:19
|
||||||
#: assets/models/group.py:22 assets/models/label.py:25
|
#: assets/models/group.py:22 assets/models/label.py:25 common/db/models.py:68
|
||||||
#: common/mixins/models.py:50 ops/models/adhoc.py:38 ops/models/command.py:27
|
#: common/mixins/models.py:50 ops/models/adhoc.py:38 ops/models/command.py:27
|
||||||
#: orgs/models.py:24 orgs/models.py:314 perms/models/base.py:55
|
#: orgs/models.py:24 orgs/models.py:314 perms/models/base.py:55
|
||||||
#: users/models/group.py:18 users/templates/users/user_group_detail.html:58
|
#: users/models/group.py:18 users/templates/users/user_group_detail.html:58
|
||||||
|
@ -241,7 +242,7 @@ msgstr "节点"
|
||||||
|
|
||||||
#: assets/models/asset.py:196 assets/models/cmd_filter.py:22
|
#: assets/models/asset.py:196 assets/models/cmd_filter.py:22
|
||||||
#: assets/models/domain.py:55 assets/models/label.py:22
|
#: assets/models/domain.py:55 assets/models/label.py:22
|
||||||
#: authentication/models.py:45
|
#: authentication/models.py:48
|
||||||
msgid "Is active"
|
msgid "Is active"
|
||||||
msgstr "激活"
|
msgstr "激活"
|
||||||
|
|
||||||
|
@ -379,7 +380,8 @@ msgid "SSH public key"
|
||||||
msgstr "SSH公钥"
|
msgstr "SSH公钥"
|
||||||
|
|
||||||
#: assets/models/base.py:239 assets/models/gathered_user.py:20
|
#: assets/models/base.py:239 assets/models/gathered_user.py:20
|
||||||
#: common/mixins/models.py:51 ops/models/adhoc.py:39 orgs/models.py:315
|
#: common/db/models.py:69 common/mixins/models.py:51 ops/models/adhoc.py:39
|
||||||
|
#: orgs/models.py:315
|
||||||
msgid "Date updated"
|
msgid "Date updated"
|
||||||
msgstr "更新日期"
|
msgstr "更新日期"
|
||||||
|
|
||||||
|
@ -534,9 +536,9 @@ msgid "Default asset group"
|
||||||
msgstr "默认资产组"
|
msgstr "默认资产组"
|
||||||
|
|
||||||
#: assets/models/label.py:15 audits/models.py:36 audits/models.py:56
|
#: assets/models/label.py:15 audits/models.py:36 audits/models.py:56
|
||||||
#: audits/models.py:69 audits/serializers.py:77 authentication/models.py:43
|
#: audits/models.py:69 audits/serializers.py:77 authentication/models.py:46
|
||||||
#: orgs/models.py:16 orgs/models.py:312 perms/forms/asset_permission.py:83
|
#: authentication/models.py:90 orgs/models.py:16 orgs/models.py:312
|
||||||
#: perms/forms/database_app_permission.py:38
|
#: perms/forms/asset_permission.py:83 perms/forms/database_app_permission.py:38
|
||||||
#: perms/forms/remote_app_permission.py:40 perms/models/base.py:49
|
#: perms/forms/remote_app_permission.py:40 perms/models/base.py:49
|
||||||
#: templates/index.html:78 terminal/backends/command/models.py:18
|
#: templates/index.html:78 terminal/backends/command/models.py:18
|
||||||
#: terminal/backends/command/serializers.py:12 terminal/models.py:185
|
#: terminal/backends/command/serializers.py:12 terminal/models.py:185
|
||||||
|
@ -1042,94 +1044,94 @@ msgstr "运行用户"
|
||||||
msgid "Code is invalid"
|
msgid "Code is invalid"
|
||||||
msgstr "Code无效"
|
msgstr "Code无效"
|
||||||
|
|
||||||
#: authentication/backends/api.py:53
|
#: authentication/backends/api.py:52
|
||||||
msgid "Invalid signature header. No credentials provided."
|
msgid "Invalid signature header. No credentials provided."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentication/backends/api.py:56
|
#: authentication/backends/api.py:55
|
||||||
msgid "Invalid signature header. Signature string should not contain spaces."
|
msgid "Invalid signature header. Signature string should not contain spaces."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentication/backends/api.py:63
|
#: authentication/backends/api.py:62
|
||||||
msgid "Invalid signature header. Format like AccessKeyId:Signature"
|
msgid "Invalid signature header. Format like AccessKeyId:Signature"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentication/backends/api.py:67
|
#: authentication/backends/api.py:66
|
||||||
msgid ""
|
msgid ""
|
||||||
"Invalid signature header. Signature string should not contain invalid "
|
"Invalid signature header. Signature string should not contain invalid "
|
||||||
"characters."
|
"characters."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentication/backends/api.py:87 authentication/backends/api.py:103
|
#: authentication/backends/api.py:86 authentication/backends/api.py:102
|
||||||
msgid "Invalid signature."
|
msgid "Invalid signature."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentication/backends/api.py:94
|
#: authentication/backends/api.py:93
|
||||||
msgid "HTTP header: Date not provide or not %a, %d %b %Y %H:%M:%S GMT"
|
msgid "HTTP header: Date not provide or not %a, %d %b %Y %H:%M:%S GMT"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentication/backends/api.py:99
|
#: authentication/backends/api.py:98
|
||||||
msgid "Expired, more than 15 minutes"
|
msgid "Expired, more than 15 minutes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentication/backends/api.py:106
|
#: authentication/backends/api.py:105
|
||||||
msgid "User disabled."
|
msgid "User disabled."
|
||||||
msgstr "用户已禁用"
|
msgstr "用户已禁用"
|
||||||
|
|
||||||
#: authentication/backends/api.py:124
|
#: authentication/backends/api.py:123
|
||||||
msgid "Invalid token header. No credentials provided."
|
msgid "Invalid token header. No credentials provided."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentication/backends/api.py:127
|
#: authentication/backends/api.py:126
|
||||||
msgid "Invalid token header. Sign string should not contain spaces."
|
msgid "Invalid token header. Sign string should not contain spaces."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentication/backends/api.py:134
|
#: authentication/backends/api.py:133
|
||||||
msgid ""
|
msgid ""
|
||||||
"Invalid token header. Sign string should not contain invalid characters."
|
"Invalid token header. Sign string should not contain invalid characters."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentication/backends/api.py:145
|
#: authentication/backends/api.py:144
|
||||||
msgid "Invalid token or cache refreshed."
|
msgid "Invalid token or cache refreshed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentication/errors.py:22
|
#: authentication/errors.py:23
|
||||||
msgid "Username/password check failed"
|
msgid "Username/password check failed"
|
||||||
msgstr "用户名/密码 校验失败"
|
msgstr "用户名/密码 校验失败"
|
||||||
|
|
||||||
#: authentication/errors.py:23
|
#: authentication/errors.py:24
|
||||||
msgid "Password decrypt failed"
|
msgid "Password decrypt failed"
|
||||||
msgstr "密码解密失败"
|
msgstr "密码解密失败"
|
||||||
|
|
||||||
#: authentication/errors.py:24
|
#: authentication/errors.py:25
|
||||||
msgid "MFA failed"
|
msgid "MFA failed"
|
||||||
msgstr "多因子认证失败"
|
msgstr "多因子认证失败"
|
||||||
|
|
||||||
#: authentication/errors.py:25
|
#: authentication/errors.py:26
|
||||||
msgid "MFA unset"
|
msgid "MFA unset"
|
||||||
msgstr "多因子认证没有设定"
|
msgstr "多因子认证没有设定"
|
||||||
|
|
||||||
#: authentication/errors.py:26
|
#: authentication/errors.py:27
|
||||||
msgid "Username does not exist"
|
msgid "Username does not exist"
|
||||||
msgstr "用户名不存在"
|
msgstr "用户名不存在"
|
||||||
|
|
||||||
#: authentication/errors.py:27
|
#: authentication/errors.py:28
|
||||||
msgid "Password expired"
|
msgid "Password expired"
|
||||||
msgstr "密码已过期"
|
msgstr "密码已过期"
|
||||||
|
|
||||||
#: authentication/errors.py:28
|
#: authentication/errors.py:29
|
||||||
msgid "Disabled or expired"
|
msgid "Disabled or expired"
|
||||||
msgstr "禁用或失效"
|
msgstr "禁用或失效"
|
||||||
|
|
||||||
#: authentication/errors.py:29
|
#: authentication/errors.py:30
|
||||||
msgid "This account is inactive."
|
msgid "This account is inactive."
|
||||||
msgstr "此账户已禁用"
|
msgstr "此账户已禁用"
|
||||||
|
|
||||||
#: authentication/errors.py:39
|
#: authentication/errors.py:40
|
||||||
msgid "No session found, check your cookie"
|
msgid "No session found, check your cookie"
|
||||||
msgstr "会话已变更,刷新页面"
|
msgstr "会话已变更,刷新页面"
|
||||||
|
|
||||||
#: authentication/errors.py:41
|
#: authentication/errors.py:42
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"The username or password you entered is incorrect, please enter it again. "
|
"The username or password you entered is incorrect, please enter it again. "
|
||||||
|
@ -1139,37 +1141,41 @@ msgstr ""
|
||||||
"您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将"
|
"您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将"
|
||||||
"被临时 锁定 {block_time} 分钟)"
|
"被临时 锁定 {block_time} 分钟)"
|
||||||
|
|
||||||
#: authentication/errors.py:47
|
#: authentication/errors.py:48
|
||||||
msgid ""
|
msgid ""
|
||||||
"The account has been locked (please contact admin to unlock it or try again "
|
"The account has been locked (please contact admin to unlock it or try again "
|
||||||
"after {} minutes)"
|
"after {} minutes)"
|
||||||
msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)"
|
msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)"
|
||||||
|
|
||||||
#: authentication/errors.py:50 users/views/profile/otp.py:107
|
#: authentication/errors.py:51 users/views/profile/otp.py:107
|
||||||
#: users/views/profile/otp.py:146 users/views/profile/otp.py:166
|
#: users/views/profile/otp.py:146 users/views/profile/otp.py:166
|
||||||
msgid "MFA code invalid, or ntp sync server time"
|
msgid "MFA code invalid, or ntp sync server time"
|
||||||
msgstr "MFA验证码不正确,或者服务器端时间不对"
|
msgstr "MFA验证码不正确,或者服务器端时间不对"
|
||||||
|
|
||||||
#: authentication/errors.py:52
|
#: authentication/errors.py:53
|
||||||
msgid "MFA required"
|
msgid "MFA required"
|
||||||
msgstr "需要多因子认证"
|
msgstr "需要多因子认证"
|
||||||
|
|
||||||
#: authentication/errors.py:53
|
#: authentication/errors.py:54
|
||||||
msgid "MFA not set, please set it first"
|
msgid "MFA not set, please set it first"
|
||||||
msgstr "多因子认证没有设置,请先完成设置"
|
msgstr "多因子认证没有设置,请先完成设置"
|
||||||
|
|
||||||
#: authentication/errors.py:54
|
#: authentication/errors.py:55
|
||||||
msgid "Login confirm required"
|
msgid "Login confirm required"
|
||||||
msgstr "需要登录复核"
|
msgstr "需要登录复核"
|
||||||
|
|
||||||
#: authentication/errors.py:55
|
#: authentication/errors.py:56
|
||||||
msgid "Wait login confirm ticket for accept"
|
msgid "Wait login confirm ticket for accept"
|
||||||
msgstr "等待登录复核处理"
|
msgstr "等待登录复核处理"
|
||||||
|
|
||||||
#: authentication/errors.py:56
|
#: authentication/errors.py:57
|
||||||
msgid "Login confirm ticket was {}"
|
msgid "Login confirm ticket was {}"
|
||||||
msgstr "登录复核 {}"
|
msgstr "登录复核 {}"
|
||||||
|
|
||||||
|
#: authentication/errors.py:213
|
||||||
|
msgid "SSO auth closed"
|
||||||
|
msgstr "SSO 认证关闭了"
|
||||||
|
|
||||||
#: authentication/forms.py:26 authentication/forms.py:34
|
#: authentication/forms.py:26 authentication/forms.py:34
|
||||||
#: authentication/templates/authentication/login.html:38
|
#: authentication/templates/authentication/login.html:38
|
||||||
#: authentication/templates/authentication/xpack_login.html:118
|
#: authentication/templates/authentication/xpack_login.html:118
|
||||||
|
@ -1177,7 +1183,7 @@ msgstr "登录复核 {}"
|
||||||
msgid "MFA code"
|
msgid "MFA code"
|
||||||
msgstr "多因子认证验证码"
|
msgstr "多因子认证验证码"
|
||||||
|
|
||||||
#: authentication/models.py:19
|
#: authentication/models.py:22
|
||||||
#: authentication/templates/authentication/_access_key_modal.html:32
|
#: authentication/templates/authentication/_access_key_modal.html:32
|
||||||
#: perms/models/base.py:51 users/templates/users/_select_user_modal.html:18
|
#: perms/models/base.py:51 users/templates/users/_select_user_modal.html:18
|
||||||
#: users/templates/users/user_detail.html:132
|
#: users/templates/users/user_detail.html:132
|
||||||
|
@ -1185,23 +1191,31 @@ msgstr "多因子认证验证码"
|
||||||
msgid "Active"
|
msgid "Active"
|
||||||
msgstr "激活中"
|
msgstr "激活中"
|
||||||
|
|
||||||
#: authentication/models.py:39
|
#: authentication/models.py:42
|
||||||
msgid "Private Token"
|
msgid "Private Token"
|
||||||
msgstr "SSH密钥"
|
msgstr "SSH密钥"
|
||||||
|
|
||||||
#: authentication/models.py:44 users/templates/users/user_detail.html:258
|
#: authentication/models.py:47 users/templates/users/user_detail.html:258
|
||||||
msgid "Reviewers"
|
msgid "Reviewers"
|
||||||
msgstr "审批人"
|
msgstr "审批人"
|
||||||
|
|
||||||
#: authentication/models.py:53 tickets/models/ticket.py:27
|
#: authentication/models.py:56 tickets/models/ticket.py:27
|
||||||
#: users/templates/users/user_detail.html:250
|
#: users/templates/users/user_detail.html:250
|
||||||
msgid "Login confirm"
|
msgid "Login confirm"
|
||||||
msgstr "登录复核"
|
msgstr "登录复核"
|
||||||
|
|
||||||
#: authentication/models.py:63
|
#: authentication/models.py:66
|
||||||
msgid "City"
|
msgid "City"
|
||||||
msgstr "城市"
|
msgstr "城市"
|
||||||
|
|
||||||
|
#: authentication/models.py:88
|
||||||
|
msgid "Token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentication/models.py:89
|
||||||
|
msgid "Expired"
|
||||||
|
msgstr "过期时间"
|
||||||
|
|
||||||
#: authentication/templates/authentication/_access_key_modal.html:6
|
#: authentication/templates/authentication/_access_key_modal.html:6
|
||||||
msgid "API key list"
|
msgid "API key list"
|
||||||
msgstr "API Key列表"
|
msgstr "API Key列表"
|
||||||
|
@ -1349,7 +1363,7 @@ msgstr "欢迎回来,请输入用户名和密码登录"
|
||||||
msgid "Please enable cookies and try again."
|
msgid "Please enable cookies and try again."
|
||||||
msgstr "设置你的浏览器支持cookie"
|
msgstr "设置你的浏览器支持cookie"
|
||||||
|
|
||||||
#: authentication/views/login.py:172
|
#: authentication/views/login.py:178
|
||||||
msgid ""
|
msgid ""
|
||||||
"Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>\n"
|
"Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>\n"
|
||||||
" Don't close this page"
|
" Don't close this page"
|
||||||
|
@ -1357,15 +1371,15 @@ msgstr ""
|
||||||
"等待 <b>{}</b> 确认, 你也可以复制链接发给他/她 <br/>\n"
|
"等待 <b>{}</b> 确认, 你也可以复制链接发给他/她 <br/>\n"
|
||||||
" 不要关闭本页面"
|
" 不要关闭本页面"
|
||||||
|
|
||||||
#: authentication/views/login.py:177
|
#: authentication/views/login.py:183
|
||||||
msgid "No ticket found"
|
msgid "No ticket found"
|
||||||
msgstr "没有发现工单"
|
msgstr "没有发现工单"
|
||||||
|
|
||||||
#: authentication/views/login.py:209
|
#: authentication/views/login.py:215
|
||||||
msgid "Logout success"
|
msgid "Logout success"
|
||||||
msgstr "退出登录成功"
|
msgstr "退出登录成功"
|
||||||
|
|
||||||
#: authentication/views/login.py:210
|
#: authentication/views/login.py:216
|
||||||
msgid "Logout success, return login page"
|
msgid "Logout success, return login page"
|
||||||
msgstr "退出登录成功,返回到登录页面"
|
msgstr "退出登录成功,返回到登录页面"
|
||||||
|
|
||||||
|
@ -1379,11 +1393,20 @@ msgstr "%(name)s 创建成功"
|
||||||
msgid "%(name)s was updated successfully"
|
msgid "%(name)s was updated successfully"
|
||||||
msgstr "%(name)s 更新成功"
|
msgstr "%(name)s 更新成功"
|
||||||
|
|
||||||
|
#: common/db/models.py:67
|
||||||
|
msgid "Updated by"
|
||||||
|
msgstr "更新人"
|
||||||
|
|
||||||
#: common/drf/parsers/csv.py:22
|
#: common/drf/parsers/csv.py:22
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "The max size of CSV is %d bytes"
|
msgid "The max size of CSV is %d bytes"
|
||||||
msgstr "CSV 文件最大为 %d 字节"
|
msgstr "CSV 文件最大为 %d 字节"
|
||||||
|
|
||||||
|
#: common/exceptions.py:15
|
||||||
|
#, python-format
|
||||||
|
msgid "%s object does not exist."
|
||||||
|
msgstr "%s对象不存在"
|
||||||
|
|
||||||
#: common/fields/form.py:33
|
#: common/fields/form.py:33
|
||||||
msgid "Not a valid json"
|
msgid "Not a valid json"
|
||||||
msgstr "不是合法json"
|
msgstr "不是合法json"
|
||||||
|
@ -5541,9 +5564,6 @@ msgstr "旗舰版"
|
||||||
#~ msgid "Corporation"
|
#~ msgid "Corporation"
|
||||||
#~ msgstr "公司"
|
#~ msgstr "公司"
|
||||||
|
|
||||||
#~ msgid "Expired"
|
|
||||||
#~ msgstr "过期时间"
|
|
||||||
|
|
||||||
#~ msgid "Edition"
|
#~ msgid "Edition"
|
||||||
#~ msgstr "版本"
|
#~ msgstr "版本"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue