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 8599cd7c5..e779e4164 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ 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 "版本"