Merge remote-tracking branch 'origin/v3' into v3

pull/9118/head^2^2
Aaron3S 2022-11-24 00:52:45 +08:00
commit d977013dc6
9 changed files with 185 additions and 97 deletions

View File

@ -3,7 +3,6 @@ from .common import Asset
class Host(Asset): class Host(Asset):
pass
@classmethod @classmethod
def get_gateway_queryset(cls): def get_gateway_queryset(cls):

View File

@ -13,8 +13,9 @@ from django.utils.translation import ugettext_lazy as _
from common.db import fields from common.db import fields
from common.utils import get_logger, lazyproperty from common.utils import get_logger, lazyproperty
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from assets.models import Host
from .base import BaseAccount from .base import BaseAccount
from ..const import SecretType, GATEWAY_NAME from ..const import SecretType
logger = get_logger(__file__) logger = get_logger(__file__)
@ -37,7 +38,7 @@ class Domain(OrgModelMixin):
@lazyproperty @lazyproperty
def gateways(self): def gateways(self):
return self.assets.filter(platform__name=GATEWAY_NAME, is_active=True) return Host.get_gateway_queryset().filter(domain=self, is_active=True)
def select_gateway(self): def select_gateway(self):
return self.random_gateway() return self.random_gateway()

View File

@ -1,28 +1,33 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework import serializers from rest_framework import serializers
from rest_framework.generics import get_object_or_404
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.drf.serializers import SecretReadableMixin from common.drf.serializers import SecretReadableMixin
from ..models import Domain, Asset from common.drf.fields import ObjectRelatedField, EncryptedField
from assets.const import SecretType
from ..models import Domain, Asset, Account
from ..serializers import HostSerializer
from .utils import validate_password_for_ansible, validate_ssh_key
class DomainSerializer(BulkOrgResourceModelSerializer): class DomainSerializer(BulkOrgResourceModelSerializer):
asset_count = serializers.SerializerMethodField(label=_('Assets amount')) asset_count = serializers.SerializerMethodField(label=_('Assets amount'))
gateway_count = serializers.SerializerMethodField(label=_('Gateways count')) gateway_count = serializers.SerializerMethodField(label=_('Gateways count'))
assets = ObjectRelatedField(
many=True, required=False, queryset=Asset.objects, label=_('Asset')
)
class Meta: class Meta:
model = Domain model = Domain
fields_mini = ['id', 'name'] fields_mini = ['id', 'name']
fields_small = fields_mini + [ fields_small = fields_mini + ['comment']
'comment', 'date_created' fields_m2m = ['assets']
] read_only_fields = ['asset_count', 'gateway_count', 'date_created']
fields_m2m = [ fields = fields_small + fields_m2m + read_only_fields
'asset_count', 'assets', 'gateway_count',
]
fields = fields_small + fields_m2m
read_only_fields = ('asset_count', 'gateway_count', 'date_created')
extra_kwargs = { extra_kwargs = {
'assets': {'required': False, 'label': _('Assets')}, 'assets': {'required': False, 'label': _('Assets')},
} }
@ -36,20 +41,86 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
return obj.gateways.count() return obj.gateways.count()
class GatewaySerializer(BulkOrgResourceModelSerializer): class GatewaySerializer(HostSerializer):
is_connective = serializers.BooleanField(required=False, label=_('Connectivity')) password = EncryptedField(
label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024,
validators=[validate_password_for_ansible], write_only=True
)
private_key = EncryptedField(
label=_('SSH private key'), required=False, allow_blank=True, allow_null=True,
max_length=16384, write_only=True
)
passphrase = serializers.CharField(
label=_('Key password'), allow_blank=True, allow_null=True, required=False, write_only=True,
max_length=512,
)
username = serializers.CharField(
label=_('Username'), allow_blank=True, max_length=128, required=True,
)
class Meta: class Meta(HostSerializer.Meta):
model = Asset fields = HostSerializer.Meta.fields + [
fields_mini = ['id'] 'username', 'password', 'private_key', 'passphrase'
fields_small = fields_mini + [
'address', 'port', 'protocol',
'is_active', 'is_connective',
'date_created', 'date_updated',
'created_by', 'comment',
] ]
fields_fk = ['domain']
fields = fields_small + fields_fk def validate_private_key(self, secret):
if not secret:
return
passphrase = self.initial_data.get('passphrase')
passphrase = passphrase if passphrase else None
validate_ssh_key(secret, passphrase)
return secret
@staticmethod
def clean_auth_fields(validated_data):
username = validated_data.pop('username', None)
password = validated_data.pop('password', None)
private_key = validated_data.pop('private_key', None)
validated_data.pop('passphrase', None)
return username, password, private_key
@staticmethod
def create_accounts(instance, username, password, private_key):
account_name = f'{instance.name}-{_("Gateway")}'
account_data = {
'privileged': True,
'name': account_name,
'username': username,
'asset_id': instance.id,
'created_by': instance.created_by
}
if password:
Account.objects.create(
**account_data, secret=password, secret_type=SecretType.PASSWORD
)
if private_key:
Account.objects.create(
**account_data, secret=private_key, secret_type=SecretType.SSH_KEY
)
@staticmethod
def update_accounts(instance, username, password, private_key):
accounts = instance.accounts.filter(username=username)
if password:
account = get_object_or_404(accounts, SecretType.PASSWORD)
account.secret = password
account.save()
if private_key:
account = get_object_or_404(accounts, SecretType.SSH_KEY)
account.secret = private_key
account.save()
def create(self, validated_data):
auth_fields = self.clean_auth_fields(validated_data)
instance = super().create(validated_data)
self.create_accounts(instance, *auth_fields)
return instance
def update(self, instance, validated_data):
auth_fields = self.clean_auth_fields(validated_data)
instance = super().update(instance, validated_data)
self.update_accounts(instance, *auth_fields)
return instance
class GatewayWithAuthSerializer(SecretReadableMixin, GatewaySerializer): class GatewayWithAuthSerializer(SecretReadableMixin, GatewaySerializer):

View File

@ -28,9 +28,6 @@ from ..serializers import (
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet'] __all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
# ExtraActionApiMixin
class RDPFileClientProtocolURLMixin: class RDPFileClientProtocolURLMixin:
request: Request request: Request
get_serializer: callable get_serializer: callable
@ -72,8 +69,7 @@ class RDPFileClientProtocolURLMixin:
# 设置磁盘挂载 # 设置磁盘挂载
drives_redirect = is_true(self.request.query_params.get('drives_redirect')) drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
if drives_redirect: if drives_redirect:
actions = ActionChoices.choices_to_value(token.actions) if ActionChoices.contains(token.actions, ActionChoices.transfer()):
if actions & Action.TRANSFER == Action.TRANSFER:
rdp_options['drivestoredirect:s'] = '*' rdp_options['drivestoredirect:s'] = '*'
# 设置全屏 # 设置全屏
@ -181,22 +177,10 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
get_serializer: callable get_serializer: callable
perform_create: callable perform_create: callable
@action(methods=['POST'], detail=False, url_path='secret-info/detail')
def get_secret_detail(self, request, *args, **kwargs):
""" 非常重要的 api, 在逻辑层再判断一下 rbac 权限, 双重保险 """
rbac_perm = 'authentication.view_connectiontokensecret'
if not request.user.has_perm(rbac_perm):
raise PermissionDenied('Not allow to view secret')
token_id = request.data.get('token') or ''
token = get_object_or_404(ConnectionToken, pk=token_id)
self.check_token_permission(token)
serializer = self.get_serializer(instance=token)
return Response(serializer.data, status=status.HTTP_200_OK)
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file') @action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
def get_rdp_file(self, request, *args, **kwargs): def get_rdp_file(self, request, *args, **kwargs):
token = self.create_connection_token() token = self.create_connection_token()
self.check_token_permission(token) token.is_valid()
filename, content = self.get_rdp_file_info(token) filename, content = self.get_rdp_file_info(token)
filename = '{}.rdp'.format(filename) filename = '{}.rdp'.format(filename)
response = HttpResponse(content, content_type='application/octet-stream') response = HttpResponse(content, content_type='application/octet-stream')
@ -206,7 +190,7 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
@action(methods=['POST', 'GET'], detail=False, url_path='client-url') @action(methods=['POST', 'GET'], detail=False, url_path='client-url')
def get_client_protocol_url(self, request, *args, **kwargs): def get_client_protocol_url(self, request, *args, **kwargs):
token = self.create_connection_token() token = self.create_connection_token()
self.check_token_permission(token) token.is_valid()
try: try:
protocol_data = self.get_client_protocol_data(token) protocol_data = self.get_client_protocol_data(token)
except ValueError as e: except ValueError as e:
@ -224,12 +208,6 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
instance.expire() instance.expire()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@staticmethod
def check_token_permission(token: ConnectionToken):
is_valid, error = token.check_permission()
if not is_valid:
raise PermissionDenied(error)
def create_connection_token(self): def create_connection_token(self):
data = self.request.query_params if self.request.method == 'GET' else self.request.data data = self.request.query_params if self.request.method == 'GET' else self.request.data
serializer = self.get_serializer(data=data) serializer = self.get_serializer(data=data)
@ -259,6 +237,18 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
'get_client_protocol_url': 'authentication.add_connectiontoken', 'get_client_protocol_url': 'authentication.add_connectiontoken',
} }
@action(methods=['POST'], detail=False, url_path='secret')
def get_secret_detail(self, request, *args, **kwargs):
""" 非常重要的 api, 在逻辑层再判断一下 rbac 权限, 双重保险 """
rbac_perm = 'authentication.view_connectiontokensecret'
if not request.user.has_perm(rbac_perm):
raise PermissionDenied('Not allow to view secret')
token_id = request.data.get('token') or ''
token = get_object_or_404(ConnectionToken, pk=token_id)
token.is_valid()
serializer = self.get_serializer(instance=token)
return Response(serializer.data, status=status.HTTP_200_OK)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
with tmp_to_root_org(): with tmp_to_root_org():
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@ -296,9 +286,9 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
raise PermissionDenied('Expired') raise PermissionDenied('Expired')
if permed_account.has_secret: if permed_account.has_secret:
serializer.validated_data['secret'] = '' data['secret'] = ''
if permed_account.username != '@INPUT': if permed_account.username != '@INPUT':
serializer.validated_data['username'] = '' data['username'] = ''
return permed_account return permed_account

View File

@ -16,6 +16,11 @@ class Migration(migrations.Migration):
old_name='account_username', old_name='account_username',
new_name='login' new_name='login'
), ),
migrations.AlterField(
model_name='connectiontoken',
name='login',
field=models.CharField(max_length=128, verbose_name='Login account'),
),
migrations.AddField( migrations.AddField(
model_name='connectiontoken', model_name='connectiontoken',
name='username', name='username',

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.14 on 2022-11-23 02:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0014_auto_20221122_2152'),
]
operations = [
migrations.AlterField(
model_name='connectiontoken',
name='login',
field=models.CharField(max_length=128, verbose_name='Login account'),
),
]

View File

@ -46,10 +46,6 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
('view_connectiontokensecret', _('Can view connection token secret')) ('view_connectiontokensecret', _('Can view connection token secret'))
] ]
@property
def is_valid(self):
return not self.is_expired
@property @property
def is_expired(self): def is_expired(self):
return self.date_expired < timezone.now() return self.date_expired < timezone.now()
@ -76,69 +72,71 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
self.date_expired = date_expired_default() self.date_expired = date_expired_default()
self.save() self.save()
# actions 和 expired_at 在 check_valid() 中赋值 @lazyproperty
actions = expire_at = None def permed_account(self):
from perms.utils import PermAccountUtil
permed_account = PermAccountUtil().validate_permission(
self.user, self.asset, self.login
)
return permed_account
def check_permission(self): @lazyproperty
from perms.utils.account import PermAccountUtil def actions(self):
return self.permed_account.actions
@lazyproperty
def expire_at(self):
return self.permed_account.date_expired.timestamp()
def is_valid(self):
if self.is_expired: if self.is_expired:
is_valid = False
error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired)) error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired))
return is_valid, error raise PermissionDenied(error)
if not self.user or not self.user.is_valid: if not self.user or not self.user.is_valid:
is_valid = False
error = _('No user or invalid user') error = _('No user or invalid user')
return is_valid, error raise PermissionDenied(error)
if not self.asset or not self.asset.is_active: if not self.asset or not self.asset.is_active:
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.login: if not self.login:
is_valid = False
error = _('No account') error = _('No account')
return is_valid, error raise PermissionDenied(error)
permed_account = PermAccountUtil().validate_permission( if not self.permed_account or not self.permed_account.actions:
self.user, self.asset, self.login
)
if not permed_account or not permed_account.actions:
msg = 'user `{}` not has asset `{}` permission for login `{}`'.format( msg = 'user `{}` not has asset `{}` permission for login `{}`'.format(
self.user, self.asset, self.login self.user, self.asset, self.login
) )
raise PermissionDenied(msg) raise PermissionDenied(msg)
if permed_account.date_expired < timezone.now(): if self.permed_account.date_expired < timezone.now():
raise PermissionDenied('Expired') raise PermissionDenied('Expired')
return True
is_valid, error = True, ''
return is_valid, error
@lazyproperty @lazyproperty
def platform(self): def platform(self):
return self.asset.platform return self.asset.platform
@lazyproperty @lazyproperty
def accounts(self): def account(self):
if not self.asset: if not self.asset:
return None return None
data = [] account = self.asset.accounts.filter(name=self.login).first()
if self.login == '@INPUT': if self.login == '@INPUT' or not account:
data.append({ return {
'name': self.login, 'name': self.login,
'username': self.username, 'username': self.username,
'secret_type': 'password', 'secret_type': 'password',
'secret': self.secret 'secret': self.secret
}) }
else: else:
accounts = self.asset.accounts.filter(username=self.login) return {
for account in accounts: 'name': account.name,
data.append({ 'username': account.username,
'username': account.uesrname, 'secret_type': account.secret_type,
'secret_type': account.secret_type, 'secret': account.secret_type or self.secret
'secret': account.secret if account.secret else self.secret }
})
return data
@lazyproperty @lazyproperty
def domain(self): def domain(self):

View File

@ -17,7 +17,6 @@ __all__ = [
class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
username = serializers.CharField(max_length=128, label=_("Input username"), username = serializers.CharField(max_length=128, label=_("Input username"),
allow_null=True, allow_blank=True) 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')) expire_time = serializers.IntegerField(read_only=True, label=_('Expired time'))
class Meta: class Meta:
@ -25,7 +24,7 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
fields_mini = ['id'] fields_mini = ['id']
fields_small = fields_mini + [ fields_small = fields_mini + [
'protocol', 'login', 'secret', 'username', 'protocol', 'login', 'secret', 'username',
'date_expired', 'date_created', 'actions', 'date_expired', 'date_created',
'date_updated', 'created_by', 'date_updated', 'created_by',
'updated_by', 'org_id', 'org_name', 'updated_by', 'org_id', 'org_name',
] ]
@ -34,7 +33,7 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
] ]
read_only_fields = [ read_only_fields = [
# 普通 Token 不支持指定 user # 普通 Token 不支持指定 user
'user', 'is_valid', 'expire_time', 'user', 'expire_time',
'user_display', 'asset_display', 'user_display', 'asset_display',
] ]
fields = fields_small + fields_fk + read_only_fields fields = fields_small + fields_fk + read_only_fields
@ -98,7 +97,7 @@ class ConnectionTokenAccountSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Account model = Account
fields = [ fields = [
'username', 'secret_type', 'secret', 'name', 'username', 'secret_type', 'secret',
] ]
@ -144,7 +143,7 @@ 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)
accounts = ConnectionTokenAccountSerializer(read_only=True, many=True) account = ConnectionTokenAccountSerializer(read_only=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()
@ -153,8 +152,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
class Meta: class Meta:
model = ConnectionToken model = ConnectionToken
fields = [ fields = [
'id', 'secret', 'user', 'asset', 'login', 'id', 'secret', 'user', 'asset', 'account',
'accounts', 'protocol', 'domain', 'gateway', 'protocol', 'domain', 'gateway',
'actions', 'expire_at', 'actions', 'expire_at', 'platform',
'platform',
] ]

View File

@ -28,8 +28,16 @@ class ActionChoices(BitChoices):
) )
@classmethod @classmethod
def has_perm(cls, action_name, total): def transfer(cls):
action_value = getattr(cls, action_name) return cls.upload | cls.download
@classmethod
def clipboard(cls):
return cls.copy | cls.paste
@classmethod
def contains(cls, total, action):
action_value = getattr(cls, action)
return action_value & total == action_value return action_value & total == action_value
@classmethod @classmethod