mirror of https://github.com/jumpserver/jumpserver
perf: 修改 connection token
parent
6d5be66b5e
commit
779161d79a
|
@ -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):
|
||||
|
|
|
@ -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'])
|
||||
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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):
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:860b4d38beff81667c64da41c026a7dd28c3c93a28ae61fefaa7c26875f35638
|
||||
size 73906864
|
|
@ -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')),
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
|
||||
class PermTokenViewSet(ModelViewSet):
|
||||
pass
|
|
@ -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):
|
||||
""" 获取授权给用户某个资产的账号 """
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
Loading…
Reference in New Issue