perf: 修改 connection token

This commit is contained in:
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.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils import timezone
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
@ -15,6 +16,7 @@ from rest_framework.response import Response
from common.drf.api import JMSModelViewSet from common.drf.api import JMSModelViewSet
from common.http import is_true from common.http import is_true
from orgs.mixins.api import RootOrgViewMixin from orgs.mixins.api import RootOrgViewMixin
from orgs.utils import tmp_to_root_org
from perms.models import ActionChoices from perms.models import ActionChoices
from terminal.models import EndpointRule from terminal.models import EndpointRule
from ..models import ConnectionToken from ..models import ConnectionToken
@ -257,6 +259,10 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
'get_client_protocol_url': 'authentication.add_connectiontoken', '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): def get_queryset(self):
return ConnectionToken.objects.filter(user=self.request.user) return ConnectionToken.objects.filter(user=self.request.user)
@ -264,22 +270,36 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
return self.request.user return self.request.user
def perform_create(self, serializer): def perform_create(self, serializer):
user = self.get_user(serializer) self.validate_serializer(serializer)
asset = serializer.validated_data.get('asset') return super().perform_create(serializer)
account_username = serializer.validated_data.get('account_username')
self.validate_asset_permission(user, asset, account_username)
return super(ConnectionTokenViewSet, self).perform_create(serializer)
@staticmethod def validate_serializer(self, serializer):
def validate_asset_permission(user, asset, account_username):
from perms.utils.account import PermAccountUtil from perms.utils.account import PermAccountUtil
actions, expire_at = PermAccountUtil().validate_permission(user, asset, account_username)
if not actions: data = serializer.validated_data
error = 'No actions' user = self.get_user(serializer)
raise PermissionDenied(error) asset = data.get('asset')
if expire_at < time.time(): login = data.get('login')
error = 'Expired' data['org_id'] = asset.org_id
raise PermissionDenied(error) 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): class SuperConnectionTokenViewSet(ConnectionTokenViewSet):

View File

@ -16,7 +16,7 @@ def migrate_system_user_to_account(apps, schema_editor):
count += len(connection_tokens) count += len(connection_tokens)
updated = [] updated = []
for connection_token in connection_tokens: 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) updated.append(connection_token)
connection_token_model.objects.bulk_update(updated, ['account_username']) 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 datetime import timedelta
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ 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 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.utils.timezone import as_current_tz
from common.db.models import JMSBaseModel from common.db.models import JMSBaseModel
from common.db.fields import EncryptCharField
from assets.const import Protocol from assets.const import Protocol
@ -25,13 +27,14 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
'assets.Asset', on_delete=models.SET_NULL, null=True, blank=True, 'assets.Asset', on_delete=models.SET_NULL, null=True, blank=True,
related_name='connection_tokens', verbose_name=_('Asset'), 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( protocol = models.CharField(
choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol") choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol")
) )
user_display = models.CharField(max_length=128, default='', verbose_name=_("User display")) user_display = models.CharField(max_length=128, default='', verbose_name=_("User display"))
asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset 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( date_expired = models.DateTimeField(
default=date_expired_default, verbose_name=_("Date expired") default=date_expired_default, verbose_name=_("Date expired")
) )
@ -59,9 +62,10 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
seconds = 0 seconds = 0
return int(seconds) return int(seconds)
@classmethod def save(self, *args, **kwargs):
def get_default_date_expired(cls): self.asset_display = pretty_string(self.asset, max_length=128)
return date_expired_default() self.user_display = pretty_string(self.user, max_length=128)
return super().save(*args, **kwargs)
def expire(self): def expire(self):
self.date_expired = timezone.now() self.date_expired = timezone.now()
@ -69,7 +73,7 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
def renewal(self): def renewal(self):
""" 续期 Token将来支持用户自定义创建 token 后,续期策略要修改 """ """ 续期 Token将来支持用户自定义创建 token 后,续期策略要修改 """
self.date_expired = self.get_default_date_expired() self.date_expired = date_expired_default()
self.save() self.save()
# actions 和 expired_at 在 check_valid() 中赋值 # actions 和 expired_at 在 check_valid() 中赋值
@ -89,28 +93,52 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
is_valid = False is_valid = False
error = _('No asset or inactive asset') error = _('No asset or inactive asset')
return is_valid, error return is_valid, error
if not self.account_username: if not self.login:
is_valid = False is_valid = False
error = _('No account') error = _('No account')
return is_valid, error 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(): if not permed_account or not permed_account.actions:
is_valid = False msg = 'user `{}` not has asset `{}` permission for login `{}`'.format(
error = _('User has no permission to access asset or permission expired') self.user, self.asset, self.login
return is_valid, error )
self.actions = actions raise PermissionDenied(msg)
self.expire_at = expire_at
if permed_account.date_expired < timezone.now():
raise PermissionDenied('Expired')
is_valid, error = True, '' is_valid, error = True, ''
return is_valid, error return is_valid, error
@lazyproperty @lazyproperty
def account(self): def platform(self):
return self.asset.platform
@lazyproperty
def accounts(self):
if not self.asset: if not self.asset:
return None 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 @lazyproperty
def domain(self): def domain(self):

View File

@ -2,9 +2,8 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from assets.models import Asset, Gateway, Domain, CommandFilterRule, Account, Platform from assets.models import Asset, Gateway, Domain, CommandFilterRule, Account, Platform
from assets.serializers import PlatformSerializer
from authentication.models import ConnectionToken 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 orgs.mixins.serializers import OrgResourceModelSerializerMixin
from perms.serializers.permission import ActionChoicesField from perms.serializers.permission import ActionChoicesField
from users.models import User from users.models import User
@ -16,6 +15,8 @@ __all__ = [
class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): 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')) is_valid = serializers.BooleanField(read_only=True, label=_('Validity'))
expire_time = serializers.IntegerField(read_only=True, label=_('Expired time')) expire_time = serializers.IntegerField(read_only=True, label=_('Expired time'))
@ -23,9 +24,10 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
model = ConnectionToken model = ConnectionToken
fields_mini = ['id'] fields_mini = ['id']
fields_small = fields_mini + [ fields_small = fields_mini + [
'secret', 'account_username', 'date_expired', 'protocol', 'login', 'secret', 'username',
'date_created', 'date_updated', 'date_expired', 'date_created',
'created_by', 'updated_by', 'org_id', 'org_name', 'date_updated', 'created_by',
'updated_by', 'org_id', 'org_name',
] ]
fields_fk = [ fields_fk = [
'user', 'asset', 'user', 'asset',
@ -45,32 +47,6 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
def get_user(self, attrs): def get_user(self, attrs):
return self.get_request_user() 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 ConnectionTokenDisplaySerializer(ConnectionTokenSerializer):
class Meta(ConnectionTokenSerializer.Meta): class Meta(ConnectionTokenSerializer.Meta):
@ -122,7 +98,7 @@ class ConnectionTokenAccountSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Account model = Account
fields = [ 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 ConnectionTokenPlatform(PlatformSerializer):
class Meta: class Meta(PlatformSerializer.Meta):
model = Platform 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): class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
user = ConnectionTokenUserSerializer(read_only=True) user = ConnectionTokenUserSerializer(read_only=True)
asset = ConnectionTokenAssetSerializer(read_only=True) asset = ConnectionTokenAssetSerializer(read_only=True)
platform = ConnectionTokenPlatform(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) gateway = ConnectionTokenGatewaySerializer(read_only=True)
cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True) # cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True)
actions = ActionChoicesField() actions = ActionChoicesField()
expire_at = serializers.IntegerField() expire_at = serializers.IntegerField()
class Meta: class Meta:
model = ConnectionToken model = ConnectionToken
fields = [ fields = [
'id', 'secret', 'user', 'asset', 'account_username', 'id', 'secret', 'user', 'asset', 'login',
'account', 'protocol', 'domain', 'gateway', 'accounts', 'protocol', 'domain', 'gateway',
'cmd_filter_rules', 'actions', 'expire_at', 'actions', 'expire_at',
'platform',
] ]

View File

@ -344,7 +344,7 @@ def get_file_by_arch(dir, filename):
return file_path return file_path
def pretty_string(data: str, max_length=128, ellipsis_str='...'): def pretty_string(data, max_length=128, ellipsis_str='...'):
""" """
params: params:
data: abcdefgh data: abcdefgh
@ -353,6 +353,7 @@ def pretty_string(data: str, max_length=128, ellipsis_str='...'):
return: return:
ab...gh ab...gh
""" """
data = str(data)
if len(data) < max_length: if len(data) < max_length:
return data return data
remain_length = max_length - len(ellipsis_str) 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(( default_interface = dict((
('logo_logout', static('img/logo.png')), ('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')), ('login_image', static('img/login_image.jpg')),
('favicon', static('img/facio.ico')), ('favicon', static('img/facio.ico')),
('login_title', _('JumpServer Open Source Bastion Host')), ('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) permed_accounts = self.get_permed_accounts_for_user(user, asset)
accounts_mapper = {account.username: account for account in permed_accounts} accounts_mapper = {account.username: account for account in permed_accounts}
account = accounts_mapper.get(account_username) account = accounts_mapper.get(account_username)
actions, date_expired = (account.actions, account.date_expired) if account else (False, None) return account
return actions, date_expired
def get_permed_accounts_for_user(self, user, asset): 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),
),
]