From 90f03dda62405531f8b529d4318659fc765b0362 Mon Sep 17 00:00:00 2001 From: xinwen Date: Fri, 31 Jul 2020 18:18:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(authentication):=20=E7=B1=BB=E4=BC=BC?= =?UTF-8?q?=E8=85=BE=E8=AE=AF=E4=BC=81=E4=B8=9A=E9=82=AE=E5=8D=95=E7=82=B9?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/__init__.py | 1 + apps/authentication/api/sso.py | 77 +++++++++++ apps/authentication/backends/api.py | 10 +- apps/authentication/errors.py | 6 + apps/authentication/filters.py | 15 +++ .../migrations/0004_ssotoken.py | 32 +++++ apps/authentication/models.py | 14 +- apps/authentication/serializers.py | 9 +- apps/authentication/urls/api_urls.py | 1 + apps/common/db/models.py | 33 ++++- apps/common/drf/exc_handlers.py | 45 +++++++ apps/common/exceptions.py | 12 ++ apps/jumpserver/conf.py | 5 + apps/jumpserver/settings/auth.py | 3 + apps/jumpserver/settings/libs.py | 1 + apps/locale/zh/LC_MESSAGES/django.mo | Bin 55907 -> 56117 bytes apps/locale/zh/LC_MESSAGES/django.po | 122 ++++++++++-------- 17 files changed, 329 insertions(+), 57 deletions(-) create mode 100644 apps/authentication/api/sso.py create mode 100644 apps/authentication/filters.py create mode 100644 apps/authentication/migrations/0004_ssotoken.py create mode 100644 apps/common/drf/exc_handlers.py diff --git a/apps/authentication/api/__init__.py b/apps/authentication/api/__init__.py index 4f6475124..af5d8d1b4 100644 --- a/apps/authentication/api/__init__.py +++ b/apps/authentication/api/__init__.py @@ -6,3 +6,4 @@ from .token import * from .mfa import * from .access_key import * from .login_confirm import * +from .sso import * diff --git a/apps/authentication/api/sso.py b/apps/authentication/api/sso.py new file mode 100644 index 000000000..5740c80d8 --- /dev/null +++ b/apps/authentication/api/sso.py @@ -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')) diff --git a/apps/authentication/backends/api.py b/apps/authentication/backends/api.py index b61798695..ff62677ef 100644 --- a/apps/authentication/backends/api.py +++ b/apps/authentication/backends/api.py @@ -5,14 +5,13 @@ import uuid import time from django.core.cache import cache -from django.conf import settings from django.utils.translation import ugettext as _ from django.utils.six import text_type 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 authentication, exceptions 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 ..models import AccessKey, PrivateToken @@ -197,3 +196,10 @@ class SignatureAuthentication(signature.SignatureAuthentication): return user, secret except AccessKey.DoesNotExist: return None, None + + +class SSOAuthentication(ModelBackend): + """ + 什么也不做呀😺 + """ + pass diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index 20ec0aedf..241881ba0 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -4,6 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from django.urls import reverse from django.conf import settings +from common.exceptions import JMSException from .signals import post_auth_failed from users.utils import ( increase_login_failed_count, get_login_failed_count @@ -205,3 +206,8 @@ class LoginConfirmOtherError(LoginConfirmBaseError): def __init__(self, ticket_id, status): msg = login_confirm_error_msg.format(status) super().__init__(ticket_id=ticket_id, msg=msg) + + +class SSOAuthClosed(JMSException): + default_code = 'sso_auth_closed' + default_detail = _('SSO auth closed') diff --git a/apps/authentication/filters.py b/apps/authentication/filters.py new file mode 100644 index 000000000..30ab8c157 --- /dev/null +++ b/apps/authentication/filters.py @@ -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' + ) + ) + ] diff --git a/apps/authentication/migrations/0004_ssotoken.py b/apps/authentication/migrations/0004_ssotoken.py new file mode 100644 index 000000000..57d2f9805 --- /dev/null +++ b/apps/authentication/migrations/0004_ssotoken.py @@ -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, + }, + ), + ] diff --git a/apps/authentication/models.py b/apps/authentication/models.py index 6a60b3432..27a9d7857 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -1,10 +1,13 @@ import uuid -from django.db import models +from functools import partial + from django.utils import timezone from django.utils.translation import ugettext_lazy as _, ugettext as __ from rest_framework.authtoken.models import Token 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.utils import get_object_or_none, get_request_ip, get_ip_city @@ -76,3 +79,12 @@ class LoginConfirmSetting(CommonModelMixin): def __str__(self): 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) diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py index f000c3438..f04b847b4 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -5,12 +5,12 @@ from rest_framework import serializers from common.utils import get_object_or_none from users.models import User from users.serializers import UserProfileSerializer -from .models import AccessKey, LoginConfirmSetting +from .models import AccessKey, LoginConfirmSetting, SSOToken __all__ = [ 'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer', - 'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', + 'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer', ] @@ -76,3 +76,8 @@ class LoginConfirmSettingSerializer(serializers.ModelSerializer): model = LoginConfirmSetting fields = ['id', 'user', 'reviewers', '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) diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index da59711c4..3027fdad0 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -8,6 +8,7 @@ from .. import api app_name = 'authentication' router = DefaultRouter() router.register('access-keys', api.AccessKeyViewSet, 'access-key') +router.register('sso', api.SSOViewSet, 'sso') urlpatterns = [ diff --git a/apps/common/db/models.py b/apps/common/db/models.py index 04d501b8f..5d9827c06 100644 --- a/apps/common/db/models.py +++ b/apps/common/db/models.py @@ -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): @@ -46,3 +60,20 @@ class ChoiceSetType(type): class ChoiceSet(metaclass=ChoiceSetType): 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 diff --git a/apps/common/drf/exc_handlers.py b/apps/common/drf/exc_handlers.py new file mode 100644 index 000000000..8515c95ec --- /dev/null +++ b/apps/common/drf/exc_handlers.py @@ -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 diff --git a/apps/common/exceptions.py b/apps/common/exceptions.py index 4b3718837..ded24374a 100644 --- a/apps/common/exceptions.py +++ b/apps/common/exceptions.py @@ -1,8 +1,20 @@ # -*- coding: utf-8 -*- # +from django.utils.translation import gettext_lazy as _ from rest_framework.exceptions import APIException from rest_framework import status class JMSException(APIException): 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) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 6f521c453..3d8b6098f 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -211,6 +211,9 @@ class Config(dict): 'CAS_LOGOUT_COMPLETELY': True, 'CAS_VERSION': 3, + 'AUTH_SSO': False, + 'AUTH_SSO_AUTHKEY_TTL': 60 * 15, + 'OTP_VALID_WINDOW': 2, 'OTP_ISSUER_NAME': 'JumpServer', 'EMAIL_SUFFIX': 'jumpserver.org', @@ -440,6 +443,8 @@ class DynamicConfig: backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthCodeBackend') if self.static_config.get('AUTH_RADIUS'): backends.insert(0, 'authentication.backends.radius.RadiusBackend') + if self.static_config.get('AUTH_SSO'): + backends.insert(0, 'authentication.backends.api.SSOAuthentication') return backends def XPACK_LICENSE_IS_VALID(self): diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index ae31ba10d..92c0d82f1 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -94,6 +94,9 @@ CAS_VERSION = CONFIG.CAS_VERSION CAS_ROOT_PROXIED_AS = CONFIG.CAS_ROOT_PROXIED_AS 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 TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION diff --git a/apps/jumpserver/settings/libs.py b/apps/jumpserver/settings/libs.py index eb07e299d..9e4b56e21 100644 --- a/apps/jumpserver/settings/libs.py +++ b/apps/jumpserver/settings/libs.py @@ -40,6 +40,7 @@ REST_FRAMEWORK = { 'DATETIME_FORMAT': '%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', + 'EXCEPTION_HANDLER': 'common.drf.exc_handlers.common_exception_handler', # 'PAGE_SIZE': 100, # 'MAX_PAGE_SIZE': 5000 diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 8599cd7c5784765fb2b455e74722b25e66e3775c..e779e41643d2ea599b69b258531bf543208f7062 100644 GIT binary patch delta 16934 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 H8UOzP<>ldS diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index eb34c4400..92d93b15f 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\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" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -131,9 +131,10 @@ msgstr "参数" #: applications/models/remote_app.py:39 assets/models/asset.py:224 #: assets/models/base.py:240 assets/models/cluster.py:28 #: 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 -#: orgs/models.py:316 perms/models/base.py:54 users/models/user.py:530 -#: users/serializers/group.py:35 users/templates/users/user_detail.html:97 +#: assets/models/group.py:21 common/db/models.py:66 common/mixins/models.py:49 +#: orgs/models.py:23 orgs/models.py:316 perms/models/base.py:54 +#: 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/cloud/models.py:146 xpack/plugins/gathered_user/models.py:30 msgid "Created by" @@ -144,7 +145,7 @@ msgstr "创建者" #: applications/models/remote_app.py:42 assets/models/asset.py:225 #: assets/models/base.py:238 assets/models/cluster.py:26 #: 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 #: 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 @@ -241,7 +242,7 @@ msgstr "节点" #: assets/models/asset.py:196 assets/models/cmd_filter.py:22 #: assets/models/domain.py:55 assets/models/label.py:22 -#: authentication/models.py:45 +#: authentication/models.py:48 msgid "Is active" msgstr "激活" @@ -379,7 +380,8 @@ msgid "SSH public key" msgstr "SSH公钥" #: 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" msgstr "更新日期" @@ -534,9 +536,9 @@ msgid "Default asset group" msgstr "默认资产组" #: 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 -#: orgs/models.py:16 orgs/models.py:312 perms/forms/asset_permission.py:83 -#: perms/forms/database_app_permission.py:38 +#: audits/models.py:69 audits/serializers.py:77 authentication/models.py:46 +#: authentication/models.py:90 orgs/models.py:16 orgs/models.py:312 +#: 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 #: templates/index.html:78 terminal/backends/command/models.py:18 #: terminal/backends/command/serializers.py:12 terminal/models.py:185 @@ -1042,94 +1044,94 @@ msgstr "运行用户" msgid "Code is invalid" msgstr "Code无效" -#: authentication/backends/api.py:53 +#: authentication/backends/api.py:52 msgid "Invalid signature header. No credentials provided." msgstr "" -#: authentication/backends/api.py:56 +#: authentication/backends/api.py:55 msgid "Invalid signature header. Signature string should not contain spaces." msgstr "" -#: authentication/backends/api.py:63 +#: authentication/backends/api.py:62 msgid "Invalid signature header. Format like AccessKeyId:Signature" msgstr "" -#: authentication/backends/api.py:67 +#: authentication/backends/api.py:66 msgid "" "Invalid signature header. Signature string should not contain invalid " "characters." msgstr "" -#: authentication/backends/api.py:87 authentication/backends/api.py:103 +#: authentication/backends/api.py:86 authentication/backends/api.py:102 msgid "Invalid signature." 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" msgstr "" -#: authentication/backends/api.py:99 +#: authentication/backends/api.py:98 msgid "Expired, more than 15 minutes" msgstr "" -#: authentication/backends/api.py:106 +#: authentication/backends/api.py:105 msgid "User disabled." msgstr "用户已禁用" -#: authentication/backends/api.py:124 +#: authentication/backends/api.py:123 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication/backends/api.py:127 +#: authentication/backends/api.py:126 msgid "Invalid token header. Sign string should not contain spaces." msgstr "" -#: authentication/backends/api.py:134 +#: authentication/backends/api.py:133 msgid "" "Invalid token header. Sign string should not contain invalid characters." msgstr "" -#: authentication/backends/api.py:145 +#: authentication/backends/api.py:144 msgid "Invalid token or cache refreshed." msgstr "" -#: authentication/errors.py:22 +#: authentication/errors.py:23 msgid "Username/password check failed" msgstr "用户名/密码 校验失败" -#: authentication/errors.py:23 +#: authentication/errors.py:24 msgid "Password decrypt failed" msgstr "密码解密失败" -#: authentication/errors.py:24 +#: authentication/errors.py:25 msgid "MFA failed" msgstr "多因子认证失败" -#: authentication/errors.py:25 +#: authentication/errors.py:26 msgid "MFA unset" msgstr "多因子认证没有设定" -#: authentication/errors.py:26 +#: authentication/errors.py:27 msgid "Username does not exist" msgstr "用户名不存在" -#: authentication/errors.py:27 +#: authentication/errors.py:28 msgid "Password expired" msgstr "密码已过期" -#: authentication/errors.py:28 +#: authentication/errors.py:29 msgid "Disabled or expired" msgstr "禁用或失效" -#: authentication/errors.py:29 +#: authentication/errors.py:30 msgid "This account is inactive." msgstr "此账户已禁用" -#: authentication/errors.py:39 +#: authentication/errors.py:40 msgid "No session found, check your cookie" msgstr "会话已变更,刷新页面" -#: authentication/errors.py:41 +#: authentication/errors.py:42 #, python-brace-format msgid "" "The username or password you entered is incorrect, please enter it again. " @@ -1139,37 +1141,41 @@ msgstr "" "您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将" "被临时 锁定 {block_time} 分钟)" -#: authentication/errors.py:47 +#: authentication/errors.py:48 msgid "" "The account has been locked (please contact admin to unlock it or try again " "after {} minutes)" 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 msgid "MFA code invalid, or ntp sync server time" msgstr "MFA验证码不正确,或者服务器端时间不对" -#: authentication/errors.py:52 +#: authentication/errors.py:53 msgid "MFA required" msgstr "需要多因子认证" -#: authentication/errors.py:53 +#: authentication/errors.py:54 msgid "MFA not set, please set it first" msgstr "多因子认证没有设置,请先完成设置" -#: authentication/errors.py:54 +#: authentication/errors.py:55 msgid "Login confirm required" msgstr "需要登录复核" -#: authentication/errors.py:55 +#: authentication/errors.py:56 msgid "Wait login confirm ticket for accept" msgstr "等待登录复核处理" -#: authentication/errors.py:56 +#: authentication/errors.py:57 msgid "Login confirm ticket was {}" msgstr "登录复核 {}" +#: authentication/errors.py:213 +msgid "SSO auth closed" +msgstr "SSO 认证关闭了" + #: authentication/forms.py:26 authentication/forms.py:34 #: authentication/templates/authentication/login.html:38 #: authentication/templates/authentication/xpack_login.html:118 @@ -1177,7 +1183,7 @@ msgstr "登录复核 {}" msgid "MFA code" msgstr "多因子认证验证码" -#: authentication/models.py:19 +#: authentication/models.py:22 #: authentication/templates/authentication/_access_key_modal.html:32 #: perms/models/base.py:51 users/templates/users/_select_user_modal.html:18 #: users/templates/users/user_detail.html:132 @@ -1185,23 +1191,31 @@ msgstr "多因子认证验证码" msgid "Active" msgstr "激活中" -#: authentication/models.py:39 +#: authentication/models.py:42 msgid "Private Token" 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" 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 msgid "Login confirm" msgstr "登录复核" -#: authentication/models.py:63 +#: authentication/models.py:66 msgid "City" msgstr "城市" +#: authentication/models.py:88 +msgid "Token" +msgstr "" + +#: authentication/models.py:89 +msgid "Expired" +msgstr "过期时间" + #: authentication/templates/authentication/_access_key_modal.html:6 msgid "API key list" msgstr "API Key列表" @@ -1349,7 +1363,7 @@ msgstr "欢迎回来,请输入用户名和密码登录" msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:172 +#: authentication/views/login.py:178 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -1357,15 +1371,15 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\n" " 不要关闭本页面" -#: authentication/views/login.py:177 +#: authentication/views/login.py:183 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:209 +#: authentication/views/login.py:215 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:210 +#: authentication/views/login.py:216 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" @@ -1379,11 +1393,20 @@ msgstr "%(name)s 创建成功" msgid "%(name)s was updated successfully" msgstr "%(name)s 更新成功" +#: common/db/models.py:67 +msgid "Updated by" +msgstr "更新人" + #: common/drf/parsers/csv.py:22 #, python-format msgid "The max size of CSV is %d bytes" msgstr "CSV 文件最大为 %d 字节" +#: common/exceptions.py:15 +#, python-format +msgid "%s object does not exist." +msgstr "%s对象不存在" + #: common/fields/form.py:33 msgid "Not a valid json" msgstr "不是合法json" @@ -5541,9 +5564,6 @@ msgstr "旗舰版" #~ msgid "Corporation" #~ msgstr "公司" -#~ msgid "Expired" -#~ msgstr "过期时间" - #~ msgid "Edition" #~ msgstr "版本"