From 2aa03d5b79e651e17d6ddab2cc2656b1bf23b66f Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 9 May 2023 13:43:46 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20connect=20token=20=E5=85=81=E8=AE=B8?= =?UTF-8?q?=E5=A4=8D=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/connection_token.py | 34 ++++++++++------ .../0019_connectiontoken_is_reusable.py | 18 +++++++++ .../authentication/models/connection_token.py | 6 +-- .../serializers/connection_token.py | 39 +++++++++++++++++-- apps/common/utils/http.py | 4 ++ apps/jumpserver/conf.py | 4 +- apps/jumpserver/settings/custom.py | 3 ++ apps/settings/serializers/public.py | 1 + 8 files changed, 89 insertions(+), 20 deletions(-) create mode 100644 apps/authentication/migrations/0019_connectiontoken_is_reusable.py diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index a7fed5e7a..03fe9cc8e 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -12,14 +12,12 @@ from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.serializers import ValidationError -from assets.const import CloudTypes from common.api import JMSModelViewSet from common.exceptions import JMSException from common.utils import random_string, get_logger from common.utils.django import get_request_os -from common.utils.http import is_true +from common.utils.http import is_true, is_false from orgs.mixins.api import RootOrgViewMixin from perms.models import ActionChoices from terminal.connect_methods import NativeClient, ConnectMethodUtil @@ -27,7 +25,7 @@ from terminal.models import EndpointRule from ..models import ConnectionToken, date_expired_default from ..serializers import ( ConnectionTokenSerializer, ConnectionTokenSecretSerializer, - SuperConnectionTokenSerializer, ConnectTokenAppletOptionSerializer + SuperConnectionTokenSerializer, ConnectTokenAppletOptionSerializer, ConnectionTokenUpdateSerializer ) __all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet'] @@ -230,10 +228,14 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView search_fields = filterset_fields serializer_classes = { 'default': ConnectionTokenSerializer, + 'update': ConnectionTokenUpdateSerializer, + 'partial_update': ConnectionTokenUpdateSerializer, } + http_method_names = ['get', 'post', 'patch', 'head', 'options', 'trace'] rbac_perms = { 'list': 'authentication.view_connectiontoken', 'retrieve': 'authentication.view_connectiontoken', + 'update': 'authentication.change_connectiontoken', 'create': 'authentication.add_connectiontoken', 'exchange': 'authentication.add_connectiontoken', 'expire': 'authentication.change_connectiontoken', @@ -370,19 +372,27 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet): token_id = request.data.get('id') or '' token = get_object_or_404(ConnectionToken, pk=token_id) - if token.is_expired: - raise ValidationError({'id': 'Token is expired'}) - token.is_valid() serializer = self.get_serializer(instance=token) - expire_now = request.data.get('expire_now', True) - # TODO 暂时特殊处理 k8s 不过期 - if token.asset.type == CloudTypes.K8S: - expire_now = False + expire_now = request.data.get('expire_now', None) + asset_type = token.asset.type + asset_category = token.asset.category + # 设置默认值 + if expire_now is None: + # TODO 暂时特殊处理 k8s 不过期 + if asset_type in ['k8s', 'kubernetes']: + expire_now = False + elif asset_category in ['database', 'db']: + expire_now = False + else: + expire_now = True - if expire_now: + if is_false(expire_now) or token.is_reusable: + logger.debug('Token is reusable or specify, not expire') + else: token.expire() + return Response(serializer.data, status=status.HTTP_200_OK) @action(methods=['POST'], detail=False, url_path='applet-option') diff --git a/apps/authentication/migrations/0019_connectiontoken_is_reusable.py b/apps/authentication/migrations/0019_connectiontoken_is_reusable.py new file mode 100644 index 000000000..92ccc892a --- /dev/null +++ b/apps/authentication/migrations/0019_connectiontoken_is_reusable.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.17 on 2023-05-08 07:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0018_alter_connectiontoken_input_secret'), + ] + + operations = [ + migrations.AddField( + model_name='connectiontoken', + name='is_reusable', + field=models.BooleanField(default=False, verbose_name='Reusable'), + ), + ] diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index eef633d95..112fe9331 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -40,6 +40,7 @@ class ConnectionToken(JMSOrgBaseModel): connect_method = models.CharField(max_length=32, verbose_name=_("Connect method")) user_display = models.CharField(max_length=128, default='', verbose_name=_("User display")) asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display")) + is_reusable = models.BooleanField(default=False, verbose_name=_("Reusable")) date_expired = models.DateTimeField(default=date_expired_default, verbose_name=_("Date expired")) from_ticket = models.OneToOneField( 'tickets.ApplyLoginAssetTicket', related_name='connection_token', @@ -74,7 +75,7 @@ class ConnectionToken(JMSOrgBaseModel): def expire(self): self.date_expired = timezone.now() - self.save() + self.save(update_fields=['date_expired']) def renewal(self): """ 续期 Token,将来支持用户自定义创建 token 后,续期策略要修改 """ @@ -108,9 +109,8 @@ class ConnectionToken(JMSOrgBaseModel): error = _('No user or invalid user') raise PermissionDenied(error) if not self.asset or not self.asset.is_active: - is_valid = False error = _('No asset or inactive asset') - return is_valid, error + raise PermissionDenied(error) if not self.account: error = _('No account') raise PermissionDenied(error) diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index 22545115f..7ce1b4291 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -1,13 +1,17 @@ +from django.conf import settings +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from perms.serializers.permission import ActionChoicesField -from orgs.mixins.serializers import OrgResourceModelSerializerMixin from common.serializers.fields import EncryptedField +from common.utils import lazyproperty +from orgs.mixins.serializers import OrgResourceModelSerializerMixin +from perms.serializers.permission import ActionChoicesField from ..models import ConnectionToken __all__ = [ 'ConnectionTokenSerializer', 'SuperConnectionTokenSerializer', + 'ConnectionTokenUpdateSerializer', ] @@ -25,13 +29,13 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): fields_small = fields_mini + [ 'user', 'asset', 'account', 'input_username', 'input_secret', 'connect_method', 'protocol', 'actions', - 'is_active', 'from_ticket', 'from_ticket_info', + 'is_active', 'is_reusable', 'from_ticket', 'from_ticket_info', 'date_expired', 'date_created', 'date_updated', 'created_by', 'updated_by', 'org_id', 'org_name', ] read_only_fields = [ # 普通 Token 不支持指定 user - 'user', 'expire_time', 'is_expired', + 'user', 'expire_time', 'is_expired', 'date_expired', 'user_display', 'asset_display', ] fields = fields_small + read_only_fields @@ -57,6 +61,33 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): return info +class ConnectionTokenUpdateSerializer(ConnectionTokenSerializer): + class Meta(ConnectionTokenSerializer.Meta): + can_update_fields = ['is_reusable'] + read_only_fields = list(set(ConnectionTokenSerializer.Meta.fields) - set(can_update_fields)) + + @lazyproperty + def date_expired_max(self): + delta = self.instance.date_expired - self.instance.date_created + if delta.total_seconds() > 3600 * 24: + return self.instance.date_expired + + seconds = settings.CONNECTION_TOKEN_EXPIRATION_MAX + return timezone.now() + timezone.timedelta(seconds=seconds) + + @staticmethod + def validate_is_reusable(value): + if value and not settings.CONNECTION_TOKEN_REUSABLE: + raise serializers.ValidationError(_('Reusable connection token is not allowed, global setting not enabled')) + return value + + def validate(self, attrs): + reusable = attrs.get('is_reusable', False) + if reusable: + attrs['date_expired'] = self.date_expired_max + return attrs + + class SuperConnectionTokenSerializer(ConnectionTokenSerializer): class Meta(ConnectionTokenSerializer.Meta): read_only_fields = list(set(ConnectionTokenSerializer.Meta.read_only_fields) - {'user'}) diff --git a/apps/common/utils/http.py b/apps/common/utils/http.py index b684f004a..baf741407 100644 --- a/apps/common/utils/http.py +++ b/apps/common/utils/http.py @@ -45,3 +45,7 @@ def get_remote_addr(request): def is_true(value): return value in BooleanField.TRUE_VALUES + + +def is_false(value): + return value in BooleanField.FALSE_VALUES diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 343224675..18872058a 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -229,7 +229,9 @@ class Config(dict): 'SESSION_COOKIE_AGE': 3600 * 24, 'SESSION_EXPIRE_AT_BROWSER_CLOSE': False, 'LOGIN_URL': reverse_lazy('authentication:login'), - 'CONNECTION_TOKEN_EXPIRATION': 5 * 60, + 'CONNECTION_TOKEN_EXPIRATION': 5 * 60, # 默认 + 'CONNECTION_TOKEN_EXPIRATION_MAX': 60 * 60 * 24 * 30, # 最大 + 'CONNECTION_TOKEN_REUSABLE': False, # Custom Config 'AUTH_CUSTOM': False, diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 62ba143ac..7312bd7eb 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -131,6 +131,9 @@ TICKETS_ENABLED = CONFIG.TICKETS_ENABLED REFERER_CHECK_ENABLED = CONFIG.REFERER_CHECK_ENABLED CONNECTION_TOKEN_ENABLED = CONFIG.CONNECTION_TOKEN_ENABLED +CONNECTION_TOKEN_REUSABLE = CONFIG.CONNECTION_TOKEN_REUSABLE +CONNECTION_TOKEN_EXPIRATION_MAX = CONFIG.CONNECTION_TOKEN_EXPIRATION_MAX + FORGOT_PASSWORD_URL = CONFIG.FORGOT_PASSWORD_URL # 自定义默认组织名 diff --git a/apps/settings/serializers/public.py b/apps/settings/serializers/public.py index e9861eb72..c195b5e4e 100644 --- a/apps/settings/serializers/public.py +++ b/apps/settings/serializers/public.py @@ -48,3 +48,4 @@ class PrivateSettingSerializer(PublicSettingSerializer): ANNOUNCEMENT = serializers.DictField() TICKETS_ENABLED = serializers.BooleanField() + CONNECTION_TOKEN_REUSABLE = serializers.BooleanField()