diff --git a/apps/audits/migrations/0023_auto_20230906_1322.py b/apps/audits/migrations/0023_auto_20230906_1322.py index 98447b7b3..286ff0df5 100644 --- a/apps/audits/migrations/0023_auto_20230906_1322.py +++ b/apps/audits/migrations/0023_auto_20230906_1322.py @@ -19,7 +19,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='operatelog', name='action', - field=models.CharField(choices=[('view', 'View'), ('update', 'Update'), ('delete', 'Delete'), ('create', 'Create'), ('download', 'Download'), ('connect', 'Connect'), ('login', 'Login'), ('change_password', 'Change password')], max_length=16, verbose_name='Action'), + field=models.CharField(choices=[('view', 'View'), ('update', 'Update'), ('delete', 'Delete'), ('create', 'Create'), ('download', 'Download'), ('connect', 'Connect'), ('login', 'Login'), ('change_password', 'Change password'), ('reject', 'Reject'), ('accept', 'Accept'), ('review', 'Review')], max_length=16, verbose_name='Action'), ), migrations.AlterField( model_name='userloginlog', diff --git a/apps/authentication/api/access_key.py b/apps/authentication/api/access_key.py index 9253f449d..86551953a 100644 --- a/apps/authentication/api/access_key.py +++ b/apps/authentication/api/access_key.py @@ -1,20 +1,44 @@ # -*- coding: utf-8 -*- # -from rest_framework.viewsets import ModelViewSet +from django.utils.translation import gettext as _ +from rest_framework import serializers +from rest_framework.response import Response +from common.api import JMSModelViewSet +from common.permissions import UserConfirmation from rbac.permissions import RBACPermission +from ..const import ConfirmType from ..serializers import AccessKeySerializer -class AccessKeyViewSet(ModelViewSet): +class AccessKeyViewSet(JMSModelViewSet): serializer_class = AccessKeySerializer - search_fields = ['^id', '^secret'] + search_fields = ['^id'] permission_classes = [RBACPermission] def get_queryset(self): return self.request.user.access_keys.all() + def get_permissions(self): + if self.is_swagger_request(): + return super().get_permissions() + + if self.action == 'create': + self.permission_classes = [ + RBACPermission, UserConfirmation.require(ConfirmType.PASSWORD) + ] + return super().get_permissions() + def perform_create(self, serializer): user = self.request.user - user.create_access_key() + if user.access_keys.count() >= 10: + raise serializers.ValidationError(_('Access keys can be created at most 10')) + key = user.create_access_key() + return key + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + key = self.perform_create(serializer) + return Response({'secret': key.secret, 'id': key.id}, status=201) diff --git a/apps/authentication/api/confirm.py b/apps/authentication/api/confirm.py index 3c0e670f0..92792d72d 100644 --- a/apps/authentication/api/confirm.py +++ b/apps/authentication/api/confirm.py @@ -13,7 +13,7 @@ from ..serializers import ConfirmSerializer class ConfirmBindORUNBindOAuth(RetrieveAPIView): - permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.ReLogin),) + permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),) def retrieve(self, request, *args, **kwargs): return Response('ok') @@ -24,7 +24,7 @@ class ConfirmApi(RetrieveAPIView, CreateAPIView): serializer_class = ConfirmSerializer def get_confirm_backend(self, confirm_type): - backend_classes = ConfirmType.get_can_confirm_backend_classes(confirm_type) + backend_classes = ConfirmType.get_prop_backends(confirm_type) if not backend_classes: return for backend_cls in backend_classes: @@ -34,7 +34,7 @@ class ConfirmApi(RetrieveAPIView, CreateAPIView): return backend def retrieve(self, request, *args, **kwargs): - confirm_type = request.query_params.get('confirm_type') + confirm_type = request.query_params.get('confirm_type', 'password') backend = self.get_confirm_backend(confirm_type) if backend is None: msg = _('This action require verify your MFA') @@ -51,7 +51,7 @@ class ConfirmApi(RetrieveAPIView, CreateAPIView): serializer.is_valid(raise_exception=True) validated_data = serializer.validated_data - confirm_type = validated_data.get('confirm_type') + confirm_type = validated_data.get('confirm_type', 'password') mfa_type = validated_data.get('mfa_type') secret_key = validated_data.get('secret_key') diff --git a/apps/authentication/api/dingtalk.py b/apps/authentication/api/dingtalk.py index c8d9f7ce0..66f5a1d8c 100644 --- a/apps/authentication/api/dingtalk.py +++ b/apps/authentication/api/dingtalk.py @@ -27,7 +27,7 @@ class DingTalkQRUnBindBase(APIView): class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase): - permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.ReLogin),) + permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),) class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase): diff --git a/apps/authentication/api/feishu.py b/apps/authentication/api/feishu.py index 148b99e51..336552087 100644 --- a/apps/authentication/api/feishu.py +++ b/apps/authentication/api/feishu.py @@ -27,7 +27,7 @@ class FeiShuQRUnBindBase(APIView): class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase): - permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.ReLogin),) + permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),) class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase): diff --git a/apps/authentication/api/wecom.py b/apps/authentication/api/wecom.py index e704d3d4b..82476157b 100644 --- a/apps/authentication/api/wecom.py +++ b/apps/authentication/api/wecom.py @@ -27,7 +27,7 @@ class WeComQRUnBindBase(APIView): class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase): - permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.ReLogin),) + permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),) class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase): diff --git a/apps/authentication/backends/drf.py b/apps/authentication/backends/drf.py index 8d7d60e99..4ba879cc2 100644 --- a/apps/authentication/backends/drf.py +++ b/apps/authentication/backends/drf.py @@ -1,119 +1,33 @@ # -*- coding: utf-8 -*- # -import time -import uuid - from django.contrib.auth import get_user_model from django.core.cache import cache +from django.utils import timezone from django.utils.translation import gettext as _ -from rest_framework import HTTP_HEADER_ENCODING from rest_framework import authentication, exceptions -from six import text_type from common.auth import signature -from common.utils import get_object_or_none, make_signature, http_to_unixtime -from .base import JMSBaseAuthBackend +from common.utils import get_object_or_none from ..models import AccessKey, PrivateToken -UserModel = get_user_model() + +def date_more_than(d, seconds): + return d is None or (timezone.now() - d).seconds > seconds -def get_request_date_header(request): - date = request.META.get('HTTP_DATE', b'') - if isinstance(date, text_type): - # Work around django test client oddness - date = date.encode(HTTP_HEADER_ENCODING) - return date +def after_authenticate_update_date(user, token=None): + if date_more_than(user.date_api_key_last_used, 60): + user.date_api_key_last_used = timezone.now() + user.save(update_fields=['date_api_key_last_used']) - -class AccessKeyAuthentication(authentication.BaseAuthentication): - """App使用Access key进行签名认证, 目前签名算法比较简单, - app注册或者手动建立后,会生成 access_key_id 和 access_key_secret, - 然后使用 如下算法生成签名: - Signature = md5(access_key_secret + '\n' + Date) - example: Signature = md5('d32d2b8b-9a10-4b8d-85bb-1a66976f6fdc' + '\n' + - 'Thu, 12 Jan 2017 08:19:41 GMT') - 请求时设置请求header - header['Authorization'] = 'Sign access_key_id:Signature' 如: - header['Authorization'] = - 'Sign d32d2b8b-9a10-4b8d-85bb-1a66976f6fdc:OKOlmdxgYPZ9+SddnUUDbQ==' - - 验证时根据相同算法进行验证, 取到access_key_id对应的access_key_id, 从request - headers取到Date, 然后进行md5, 判断得到的结果是否相同, 如果是认证通过, 否则 认证 - 失败 - """ - keyword = 'Sign' - - def authenticate(self, request): - auth = authentication.get_authorization_header(request).split() - if not auth or auth[0].lower() != self.keyword.lower().encode(): - return None - - if len(auth) == 1: - msg = _('Invalid signature header. No credentials provided.') - raise exceptions.AuthenticationFailed(msg) - elif len(auth) > 2: - msg = _('Invalid signature header. Signature ' - 'string should not contain spaces.') - raise exceptions.AuthenticationFailed(msg) - - try: - sign = auth[1].decode().split(':') - if len(sign) != 2: - msg = _('Invalid signature header. ' - 'Format like AccessKeyId:Signature') - raise exceptions.AuthenticationFailed(msg) - except UnicodeError: - msg = _('Invalid signature header. ' - 'Signature string should not contain invalid characters.') - raise exceptions.AuthenticationFailed(msg) - - access_key_id = sign[0] - try: - uuid.UUID(access_key_id) - except ValueError: - raise exceptions.AuthenticationFailed('Access key id invalid') - request_signature = sign[1] - - return self.authenticate_credentials( - request, access_key_id, request_signature - ) - - @staticmethod - def authenticate_credentials(request, access_key_id, request_signature): - access_key = get_object_or_none(AccessKey, id=access_key_id) - request_date = get_request_date_header(request) - if access_key is None or not access_key.user: - raise exceptions.AuthenticationFailed(_('Invalid signature.')) - access_key_secret = access_key.secret - - try: - request_unix_time = http_to_unixtime(request_date) - except ValueError: - raise exceptions.AuthenticationFailed( - _('HTTP header: Date not provide ' - 'or not %a, %d %b %Y %H:%M:%S GMT')) - - if int(time.time()) - request_unix_time > 15 * 60: - raise exceptions.AuthenticationFailed( - _('Expired, more than 15 minutes')) - - signature = make_signature(access_key_secret, request_date) - if not signature == request_signature: - raise exceptions.AuthenticationFailed(_('Invalid signature.')) - - if not access_key.user.is_active: - raise exceptions.AuthenticationFailed(_('User disabled.')) - return access_key.user, None - - def authenticate_header(self, request): - return 'Sign access_key_id:Signature' + if token and hasattr(token, 'date_last_used') and date_more_than(token.date_last_used, 60): + token.date_last_used = timezone.now() + token.save(update_fields=['date_last_used']) class AccessTokenAuthentication(authentication.BaseAuthentication): keyword = 'Bearer' - # expiration = settings.TOKEN_EXPIRATION or 3600 model = get_user_model() def authenticate(self, request): @@ -125,19 +39,20 @@ class AccessTokenAuthentication(authentication.BaseAuthentication): msg = _('Invalid token header. No credentials provided.') raise exceptions.AuthenticationFailed(msg) elif len(auth) > 2: - msg = _('Invalid token header. Sign string ' - 'should not contain spaces.') + msg = _('Invalid token header. Sign string should not contain spaces.') raise exceptions.AuthenticationFailed(msg) try: token = auth[1].decode() except UnicodeError: - msg = _('Invalid token header. Sign string ' - 'should not contain invalid characters.') + msg = _('Invalid token header. Sign string should not contain invalid characters.') raise exceptions.AuthenticationFailed(msg) - return self.authenticate_credentials(token) + user, header = self.authenticate_credentials(token) + after_authenticate_update_date(user) + return user, header - def authenticate_credentials(self, token): + @staticmethod + def authenticate_credentials(token): model = get_user_model() user_id = cache.get(token) user = get_object_or_none(model, id=user_id) @@ -151,15 +66,23 @@ class AccessTokenAuthentication(authentication.BaseAuthentication): return self.keyword -class PrivateTokenAuthentication(JMSBaseAuthBackend, authentication.TokenAuthentication): +class PrivateTokenAuthentication(authentication.TokenAuthentication): model = PrivateToken + def authenticate(self, request): + user_token = super().authenticate(request) + if not user_token: + return + user, token = user_token + after_authenticate_update_date(user, token) + return user, token + class SessionAuthentication(authentication.SessionAuthentication): def authenticate(self, request): """ Returns a `User` if the request session currently has a logged in user. - Otherwise returns `None`. + Otherwise, returns `None`. """ # Get the session-based user from the underlying HttpRequest object @@ -195,6 +118,7 @@ class SignatureAuthentication(signature.SignatureAuthentication): if not key.is_active: return None, None user, secret = key.user, str(key.secret) + after_authenticate_update_date(user, key) return user, secret except (AccessKey.DoesNotExist, exceptions.ValidationError): return None, None diff --git a/apps/authentication/confirm/base.py b/apps/authentication/confirm/base.py index 63258abce..5926da3e1 100644 --- a/apps/authentication/confirm/base.py +++ b/apps/authentication/confirm/base.py @@ -2,7 +2,6 @@ import abc class BaseConfirm(abc.ABC): - def __init__(self, user, request): self.user = user self.request = request @@ -23,7 +22,7 @@ class BaseConfirm(abc.ABC): @property def content(self): - return '' + return [] @abc.abstractmethod def authenticate(self, secret_key, mfa_type) -> tuple: diff --git a/apps/authentication/confirm/password.py b/apps/authentication/confirm/password.py index dc49576a3..e497bc261 100644 --- a/apps/authentication/confirm/password.py +++ b/apps/authentication/confirm/password.py @@ -15,3 +15,14 @@ class ConfirmPassword(BaseConfirm): ok = authenticate(self.request, username=self.user.username, password=secret_key) msg = '' if ok else _('Authentication failed password incorrect') return ok, msg + + @property + def content(self): + return [ + { + 'name': 'password', + 'display_name': _('Password'), + 'disabled': False, + 'placeholder': _('Password'), + } + ] diff --git a/apps/authentication/const.py b/apps/authentication/const.py index d7e0690db..1e06a4d35 100644 --- a/apps/authentication/const.py +++ b/apps/authentication/const.py @@ -11,7 +11,7 @@ CONFIRM_BACKEND_MAP = {backend.name: backend for backend in CONFIRM_BACKENDS} class ConfirmType(TextChoices): - ReLogin = ConfirmReLogin.name, ConfirmReLogin.display_name + RELOGIN = ConfirmReLogin.name, ConfirmReLogin.display_name PASSWORD = ConfirmPassword.name, ConfirmPassword.display_name MFA = ConfirmMFA.name, ConfirmMFA.display_name @@ -23,10 +23,11 @@ class ConfirmType(TextChoices): return types @classmethod - def get_can_confirm_backend_classes(cls, confirm_type): + def get_prop_backends(cls, confirm_type): types = cls.get_can_confirm_types(confirm_type) backend_classes = [ - CONFIRM_BACKEND_MAP[tp] for tp in types if tp in CONFIRM_BACKEND_MAP + CONFIRM_BACKEND_MAP[tp] + for tp in types if tp in CONFIRM_BACKEND_MAP ] return backend_classes diff --git a/apps/authentication/migrations/0023_auto_20231010_1101.py b/apps/authentication/migrations/0023_auto_20231010_1101.py new file mode 100644 index 000000000..81920dfd7 --- /dev/null +++ b/apps/authentication/migrations/0023_auto_20231010_1101.py @@ -0,0 +1,57 @@ +# Generated by Django 4.1.10 on 2023-10-10 02:47 + +import uuid +import authentication.models.access_key +from django.db import migrations, models + + +def migrate_access_key_secret(apps, schema_editor): + access_key_model = apps.get_model('authentication', 'AccessKey') + db_alias = schema_editor.connection.alias + + batch_size = 100 + count = 0 + + while True: + access_keys = access_key_model.objects.using(db_alias).all()[count:count + batch_size] + if not access_keys: + break + + count += len(access_keys) + access_keys_updated = [] + for access_key in access_keys: + s = access_key.secret + if len(s) != 32 or not s.islower(): + continue + try: + access_key.secret = '%s-%s-%s-%s-%s' % (s[:8], s[8:12], s[12:16], s[16:20], s[20:]) + access_keys_updated.append(access_key) + except (ValueError, IndexError): + pass + access_key_model.objects.bulk_update(access_keys_updated, fields=['secret']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0022_passkey'), + ] + + operations = [ + migrations.AddField( + model_name='accesskey', + name='date_last_used', + field=models.DateTimeField(blank=True, null=True, verbose_name='Date last used'), + ), + migrations.AddField( + model_name='privatetoken', + name='date_last_used', + field=models.DateTimeField(blank=True, null=True, verbose_name='Date last used'), + ), + migrations.AlterField( + model_name='accesskey', + name='secret', + field=models.CharField(default=authentication.models.access_key.default_secret, max_length=36, verbose_name='AccessKeySecret'), + ), + migrations.RunPython(migrate_access_key_secret), + ] diff --git a/apps/authentication/models/access_key.py b/apps/authentication/models/access_key.py index 77fa67c74..5d9571569 100644 --- a/apps/authentication/models/access_key.py +++ b/apps/authentication/models/access_key.py @@ -5,16 +5,20 @@ from django.db import models from django.utils.translation import gettext_lazy as _ import common.db.models +from common.utils.random import random_string + + +def default_secret(): + return random_string(36) class AccessKey(models.Model): - id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True, - default=uuid.uuid4, editable=False) - secret = models.UUIDField(verbose_name='AccessKeySecret', - default=uuid.uuid4, editable=False) + id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True, default=uuid.uuid4, editable=False) + secret = models.CharField(verbose_name='AccessKeySecret', default=default_secret, max_length=36) user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='User', on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='access_keys') is_active = models.BooleanField(default=True, verbose_name=_('Active')) + date_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last used')) date_created = models.DateTimeField(auto_now_add=True) def get_id(self): diff --git a/apps/authentication/models/private_token.py b/apps/authentication/models/private_token.py index bb5f1da87..56669f018 100644 --- a/apps/authentication/models/private_token.py +++ b/apps/authentication/models/private_token.py @@ -1,9 +1,11 @@ +from django.db import models from django.utils.translation import gettext_lazy as _ from rest_framework.authtoken.models import Token class PrivateToken(Token): """Inherit from auth token, otherwise migration is boring""" + date_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last used')) class Meta: verbose_name = _('Private Token') diff --git a/apps/authentication/serializers/token.py b/apps/authentication/serializers/token.py index d1e87c0c0..9101bc9c0 100644 --- a/apps/authentication/serializers/token.py +++ b/apps/authentication/serializers/token.py @@ -10,7 +10,7 @@ from users.serializers import UserProfileSerializer from ..models import AccessKey, TempToken __all__ = [ - 'AccessKeySerializer', 'BearerTokenSerializer', + 'AccessKeySerializer', 'BearerTokenSerializer', 'SSOTokenSerializer', 'TempTokenSerializer', ] @@ -18,8 +18,8 @@ __all__ = [ class AccessKeySerializer(serializers.ModelSerializer): class Meta: model = AccessKey - fields = ['id', 'secret', 'is_active', 'date_created'] - read_only_fields = ['id', 'secret', 'date_created'] + fields = ['id', 'is_active', 'date_created', 'date_last_used'] + read_only_fields = ['id', 'date_created', 'date_last_used'] class BearerTokenSerializer(serializers.Serializer): @@ -37,7 +37,8 @@ class BearerTokenSerializer(serializers.Serializer): def get_keyword(obj): return 'Bearer' - def update_last_login(self, user): + @staticmethod + def update_last_login(user): user.last_login = timezone.now() user.save(update_fields=['last_login']) @@ -96,7 +97,7 @@ class TempTokenSerializer(serializers.ModelSerializer): username = request.user.username kwargs = { 'username': username, 'secret': secret, - 'date_expired': timezone.now() + timezone.timedelta(seconds=5*60), + 'date_expired': timezone.now() + timezone.timedelta(seconds=5 * 60), } token = TempToken(**kwargs) token.save() diff --git a/apps/authentication/tests/__init__.py b/apps/authentication/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/authentication/tests/access_key.py b/apps/authentication/tests/access_key.py new file mode 100644 index 000000000..d0ec2842c --- /dev/null +++ b/apps/authentication/tests/access_key.py @@ -0,0 +1,34 @@ +# Python 示例 +# pip install requests drf-httpsig +import datetime +import json + +import requests +from httpsig.requests_auth import HTTPSignatureAuth + + +def get_auth(KeyID, SecretID): + signature_headers = ['(request-target)', 'accept', 'date'] + auth = HTTPSignatureAuth(key_id=KeyID, secret=SecretID, algorithm='hmac-sha256', headers=signature_headers) + return auth + + +def get_user_info(jms_url, auth): + url = jms_url + '/api/v1/users/users/?limit=1' + gmt_form = '%a, %d %b %Y %H:%M:%S GMT' + headers = { + 'Accept': 'application/json', + 'X-JMS-ORG': '00000000-0000-0000-0000-000000000002', + 'Date': datetime.datetime.utcnow().strftime(gmt_form) + } + + response = requests.get(url, auth=auth, headers=headers) + print(json.loads(response.text)) + + +if __name__ == '__main__': + jms_url = 'http://localhost:8080' + KeyID = '0753098d-810c-45fb-b42c-b27077147933' + SecretID = 'a58d2530-d7ee-4390-a204-3492e44dde84' + auth = get_auth(KeyID, SecretID) + get_user_info(jms_url, auth) diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py index 819f1d055..c23827ca1 100644 --- a/apps/authentication/views/dingtalk.py +++ b/apps/authentication/views/dingtalk.py @@ -99,7 +99,7 @@ class DingTalkOAuthMixin(DingTalkBaseMixin, View): class DingTalkQRBindView(DingTalkQRMixin, View): - permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.ReLogin)) + permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.RELOGIN)) def get(self, request: HttpRequest): user = request.user diff --git a/apps/authentication/views/feishu.py b/apps/authentication/views/feishu.py index 0f62ef75d..ec483cdd4 100644 --- a/apps/authentication/views/feishu.py +++ b/apps/authentication/views/feishu.py @@ -69,7 +69,7 @@ class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMe class FeiShuQRBindView(FeiShuQRMixin, View): - permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.ReLogin)) + permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.RELOGIN)) def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') diff --git a/apps/authentication/views/wecom.py b/apps/authentication/views/wecom.py index 238d0b3e6..7bb56202c 100644 --- a/apps/authentication/views/wecom.py +++ b/apps/authentication/views/wecom.py @@ -100,7 +100,7 @@ class WeComOAuthMixin(WeComBaseMixin, View): class WeComQRBindView(WeComQRMixin, View): - permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.ReLogin)) + permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.RELOGIN)) def get(self, request: HttpRequest): user = request.user diff --git a/apps/common/api/mixin.py b/apps/common/api/mixin.py index 4795673d7..d93459104 100644 --- a/apps/common/api/mixin.py +++ b/apps/common/api/mixin.py @@ -13,7 +13,7 @@ from common.drf.filters import ( IDSpmFilterBackend, CustomFilterBackend, IDInFilterBackend, IDNotFilterBackend, NotOrRelFilterBackend ) -from common.utils import get_logger +from common.utils import get_logger, lazyproperty from .action import RenderToJsonMixin from .serializer import SerializerMixin @@ -150,9 +150,9 @@ class OrderingFielderFieldsMixin: ordering_fields = None extra_ordering_fields = [] - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.ordering_fields = self._get_ordering_fields() + @lazyproperty + def ordering_fields(self): + return self._get_ordering_fields() def _get_ordering_fields(self): if isinstance(self.__class__.ordering_fields, (list, tuple)): @@ -179,7 +179,10 @@ class OrderingFielderFieldsMixin: model = self.queryset.model else: queryset = self.get_queryset() - model = queryset.model + if isinstance(queryset, list): + model = None + else: + model = queryset.model if not model: return [] @@ -201,4 +204,6 @@ class CommonApiMixin( SerializerMixin, ExtraFilterFieldsMixin, OrderingFielderFieldsMixin, QuerySetMixin, RenderToJsonMixin, PaginatedResponseMixin ): - pass + def is_swagger_request(self): + return getattr(self, 'swagger_fake_view', False) or \ + getattr(self, 'raw_action', '') == 'metadata' diff --git a/apps/common/permissions.py b/apps/common/permissions.py index 37c51de47..742759e1d 100644 --- a/apps/common/permissions.py +++ b/apps/common/permissions.py @@ -59,7 +59,7 @@ class WithBootstrapToken(permissions.BasePermission): class UserConfirmation(permissions.BasePermission): ttl = 60 * 5 min_level = 1 - confirm_type = ConfirmType.ReLogin + confirm_type = ConfirmType.RELOGIN def has_permission(self, request, view): if not settings.SECURITY_VIEW_AUTH_NEED_MFA: @@ -82,7 +82,7 @@ class UserConfirmation(permissions.BasePermission): return ttl @classmethod - def require(cls, confirm_type=ConfirmType.ReLogin, ttl=60 * 5): + def require(cls, confirm_type=ConfirmType.RELOGIN, ttl=60 * 5): min_level = ConfirmType.values.index(confirm_type) + 1 name = 'UserConfirmationLevel{}TTL{}'.format(min_level, ttl) return type(name, (cls,), {'min_level': min_level, 'ttl': ttl, 'confirm_type': confirm_type}) diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index c0640b1af..c1648acb8 100644 --- a/apps/locale/ja/LC_MESSAGES/django.mo +++ b/apps/locale/ja/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56db214d34b91e9f6d712cef124890264144b35b99e50d6833b4af0e935778b0 -size 162655 +oid sha256:38bd8a6653f3f4dc63552b1c86379f82067f9f9daac227bacb3af3f9f62134f9 +size 161704 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index 99759dcae..bcd757537 100644 --- a/apps/locale/ja/LC_MESSAGES/django.po +++ b/apps/locale/ja/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-10-09 18:32+0800\n" +"POT-Creation-Date: 2023-10-10 11:13+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -25,10 +25,11 @@ msgstr "パラメータ 'action' は [{}] でなければなりません。" #: accounts/const/account.py:6 #: accounts/serializers/automations/change_secret.py:32 #: assets/models/_user.py:24 audits/signal_handlers/login_log.py:37 -#: authentication/confirm/password.py:9 authentication/forms.py:32 +#: authentication/confirm/password.py:9 authentication/confirm/password.py:24 +#: authentication/confirm/password.py:26 authentication/forms.py:32 #: authentication/templates/authentication/login.html:324 #: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 -#: users/forms/profile.py:22 users/serializers/user.py:105 +#: users/forms/profile.py:22 users/serializers/user.py:104 #: users/templates/users/_msg_user_created.html:13 #: users/templates/users/user_password_verify.html:18 #: xpack/plugins/cloud/serializers/account_attrs.py:28 @@ -40,7 +41,7 @@ msgstr "パスワード" msgid "SSH key" msgstr "SSH キー" -#: accounts/const/account.py:8 authentication/models/access_key.py:33 +#: accounts/const/account.py:8 authentication/models/access_key.py:37 msgid "Access key" msgstr "アクセスキー" @@ -251,7 +252,7 @@ msgid "Version" msgstr "バージョン" #: accounts/models/account.py:56 accounts/serializers/account/account.py:211 -#: users/models/user.py:840 +#: users/models/user.py:837 msgid "Source" msgstr "ソース" @@ -552,7 +553,7 @@ msgstr "特権アカウント" #: assets/models/label.py:22 #: authentication/serializers/connect_token_secret.py:114 #: terminal/models/applet/applet.py:40 -#: terminal/models/component/endpoint.py:105 users/serializers/user.py:170 +#: terminal/models/component/endpoint.py:105 users/serializers/user.py:167 msgid "Is active" msgstr "アクティブです。" @@ -711,7 +712,7 @@ msgstr "編集済み" #: acls/templates/acls/asset_login_reminder.html:6 #: assets/models/automations/base.py:19 #: assets/serializers/automations/base.py:20 -#: authentication/api/connection_token.py:381 ops/models/base.py:17 +#: authentication/api/connection_token.py:386 ops/models/base.py:17 #: ops/models/job.py:139 ops/serializers/job.py:21 #: terminal/templates/terminal/_msg_command_execute_alert.html:16 msgid "Assets" @@ -751,8 +752,8 @@ msgstr "ID" #: terminal/notifications.py:205 terminal/serializers/command.py:16 #: terminal/templates/terminal/_msg_command_warning.html:6 #: terminal/templates/terminal/_msg_session_sharing.html:6 -#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:990 -#: users/models/user.py:1026 users/serializers/group.py:18 +#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:987 +#: users/models/user.py:1023 users/serializers/group.py:18 msgid "User" msgstr "ユーザー" @@ -1006,7 +1007,7 @@ msgstr "1-100、低い値は最初に一致します" msgid "Reviewers" msgstr "レビュー担当者" -#: acls/models/base.py:43 authentication/models/access_key.py:17 +#: acls/models/base.py:43 authentication/models/access_key.py:20 #: authentication/models/connection_token.py:53 #: authentication/templates/authentication/_access_key_modal.html:32 #: perms/models/asset_permission.py:76 terminal/models/session/sharing.py:29 @@ -1312,7 +1313,7 @@ msgid "Disabled" msgstr "無効" #: assets/const/base.py:34 settings/serializers/basic.py:6 -#: users/serializers/preference/koko.py:15 +#: users/serializers/preference/koko.py:19 #: users/serializers/preference/lina.py:39 #: users/serializers/preference/luna.py:60 msgid "Basic" @@ -1527,12 +1528,12 @@ msgstr "SSHパブリックキー" #: assets/models/_user.py:28 assets/models/automations/base.py:114 #: assets/models/cmd_filter.py:41 assets/models/group.py:19 #: audits/models.py:261 common/db/models.py:34 ops/models/base.py:54 -#: ops/models/job.py:227 users/models/user.py:1027 +#: ops/models/job.py:227 users/models/user.py:1024 msgid "Date created" msgstr "作成された日付" #: assets/models/_user.py:29 assets/models/cmd_filter.py:42 -#: common/db/models.py:35 users/models/user.py:849 +#: common/db/models.py:35 users/models/user.py:846 msgid "Date updated" msgstr "更新日" @@ -1782,7 +1783,7 @@ msgstr "デフォルト" msgid "Default asset group" msgstr "デフォルトアセットグループ" -#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:1012 +#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:1009 msgid "System" msgstr "システム" @@ -2493,7 +2494,7 @@ msgstr "認証トークン" #: audits/signal_handlers/login_log.py:40 authentication/notifications.py:73 #: authentication/views/login.py:77 authentication/views/wecom.py:159 #: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 -#: users/models/user.py:745 users/models/user.py:850 +#: users/models/user.py:745 users/models/user.py:847 msgid "WeCom" msgstr "企業微信" @@ -2501,14 +2502,14 @@ msgstr "企業微信" #: authentication/views/login.py:89 notifications/backends/__init__.py:14 #: settings/serializers/auth/feishu.py:10 #: settings/serializers/auth/feishu.py:13 users/models/user.py:747 -#: users/models/user.py:852 +#: users/models/user.py:849 msgid "FeiShu" msgstr "本を飛ばす" #: audits/signal_handlers/login_log.py:42 authentication/views/dingtalk.py:159 #: authentication/views/login.py:83 notifications/backends/__init__.py:12 #: settings/serializers/auth/dingtalk.py:10 users/models/user.py:746 -#: users/models/user.py:851 +#: users/models/user.py:848 msgid "DingTalk" msgstr "DingTalk" @@ -2540,23 +2541,23 @@ msgstr "" "再使用可能な接続トークンの使用は許可されていません。グローバル設定は有効に" "なっていません" -#: authentication/api/connection_token.py:352 +#: authentication/api/connection_token.py:357 msgid "Anonymous account is not supported for this asset" msgstr "匿名アカウントはこのプロパティではサポートされていません" -#: authentication/api/connection_token.py:371 +#: authentication/api/connection_token.py:376 msgid "Account not found" msgstr "アカウントが見つかりません" -#: authentication/api/connection_token.py:374 +#: authentication/api/connection_token.py:379 msgid "Permission expired" msgstr "承認の有効期限が切れています" -#: authentication/api/connection_token.py:401 +#: authentication/api/connection_token.py:406 msgid "ACL action is reject: {}({})" msgstr "ACL アクションは拒否です: {}({})" -#: authentication/api/connection_token.py:405 +#: authentication/api/connection_token.py:410 msgid "ACL action is review" msgstr "ACL アクションはレビューです" @@ -2599,56 +2600,21 @@ msgstr "認証" msgid "User invalid, disabled or expired" msgstr "ユーザーが無効、無効、または期限切れです" -#: authentication/backends/drf.py:54 -msgid "Invalid signature header. No credentials provided." -msgstr "署名ヘッダーが無効です。資格情報は提供されていません。" - -#: authentication/backends/drf.py:57 -msgid "Invalid signature header. Signature string should not contain spaces." -msgstr "署名ヘッダーが無効です。署名文字列にはスペースを含まないでください。" - -#: authentication/backends/drf.py:64 -msgid "Invalid signature header. Format like AccessKeyId:Signature" -msgstr "署名ヘッダーが無効です。AccessKeyIdのような形式: Signature" - -#: authentication/backends/drf.py:68 -msgid "" -"Invalid signature header. Signature string should not contain invalid " -"characters." -msgstr "" -"署名ヘッダーが無効です。署名文字列に無効な文字を含めることはできません。" - -#: authentication/backends/drf.py:88 authentication/backends/drf.py:104 -msgid "Invalid signature." -msgstr "署名が無効です。" - -#: authentication/backends/drf.py:95 -msgid "HTTP header: Date not provide or not %a, %d %b %Y %H:%M:%S GMT" -msgstr "HTTP header: Date not provide or not" - -#: authentication/backends/drf.py:100 -msgid "Expired, more than 15 minutes" -msgstr "期限切れ、15分以上" - -#: authentication/backends/drf.py:107 -msgid "User disabled." -msgstr "ユーザーが無効になりました。" - -#: authentication/backends/drf.py:125 +#: authentication/backends/drf.py:39 msgid "Invalid token header. No credentials provided." msgstr "無効なトークンヘッダー。資格情報は提供されていません。" -#: authentication/backends/drf.py:128 +#: authentication/backends/drf.py:42 msgid "Invalid token header. Sign string should not contain spaces." msgstr "無効なトークンヘッダー。記号文字列にはスペースを含めないでください。" -#: authentication/backends/drf.py:135 +#: authentication/backends/drf.py:48 msgid "" "Invalid token header. Sign string should not contain invalid characters." msgstr "" "無効なトークンヘッダー。署名文字列に無効な文字を含めることはできません。" -#: authentication/backends/drf.py:146 +#: authentication/backends/drf.py:61 msgid "Invalid token or cache refreshed." msgstr "無効なトークンまたはキャッシュの更新。" @@ -2665,6 +2631,8 @@ msgid "Added on" msgstr "に追加" #: authentication/backends/passkey/models.py:14 +#: authentication/models/access_key.py:21 +#: authentication/models/private_token.py:8 msgid "Date last used" msgstr "最後に使用した日付" @@ -3041,7 +3009,7 @@ msgstr "スーパー接続トークンのシークレットを表示できます msgid "Super connection token" msgstr "スーパー接続トークン" -#: authentication/models/private_token.py:9 +#: authentication/models/private_token.py:11 msgid "Private Token" msgstr "プライベートトークン" @@ -3099,7 +3067,7 @@ msgstr "アクション" #: authentication/serializers/connection_token.py:42 #: perms/serializers/permission.py:38 perms/serializers/permission.py:57 -#: users/serializers/user.py:97 users/serializers/user.py:174 +#: users/serializers/user.py:97 users/serializers/user.py:171 msgid "Is expired" msgstr "期限切れです" @@ -3118,9 +3086,9 @@ msgstr "メール" msgid "The {} cannot be empty" msgstr "{} 空にしてはならない" -#: authentication/serializers/token.py:79 perms/serializers/permission.py:37 +#: authentication/serializers/token.py:80 perms/serializers/permission.py:37 #: perms/serializers/permission.py:58 users/serializers/user.py:98 -#: users/serializers/user.py:171 +#: users/serializers/user.py:168 msgid "Is valid" msgstr "有効です" @@ -6260,7 +6228,7 @@ msgstr "一括作成非サポート" msgid "Storage is invalid" msgstr "ストレージが無効です" -#: terminal/models/applet/applet.py:30 xpack/plugins/license/models.py:86 +#: terminal/models/applet/applet.py:30 xpack/plugins/license/models.py:88 msgid "Community edition" msgstr "コミュニティ版" @@ -7394,7 +7362,7 @@ msgstr "ユーザー設定" msgid "Force enable" msgstr "強制有効" -#: users/models/user.py:804 users/serializers/user.py:172 +#: users/models/user.py:804 users/serializers/user.py:169 msgid "Is service account" msgstr "サービスアカウントです" @@ -7406,7 +7374,7 @@ msgstr "アバター" msgid "Wechat" msgstr "微信" -#: users/models/user.py:812 users/serializers/user.py:109 +#: users/models/user.py:812 users/serializers/user.py:106 msgid "Phone" msgstr "電話" @@ -7420,43 +7388,47 @@ msgid "Private key" msgstr "ssh秘密鍵" #: users/models/user.py:830 users/serializers/profile.py:125 -#: users/serializers/user.py:169 +#: users/serializers/user.py:166 msgid "Is first login" msgstr "最初のログインです" -#: users/models/user.py:844 +#: users/models/user.py:840 msgid "Date password last updated" msgstr "最終更新日パスワード" -#: users/models/user.py:847 +#: users/models/user.py:843 msgid "Need update password" msgstr "更新パスワードが必要" -#: users/models/user.py:971 +#: users/models/user.py:845 +msgid "Date api key used" +msgstr "Api key 最後に使用した日付" + +#: users/models/user.py:968 msgid "Can not delete admin user" msgstr "管理者ユーザーを削除できませんでした" -#: users/models/user.py:997 +#: users/models/user.py:994 msgid "Can invite user" msgstr "ユーザーを招待できます" -#: users/models/user.py:998 +#: users/models/user.py:995 msgid "Can remove user" msgstr "ユーザーを削除できます" -#: users/models/user.py:999 +#: users/models/user.py:996 msgid "Can match user" msgstr "ユーザーに一致できます" -#: users/models/user.py:1008 +#: users/models/user.py:1005 msgid "Administrator" msgstr "管理者" -#: users/models/user.py:1011 +#: users/models/user.py:1008 msgid "Administrator is the super user of system" msgstr "管理者はシステムのスーパーユーザーです" -#: users/models/user.py:1036 +#: users/models/user.py:1033 msgid "User password history" msgstr "ユーザーパスワード履歴" @@ -7495,6 +7467,12 @@ msgstr "MFAのリセット" msgid "File name conflict resolution" msgstr "ファイル名競合ソリューション" +#: users/serializers/preference/koko.py:14 +#, fuzzy +#| msgid "Terminal setting" +msgid "Terminal theme name" +msgstr "ターミナル設定" + #: users/serializers/preference/lina.py:13 msgid "New file encryption password" msgstr "新しいファイルの暗号化パスワード" @@ -7583,7 +7561,7 @@ msgstr "MFAフォース有効化" msgid "Login blocked" msgstr "ログインがロックされました" -#: users/serializers/user.py:99 users/serializers/user.py:178 +#: users/serializers/user.py:99 users/serializers/user.py:175 msgid "Is OTP bound" msgstr "仮想MFAがバインドされているか" @@ -7591,27 +7569,27 @@ msgstr "仮想MFAがバインドされているか" msgid "Can public key authentication" msgstr "公開鍵認証が可能" -#: users/serializers/user.py:173 +#: users/serializers/user.py:170 msgid "Is org admin" msgstr "組織管理者です" -#: users/serializers/user.py:175 +#: users/serializers/user.py:172 msgid "Avatar url" msgstr "アバターURL" -#: users/serializers/user.py:179 +#: users/serializers/user.py:176 msgid "MFA level" msgstr "MFA レベル" -#: users/serializers/user.py:285 +#: users/serializers/user.py:282 msgid "Select users" msgstr "ユーザーの選択" -#: users/serializers/user.py:286 +#: users/serializers/user.py:283 msgid "For security, only list several users" msgstr "セキュリティのために、複数のユーザーのみをリストします" -#: users/serializers/user.py:319 +#: users/serializers/user.py:316 msgid "name not unique" msgstr "名前が一意ではない" @@ -8536,7 +8514,7 @@ msgstr "ライセンスのインポートに成功" msgid "License is invalid" msgstr "ライセンスが無効です" -#: xpack/plugins/license/meta.py:10 xpack/plugins/license/models.py:138 +#: xpack/plugins/license/meta.py:10 xpack/plugins/license/models.py:140 msgid "License" msgstr "ライセンス" @@ -8556,6 +8534,35 @@ msgstr "エンタープライズプロフェッショナル版" msgid "Ultimate edition" msgstr "エンタープライズ・フラッグシップ・エディション" +#~ msgid "Invalid signature header. No credentials provided." +#~ msgstr "署名ヘッダーが無効です。資格情報は提供されていません。" + +#~ msgid "" +#~ "Invalid signature header. Signature string should not contain spaces." +#~ msgstr "" +#~ "署名ヘッダーが無効です。署名文字列にはスペースを含まないでください。" + +#~ msgid "Invalid signature header. Format like AccessKeyId:Signature" +#~ msgstr "署名ヘッダーが無効です。AccessKeyIdのような形式: Signature" + +#~ msgid "" +#~ "Invalid signature header. Signature string should not contain invalid " +#~ "characters." +#~ msgstr "" +#~ "署名ヘッダーが無効です。署名文字列に無効な文字を含めることはできません。" + +#~ msgid "Invalid signature." +#~ msgstr "署名が無効です。" + +#~ msgid "HTTP header: Date not provide or not %a, %d %b %Y %H:%M:%S GMT" +#~ msgstr "HTTP header: Date not provide or not" + +#~ msgid "Expired, more than 15 minutes" +#~ msgstr "期限切れ、15分以上" + +#~ msgid "User disabled." +#~ msgstr "ユーザーが無効になりました。" + #~ msgid "Random" #~ msgstr "ランダム" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 213d99173..bac130e24 100644 --- a/apps/locale/zh/LC_MESSAGES/django.mo +++ b/apps/locale/zh/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06ff4e3474944be5a7f95106ecdeaa88d66be345fa69e1e62ec5d8c27be580ea -size 132912 +oid sha256:25c1d449875189c84c0d586792424d70651a1f86f55b93332287dfef44db2f2f +size 132289 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index aeea7f4b5..f42e27fd6 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-10-09 18:33+0800\n" +"POT-Creation-Date: 2023-10-10 11:13+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -24,10 +24,11 @@ msgstr "参数 'action' 必须是 [{}]" #: accounts/const/account.py:6 #: accounts/serializers/automations/change_secret.py:32 #: assets/models/_user.py:24 audits/signal_handlers/login_log.py:37 -#: authentication/confirm/password.py:9 authentication/forms.py:32 +#: authentication/confirm/password.py:9 authentication/confirm/password.py:24 +#: authentication/confirm/password.py:26 authentication/forms.py:32 #: authentication/templates/authentication/login.html:324 #: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 -#: users/forms/profile.py:22 users/serializers/user.py:105 +#: users/forms/profile.py:22 users/serializers/user.py:104 #: users/templates/users/_msg_user_created.html:13 #: users/templates/users/user_password_verify.html:18 #: xpack/plugins/cloud/serializers/account_attrs.py:28 @@ -39,7 +40,7 @@ msgstr "密码" msgid "SSH key" msgstr "SSH 密钥" -#: accounts/const/account.py:8 authentication/models/access_key.py:33 +#: accounts/const/account.py:8 authentication/models/access_key.py:37 msgid "Access key" msgstr "Access key" @@ -250,7 +251,7 @@ msgid "Version" msgstr "版本" #: accounts/models/account.py:56 accounts/serializers/account/account.py:211 -#: users/models/user.py:840 +#: users/models/user.py:837 msgid "Source" msgstr "来源" @@ -551,7 +552,7 @@ msgstr "特权账号" #: assets/models/label.py:22 #: authentication/serializers/connect_token_secret.py:114 #: terminal/models/applet/applet.py:40 -#: terminal/models/component/endpoint.py:105 users/serializers/user.py:170 +#: terminal/models/component/endpoint.py:105 users/serializers/user.py:167 msgid "Is active" msgstr "激活" @@ -709,7 +710,7 @@ msgstr "已修改" #: acls/templates/acls/asset_login_reminder.html:6 #: assets/models/automations/base.py:19 #: assets/serializers/automations/base.py:20 -#: authentication/api/connection_token.py:381 ops/models/base.py:17 +#: authentication/api/connection_token.py:386 ops/models/base.py:17 #: ops/models/job.py:139 ops/serializers/job.py:21 #: terminal/templates/terminal/_msg_command_execute_alert.html:16 msgid "Assets" @@ -749,8 +750,8 @@ msgstr "ID" #: terminal/notifications.py:205 terminal/serializers/command.py:16 #: terminal/templates/terminal/_msg_command_warning.html:6 #: terminal/templates/terminal/_msg_session_sharing.html:6 -#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:990 -#: users/models/user.py:1026 users/serializers/group.py:18 +#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:987 +#: users/models/user.py:1023 users/serializers/group.py:18 msgid "User" msgstr "用户" @@ -1003,7 +1004,7 @@ msgstr "优先级可选范围为 1-100 (数值越小越优先)" msgid "Reviewers" msgstr "审批人" -#: acls/models/base.py:43 authentication/models/access_key.py:17 +#: acls/models/base.py:43 authentication/models/access_key.py:20 #: authentication/models/connection_token.py:53 #: authentication/templates/authentication/_access_key_modal.html:32 #: perms/models/asset_permission.py:76 terminal/models/session/sharing.py:29 @@ -1304,7 +1305,7 @@ msgid "Disabled" msgstr "禁用" #: assets/const/base.py:34 settings/serializers/basic.py:6 -#: users/serializers/preference/koko.py:15 +#: users/serializers/preference/koko.py:19 #: users/serializers/preference/lina.py:39 #: users/serializers/preference/luna.py:60 msgid "Basic" @@ -1519,12 +1520,12 @@ msgstr "SSH公钥" #: assets/models/_user.py:28 assets/models/automations/base.py:114 #: assets/models/cmd_filter.py:41 assets/models/group.py:19 #: audits/models.py:261 common/db/models.py:34 ops/models/base.py:54 -#: ops/models/job.py:227 users/models/user.py:1027 +#: ops/models/job.py:227 users/models/user.py:1024 msgid "Date created" msgstr "创建日期" #: assets/models/_user.py:29 assets/models/cmd_filter.py:42 -#: common/db/models.py:35 users/models/user.py:849 +#: common/db/models.py:35 users/models/user.py:846 msgid "Date updated" msgstr "更新日期" @@ -1774,7 +1775,7 @@ msgstr "默认" msgid "Default asset group" msgstr "默认资产组" -#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:1012 +#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:1009 msgid "System" msgstr "系统" @@ -2476,7 +2477,7 @@ msgstr "认证令牌" #: audits/signal_handlers/login_log.py:40 authentication/notifications.py:73 #: authentication/views/login.py:77 authentication/views/wecom.py:159 #: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 -#: users/models/user.py:745 users/models/user.py:850 +#: users/models/user.py:745 users/models/user.py:847 msgid "WeCom" msgstr "企业微信" @@ -2484,14 +2485,14 @@ msgstr "企业微信" #: authentication/views/login.py:89 notifications/backends/__init__.py:14 #: settings/serializers/auth/feishu.py:10 #: settings/serializers/auth/feishu.py:13 users/models/user.py:747 -#: users/models/user.py:852 +#: users/models/user.py:849 msgid "FeiShu" msgstr "飞书" #: audits/signal_handlers/login_log.py:42 authentication/views/dingtalk.py:159 #: authentication/views/login.py:83 notifications/backends/__init__.py:12 #: settings/serializers/auth/dingtalk.py:10 users/models/user.py:746 -#: users/models/user.py:851 +#: users/models/user.py:848 msgid "DingTalk" msgstr "钉钉" @@ -2521,23 +2522,23 @@ msgstr "该操作需要验证您的 MFA, 请先开启并配置" msgid "Reusable connection token is not allowed, global setting not enabled" msgstr "不允许使用可重复使用的连接令牌,未启用全局设置" -#: authentication/api/connection_token.py:352 +#: authentication/api/connection_token.py:357 msgid "Anonymous account is not supported for this asset" msgstr "匿名账号不支持当前资产" -#: authentication/api/connection_token.py:371 +#: authentication/api/connection_token.py:376 msgid "Account not found" msgstr "账号未找到" -#: authentication/api/connection_token.py:374 +#: authentication/api/connection_token.py:379 msgid "Permission expired" msgstr "授权已过期" -#: authentication/api/connection_token.py:401 +#: authentication/api/connection_token.py:406 msgid "ACL action is reject: {}({})" msgstr "ACL 动作是拒绝: {}({})" -#: authentication/api/connection_token.py:405 +#: authentication/api/connection_token.py:410 msgid "ACL action is review" msgstr "ACL 动作是复核" @@ -2578,54 +2579,20 @@ msgstr "认证" msgid "User invalid, disabled or expired" msgstr "用户无效,已禁用或已过期" -#: authentication/backends/drf.py:54 -msgid "Invalid signature header. No credentials provided." -msgstr "不合法的签名头" - -#: authentication/backends/drf.py:57 -msgid "Invalid signature header. Signature string should not contain spaces." -msgstr "不合法的签名头" - -#: authentication/backends/drf.py:64 -msgid "Invalid signature header. Format like AccessKeyId:Signature" -msgstr "不合法的签名头" - -#: authentication/backends/drf.py:68 -msgid "" -"Invalid signature header. Signature string should not contain invalid " -"characters." -msgstr "不合法的签名头" - -#: authentication/backends/drf.py:88 authentication/backends/drf.py:104 -msgid "Invalid signature." -msgstr "签名无效" - -#: authentication/backends/drf.py:95 -msgid "HTTP header: Date not provide or not %a, %d %b %Y %H:%M:%S GMT" -msgstr "HTTP header not valid" - -#: authentication/backends/drf.py:100 -msgid "Expired, more than 15 minutes" -msgstr "已过期,超过15分钟" - -#: authentication/backends/drf.py:107 -msgid "User disabled." -msgstr "用户已禁用" - -#: authentication/backends/drf.py:125 +#: authentication/backends/drf.py:39 msgid "Invalid token header. No credentials provided." msgstr "无效的令牌头。没有提供任何凭据。" -#: authentication/backends/drf.py:128 +#: authentication/backends/drf.py:42 msgid "Invalid token header. Sign string should not contain spaces." msgstr "无效的令牌头。符号字符串不应包含空格。" -#: authentication/backends/drf.py:135 +#: authentication/backends/drf.py:48 msgid "" "Invalid token header. Sign string should not contain invalid characters." msgstr "无效的令牌头。符号字符串不应包含无效字符。" -#: authentication/backends/drf.py:146 +#: authentication/backends/drf.py:61 msgid "Invalid token or cache refreshed." msgstr "刷新的令牌或缓存无效。" @@ -2642,6 +2609,8 @@ msgid "Added on" msgstr "附加" #: authentication/backends/passkey/models.py:14 +#: authentication/models/access_key.py:21 +#: authentication/models/private_token.py:8 msgid "Date last used" msgstr "最后使用日期" @@ -3008,7 +2977,7 @@ msgstr "可以查看超级连接令牌密文" msgid "Super connection token" msgstr "超级连接令牌" -#: authentication/models/private_token.py:9 +#: authentication/models/private_token.py:11 msgid "Private Token" msgstr "私有令牌" @@ -3066,7 +3035,7 @@ msgstr "动作" #: authentication/serializers/connection_token.py:42 #: perms/serializers/permission.py:38 perms/serializers/permission.py:57 -#: users/serializers/user.py:97 users/serializers/user.py:174 +#: users/serializers/user.py:97 users/serializers/user.py:171 msgid "Is expired" msgstr "已过期" @@ -3085,9 +3054,9 @@ msgstr "邮箱" msgid "The {} cannot be empty" msgstr "{} 不能为空" -#: authentication/serializers/token.py:79 perms/serializers/permission.py:37 +#: authentication/serializers/token.py:80 perms/serializers/permission.py:37 #: perms/serializers/permission.py:58 users/serializers/user.py:98 -#: users/serializers/user.py:171 +#: users/serializers/user.py:168 msgid "Is valid" msgstr "是否有效" @@ -6168,7 +6137,7 @@ msgstr "不支持批量创建" msgid "Storage is invalid" msgstr "存储无效" -#: terminal/models/applet/applet.py:30 xpack/plugins/license/models.py:86 +#: terminal/models/applet/applet.py:30 xpack/plugins/license/models.py:88 msgid "Community edition" msgstr "社区版" @@ -7290,7 +7259,7 @@ msgstr "用户设置" msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:804 users/serializers/user.py:172 +#: users/models/user.py:804 users/serializers/user.py:169 msgid "Is service account" msgstr "服务账号" @@ -7302,7 +7271,7 @@ msgstr "头像" msgid "Wechat" msgstr "微信" -#: users/models/user.py:812 users/serializers/user.py:109 +#: users/models/user.py:812 users/serializers/user.py:106 msgid "Phone" msgstr "手机" @@ -7316,43 +7285,47 @@ msgid "Private key" msgstr "ssh私钥" #: users/models/user.py:830 users/serializers/profile.py:125 -#: users/serializers/user.py:169 +#: users/serializers/user.py:166 msgid "Is first login" msgstr "首次登录" -#: users/models/user.py:844 +#: users/models/user.py:840 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:847 +#: users/models/user.py:843 msgid "Need update password" msgstr "需要更新密码" -#: users/models/user.py:971 +#: users/models/user.py:845 +msgid "Date api key used" +msgstr "Api key 最后使用日期" + +#: users/models/user.py:968 msgid "Can not delete admin user" msgstr "无法删除管理员用户" -#: users/models/user.py:997 +#: users/models/user.py:994 msgid "Can invite user" msgstr "可以邀请用户" -#: users/models/user.py:998 +#: users/models/user.py:995 msgid "Can remove user" msgstr "可以移除用户" -#: users/models/user.py:999 +#: users/models/user.py:996 msgid "Can match user" msgstr "可以匹配用户" -#: users/models/user.py:1008 +#: users/models/user.py:1005 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:1011 +#: users/models/user.py:1008 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/models/user.py:1036 +#: users/models/user.py:1033 msgid "User password history" msgstr "用户密码历史" @@ -7391,6 +7364,12 @@ msgstr "重置 MFA" msgid "File name conflict resolution" msgstr "文件名冲突解决方案" +#: users/serializers/preference/koko.py:14 +#, fuzzy +#| msgid "Terminal setting" +msgid "Terminal theme name" +msgstr "终端设置" + #: users/serializers/preference/lina.py:13 msgid "New file encryption password" msgstr "文件加密密码" @@ -7479,7 +7458,7 @@ msgstr "强制 MFA" msgid "Login blocked" msgstr "登录被锁定" -#: users/serializers/user.py:99 users/serializers/user.py:178 +#: users/serializers/user.py:99 users/serializers/user.py:175 msgid "Is OTP bound" msgstr "是否绑定了虚拟 MFA" @@ -7487,27 +7466,27 @@ msgstr "是否绑定了虚拟 MFA" msgid "Can public key authentication" msgstr "可以使用公钥认证" -#: users/serializers/user.py:173 +#: users/serializers/user.py:170 msgid "Is org admin" msgstr "组织管理员" -#: users/serializers/user.py:175 +#: users/serializers/user.py:172 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:179 +#: users/serializers/user.py:176 msgid "MFA level" msgstr "MFA 级别" -#: users/serializers/user.py:285 +#: users/serializers/user.py:282 msgid "Select users" msgstr "选择用户" -#: users/serializers/user.py:286 +#: users/serializers/user.py:283 msgid "For security, only list several users" msgstr "为了安全,仅列出几个用户" -#: users/serializers/user.py:319 +#: users/serializers/user.py:316 msgid "name not unique" msgstr "名称重复" @@ -8416,7 +8395,7 @@ msgstr "许可证导入成功" msgid "License is invalid" msgstr "无效的许可证" -#: xpack/plugins/license/meta.py:10 xpack/plugins/license/models.py:138 +#: xpack/plugins/license/meta.py:10 xpack/plugins/license/models.py:140 msgid "License" msgstr "许可证" @@ -8436,5 +8415,32 @@ msgstr "企业专业版" msgid "Ultimate edition" msgstr "企业旗舰版" +#~ msgid "Invalid signature header. No credentials provided." +#~ msgstr "不合法的签名头" + +#~ msgid "" +#~ "Invalid signature header. Signature string should not contain spaces." +#~ msgstr "不合法的签名头" + +#~ msgid "Invalid signature header. Format like AccessKeyId:Signature" +#~ msgstr "不合法的签名头" + +#~ msgid "" +#~ "Invalid signature header. Signature string should not contain invalid " +#~ "characters." +#~ msgstr "不合法的签名头" + +#~ msgid "Invalid signature." +#~ msgstr "签名无效" + +#~ msgid "HTTP header: Date not provide or not %a, %d %b %Y %H:%M:%S GMT" +#~ msgstr "HTTP header not valid" + +#~ msgid "Expired, more than 15 minutes" +#~ msgstr "已过期,超过15分钟" + +#~ msgid "User disabled." +#~ msgstr "用户已禁用" + #~ msgid "Random" #~ msgstr "随机" diff --git a/apps/users/migrations/0047_user_date_api_key_last_used.py b/apps/users/migrations/0047_user_date_api_key_last_used.py new file mode 100644 index 000000000..21ccd13cb --- /dev/null +++ b/apps/users/migrations/0047_user_date_api_key_last_used.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.10 on 2023-10-08 04:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0046_auto_20230927_1456'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='date_api_key_last_used', + field=models.DateTimeField(blank=True, null=True, verbose_name='Date api key used'), + ), + ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 94b05b331..adac178a7 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -834,11 +834,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, JSONFilterMixin, Abstract ) created_by = models.CharField(max_length=30, default='', blank=True, verbose_name=_('Created by')) updated_by = models.CharField(max_length=30, default='', blank=True, verbose_name=_('Updated by')) - source = models.CharField( - max_length=30, default=Source.local, - choices=Source.choices, - verbose_name=_('Source') - ) + source = models.CharField(max_length=30, default=Source.local, choices=Source.choices, verbose_name=_('Source')) date_password_last_updated = models.DateTimeField( auto_now_add=True, blank=True, null=True, verbose_name=_('Date password last updated') @@ -846,6 +842,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, JSONFilterMixin, Abstract need_update_password = models.BooleanField( default=False, verbose_name=_('Need update password') ) + date_api_key_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date api key used')) date_updated = models.DateTimeField(auto_now=True, verbose_name=_('Date updated')) wecom_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('WeCom')) dingtalk_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('DingTalk')) diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 82ce8f0da..7929483a5 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -101,10 +101,7 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer source="can_use_ssh_key_login", label=_("Can public key authentication"), read_only=True ) - password = EncryptedField( - label=_("Password"), required=False, allow_blank=True, - allow_null=True, max_length=1024, - ) + password = EncryptedField(label=_("Password"), required=False, allow_blank=True, allow_null=True, max_length=1024, ) phone = PhoneField( validators=[PhoneValidator()], required=False, allow_blank=True, allow_null=True, label=_("Phone") ) @@ -128,8 +125,8 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer "created_by", "updated_by", "comment", # 通用字段 ] fields_date = [ - "date_expired", "date_joined", - "last_login", "date_updated" # 日期字段 + "date_expired", "date_joined", "last_login", + "date_updated", "date_api_key_last_used", ] fields_bool = [ "is_superuser", "is_org_admin", @@ -155,7 +152,7 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer read_only_fields = [ "date_joined", "last_login", "created_by", "is_first_login", "wecom_id", "dingtalk_id", - "feishu_id", + "feishu_id", "date_api_key_last_used", ] disallow_self_update_fields = ["is_active", "system_roles", "org_roles"] extra_kwargs = {