perf: 修改 connection token

pull/9115/head
ibuler 2022-11-22 21:54:40 +08:00
parent 6d5be66b5e
commit 779161d79a
11 changed files with 195 additions and 82 deletions

View File

@ -6,6 +6,7 @@ import urllib.parse
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
@ -15,6 +16,7 @@ from rest_framework.response import Response
from common.drf.api import JMSModelViewSet
from common.http import is_true
from orgs.mixins.api import RootOrgViewMixin
from orgs.utils import tmp_to_root_org
from perms.models import ActionChoices
from terminal.models import EndpointRule
from ..models import ConnectionToken
@ -257,6 +259,10 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
'get_client_protocol_url': 'authentication.add_connectiontoken',
}
def dispatch(self, request, *args, **kwargs):
with tmp_to_root_org():
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return ConnectionToken.objects.filter(user=self.request.user)
@ -264,22 +270,36 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
return self.request.user
def perform_create(self, serializer):
user = self.get_user(serializer)
asset = serializer.validated_data.get('asset')
account_username = serializer.validated_data.get('account_username')
self.validate_asset_permission(user, asset, account_username)
return super(ConnectionTokenViewSet, self).perform_create(serializer)
self.validate_serializer(serializer)
return super().perform_create(serializer)
@staticmethod
def validate_asset_permission(user, asset, account_username):
def validate_serializer(self, serializer):
from perms.utils.account import PermAccountUtil
actions, expire_at = PermAccountUtil().validate_permission(user, asset, account_username)
if not actions:
error = 'No actions'
raise PermissionDenied(error)
if expire_at < time.time():
error = 'Expired'
raise PermissionDenied(error)
data = serializer.validated_data
user = self.get_user(serializer)
asset = data.get('asset')
login = data.get('login')
data['org_id'] = asset.org_id
data['user'] = user
util = PermAccountUtil()
permed_account = util.validate_permission(user, asset, login)
if not permed_account or not permed_account.actions:
msg = 'user `{}` not has asset `{}` permission for login `{}`'.format(
user, asset, login
)
raise PermissionDenied(msg)
if permed_account.date_expired < timezone.now():
raise PermissionDenied('Expired')
if permed_account.has_secret:
serializer.validated_data['secret'] = ''
if permed_account.username != '@INPUT':
serializer.validated_data['username'] = ''
return permed_account
class SuperConnectionTokenViewSet(ConnectionTokenViewSet):

View File

@ -16,7 +16,7 @@ def migrate_system_user_to_account(apps, schema_editor):
count += len(connection_tokens)
updated = []
for connection_token in connection_tokens:
connection_token.account_username = connection_token.system_user.username
connection_token.account = connection_token.system_user.username
updated.append(connection_token)
connection_token_model.objects.bulk_update(updated, ['account_username'])

View File

@ -0,0 +1,29 @@
# Generated by Django 3.2.14 on 2022-11-22 13:52
import common.db.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0013_connectiontoken_protocol'),
]
operations = [
migrations.RenameField(
model_name='connectiontoken',
old_name='account_username',
new_name='login'
),
migrations.AddField(
model_name='connectiontoken',
name='username',
field=models.CharField(default='', max_length=128, verbose_name='Username'),
),
migrations.AlterField(
model_name='connectiontoken',
name='secret',
field=common.db.fields.EncryptCharField(default='', max_length=128, verbose_name='Secret'),
),
]

View File

@ -2,13 +2,15 @@ import time
from datetime import timedelta
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from orgs.mixins.models import OrgModelMixin
from django.db import models
from common.utils import lazyproperty
from django.conf import settings
from rest_framework.exceptions import PermissionDenied
from orgs.mixins.models import OrgModelMixin
from common.utils import lazyproperty, pretty_string
from common.utils.timezone import as_current_tz
from common.db.models import JMSBaseModel
from common.db.fields import EncryptCharField
from assets.const import Protocol
@ -25,13 +27,14 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
'assets.Asset', on_delete=models.SET_NULL, null=True, blank=True,
related_name='connection_tokens', verbose_name=_('Asset'),
)
login = models.CharField(max_length=128, verbose_name=_("Login account"))
username = models.CharField(max_length=128, default='', verbose_name=_("Username"))
secret = EncryptCharField(max_length=64, default='', verbose_name=_("Secret"))
protocol = models.CharField(
choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol")
)
user_display = models.CharField(max_length=128, default='', verbose_name=_("User display"))
asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display"))
account_username = models.CharField(max_length=128, default='', verbose_name=_("Account"))
secret = models.CharField(max_length=64, default='', verbose_name=_("Secret"))
date_expired = models.DateTimeField(
default=date_expired_default, verbose_name=_("Date expired")
)
@ -59,9 +62,10 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
seconds = 0
return int(seconds)
@classmethod
def get_default_date_expired(cls):
return date_expired_default()
def save(self, *args, **kwargs):
self.asset_display = pretty_string(self.asset, max_length=128)
self.user_display = pretty_string(self.user, max_length=128)
return super().save(*args, **kwargs)
def expire(self):
self.date_expired = timezone.now()
@ -69,7 +73,7 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
def renewal(self):
""" 续期 Token将来支持用户自定义创建 token 后,续期策略要修改 """
self.date_expired = self.get_default_date_expired()
self.date_expired = date_expired_default()
self.save()
# actions 和 expired_at 在 check_valid() 中赋值
@ -89,28 +93,52 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
is_valid = False
error = _('No asset or inactive asset')
return is_valid, error
if not self.account_username:
if not self.login:
is_valid = False
error = _('No account')
return is_valid, error
actions, expire_at = PermAccountUtil().validate_permission(
self.user, self.asset, self.account_username
permed_account = PermAccountUtil().validate_permission(
self.user, self.asset, self.login
)
if not actions or expire_at < time.time():
is_valid = False
error = _('User has no permission to access asset or permission expired')
return is_valid, error
self.actions = actions
self.expire_at = expire_at
if not permed_account or not permed_account.actions:
msg = 'user `{}` not has asset `{}` permission for login `{}`'.format(
self.user, self.asset, self.login
)
raise PermissionDenied(msg)
if permed_account.date_expired < timezone.now():
raise PermissionDenied('Expired')
is_valid, error = True, ''
return is_valid, error
@lazyproperty
def account(self):
def platform(self):
return self.asset.platform
@lazyproperty
def accounts(self):
if not self.asset:
return None
account = self.asset.accounts.filter(username=self.account_username).first()
return account
data = []
if self.login == '@INPUT':
data.append({
'name': self.login,
'username': self.username,
'secret_type': 'password',
'secret': self.secret
})
else:
accounts = self.asset.accounts.filter(username=self.login)
for account in accounts:
data.append({
'username': account.uesrname,
'secret_type': account.secret_type,
'secret': account.secret if account.secret else self.secret
})
return data
@lazyproperty
def domain(self):

View File

@ -2,9 +2,8 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from assets.models import Asset, Gateway, Domain, CommandFilterRule, Account, Platform
from assets.serializers import PlatformSerializer
from authentication.models import ConnectionToken
from common.utils import pretty_string
from common.utils.random import random_string
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from perms.serializers.permission import ActionChoicesField
from users.models import User
@ -16,6 +15,8 @@ __all__ = [
class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
username = serializers.CharField(max_length=128, label=_("Input username"),
allow_null=True, allow_blank=True)
is_valid = serializers.BooleanField(read_only=True, label=_('Validity'))
expire_time = serializers.IntegerField(read_only=True, label=_('Expired time'))
@ -23,9 +24,10 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
model = ConnectionToken
fields_mini = ['id']
fields_small = fields_mini + [
'secret', 'account_username', 'date_expired',
'date_created', 'date_updated',
'created_by', 'updated_by', 'org_id', 'org_name',
'protocol', 'login', 'secret', 'username',
'date_expired', 'date_created',
'date_updated', 'created_by',
'updated_by', 'org_id', 'org_name',
]
fields_fk = [
'user', 'asset',
@ -45,32 +47,6 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
def get_user(self, attrs):
return self.get_request_user()
def validate(self, attrs):
fields_attrs = self.construct_internal_fields_attrs(attrs)
attrs.update(fields_attrs)
return attrs
def construct_internal_fields_attrs(self, attrs):
asset = attrs.get('asset') or ''
asset_display = pretty_string(str(asset), max_length=128)
user = self.get_user(attrs)
user_display = pretty_string(str(user), max_length=128)
secret = attrs.get('secret') or random_string(16)
date_expired = attrs.get('date_expired') or ConnectionToken.get_default_date_expired()
org_id = asset.org_id
if not isinstance(asset, Asset):
error = ''
raise serializers.ValidationError(error)
attrs = {
'user': user,
'secret': secret,
'user_display': user_display,
'asset_display': asset_display,
'date_expired': date_expired,
'org_id': org_id,
}
return attrs
class ConnectionTokenDisplaySerializer(ConnectionTokenSerializer):
class Meta(ConnectionTokenSerializer.Meta):
@ -122,7 +98,7 @@ class ConnectionTokenAccountSerializer(serializers.ModelSerializer):
class Meta:
model = Account
fields = [
'id', 'name', 'username', 'secret_type', 'secret', 'version'
'username', 'secret_type', 'secret',
]
@ -154,26 +130,31 @@ class ConnectionTokenCmdFilterRuleSerializer(serializers.ModelSerializer):
]
class ConnectionTokenPlatform(serializers.ModelSerializer):
class Meta:
class ConnectionTokenPlatform(PlatformSerializer):
class Meta(PlatformSerializer.Meta):
model = Platform
fields = ['id', 'name', 'org_id']
def get_field_names(self, declared_fields, info):
names = super().get_field_names(declared_fields, info)
names = [n for n in names if n not in ['automation']]
return names
class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
user = ConnectionTokenUserSerializer(read_only=True)
asset = ConnectionTokenAssetSerializer(read_only=True)
platform = ConnectionTokenPlatform(read_only=True)
account = ConnectionTokenAccountSerializer(read_only=True)
accounts = ConnectionTokenAccountSerializer(read_only=True, many=True)
gateway = ConnectionTokenGatewaySerializer(read_only=True)
cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True)
# cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True)
actions = ActionChoicesField()
expire_at = serializers.IntegerField()
class Meta:
model = ConnectionToken
fields = [
'id', 'secret', 'user', 'asset', 'account_username',
'account', 'protocol', 'domain', 'gateway',
'cmd_filter_rules', 'actions', 'expire_at',
'id', 'secret', 'user', 'asset', 'login',
'accounts', 'protocol', 'domain', 'gateway',
'actions', 'expire_at',
'platform',
]

View File

@ -344,7 +344,7 @@ def get_file_by_arch(dir, filename):
return file_path
def pretty_string(data: str, max_length=128, ellipsis_str='...'):
def pretty_string(data, max_length=128, ellipsis_str='...'):
"""
params:
data: abcdefgh
@ -353,6 +353,7 @@ def pretty_string(data: str, max_length=128, ellipsis_str='...'):
return:
ab...gh
"""
data = str(data)
if len(data) < max_length:
return data
remain_length = max_length - len(ellipsis_str)

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:860b4d38beff81667c64da41c026a7dd28c3c93a28ae61fefaa7c26875f35638
size 73906864

View File

@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _
default_interface = dict((
('logo_logout', static('img/logo.png')),
('logo_index', static('img/logo_text.png')),
('logo_index', static('img/logo_text_white.png')),
('login_image', static('img/login_image.jpg')),
('favicon', static('img/facio.ico')),
('login_title', _('JumpServer Open Source Bastion Host')),

View File

@ -0,0 +1,5 @@
from rest_framework.viewsets import ModelViewSet
class PermTokenViewSet(ModelViewSet):
pass

View File

@ -17,10 +17,8 @@ class PermAccountUtil(AssetPermissionUtil):
"""
permed_accounts = self.get_permed_accounts_for_user(user, asset)
accounts_mapper = {account.username: account for account in permed_accounts}
account = accounts_mapper.get(account_username)
actions, date_expired = (account.actions, account.date_expired) if account else (False, None)
return actions, date_expired
return account
def get_permed_accounts_for_user(self, user, asset):
""" 获取授权给用户某个资产的账号 """

View File

@ -0,0 +1,48 @@
# Generated by Django 3.2.14 on 2022-11-21 10:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tickets', '0023_alter_applyassetticket_apply_actions'),
]
operations = [
migrations.AlterField(
model_name='approvalrule',
name='strategy',
field=models.CharField(choices=[('org_admin', 'Org admin'), ('custom_user', 'Custom user'), ('super_admin', 'Super admin'), ('super_org_admin', 'Super admin and org admin')], default='super_admin', max_length=64, verbose_name='Approve strategy'),
),
migrations.AlterField(
model_name='ticket',
name='state',
field=models.CharField(choices=[('pending', 'Open'), ('closed', 'Cancel'), ('reopen', 'Reopen'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=16, verbose_name='State'),
),
migrations.AlterField(
model_name='ticket',
name='type',
field=models.CharField(choices=[('general', 'General'), ('apply_asset', 'Apply for asset'), ('login_confirm', 'Login confirm'), ('command_confirm', 'Command confirm'), ('login_asset_confirm', 'Login asset confirm')], default='general', max_length=64, verbose_name='Type'),
),
migrations.AlterField(
model_name='ticketassignee',
name='state',
field=models.CharField(choices=[('pending', 'Open'), ('closed', 'Cancel'), ('reopen', 'Reopen'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=64),
),
migrations.AlterField(
model_name='ticketflow',
name='type',
field=models.CharField(choices=[('general', 'General'), ('apply_asset', 'Apply for asset'), ('login_confirm', 'Login confirm'), ('command_confirm', 'Command confirm'), ('login_asset_confirm', 'Login asset confirm')], default='general', max_length=64, verbose_name='Type'),
),
migrations.AlterField(
model_name='ticketstep',
name='state',
field=models.CharField(choices=[('pending', 'Pending'), ('closed', 'Closed'), ('reopen', 'Reopen'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=64, verbose_name='State'),
),
migrations.AlterField(
model_name='ticketstep',
name='status',
field=models.CharField(choices=[('active', 'Active'), ('closed', 'Closed'), ('pending', 'Pending')], default='pending', max_length=16),
),
]