perf: connect token 允许复用

pull/10420/head
ibuler 2023-05-09 13:43:46 +08:00 committed by Jiangjie.Bai
parent 926550bf26
commit 2aa03d5b79
8 changed files with 89 additions and 20 deletions

View File

@ -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')

View File

@ -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'),
),
]

View File

@ -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)

View File

@ -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'})

View File

@ -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

View File

@ -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,

View File

@ -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
# 自定义默认组织名

View File

@ -48,3 +48,4 @@ class PrivateSettingSerializer(PublicSettingSerializer):
ANNOUNCEMENT = serializers.DictField()
TICKETS_ENABLED = serializers.BooleanField()
CONNECTION_TOKEN_REUSABLE = serializers.BooleanField()