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):
pass
@classmethod
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.utils import get_logger, lazyproperty
from orgs.mixins.models import OrgModelMixin
from assets.models import Host
from .base import BaseAccount
from ..const import SecretType, GATEWAY_NAME
from ..const import SecretType
logger = get_logger(__file__)
@ -37,7 +38,7 @@ class Domain(OrgModelMixin):
@lazyproperty
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):
return self.random_gateway()

View File

@ -1,28 +1,33 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from rest_framework.generics import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
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):
asset_count = serializers.SerializerMethodField(label=_('Assets amount'))
gateway_count = serializers.SerializerMethodField(label=_('Gateways count'))
assets = ObjectRelatedField(
many=True, required=False, queryset=Asset.objects, label=_('Asset')
)
class Meta:
model = Domain
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'comment', 'date_created'
]
fields_m2m = [
'asset_count', 'assets', 'gateway_count',
]
fields = fields_small + fields_m2m
read_only_fields = ('asset_count', 'gateway_count', 'date_created')
fields_small = fields_mini + ['comment']
fields_m2m = ['assets']
read_only_fields = ['asset_count', 'gateway_count', 'date_created']
fields = fields_small + fields_m2m + read_only_fields
extra_kwargs = {
'assets': {'required': False, 'label': _('Assets')},
}
@ -36,20 +41,86 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
return obj.gateways.count()
class GatewaySerializer(BulkOrgResourceModelSerializer):
is_connective = serializers.BooleanField(required=False, label=_('Connectivity'))
class GatewaySerializer(HostSerializer):
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:
model = Asset
fields_mini = ['id']
fields_small = fields_mini + [
'address', 'port', 'protocol',
'is_active', 'is_connective',
'date_created', 'date_updated',
'created_by', 'comment',
class Meta(HostSerializer.Meta):
fields = HostSerializer.Meta.fields + [
'username', 'password', 'private_key', 'passphrase'
]
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):

View File

@ -28,9 +28,6 @@ from ..serializers import (
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
# ExtraActionApiMixin
class RDPFileClientProtocolURLMixin:
request: Request
get_serializer: callable
@ -72,8 +69,7 @@ class RDPFileClientProtocolURLMixin:
# 设置磁盘挂载
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
if drives_redirect:
actions = ActionChoices.choices_to_value(token.actions)
if actions & Action.TRANSFER == Action.TRANSFER:
if ActionChoices.contains(token.actions, ActionChoices.transfer()):
rdp_options['drivestoredirect:s'] = '*'
# 设置全屏
@ -181,22 +177,10 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
get_serializer: 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')
def get_rdp_file(self, request, *args, **kwargs):
token = self.create_connection_token()
self.check_token_permission(token)
token.is_valid()
filename, content = self.get_rdp_file_info(token)
filename = '{}.rdp'.format(filename)
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')
def get_client_protocol_url(self, request, *args, **kwargs):
token = self.create_connection_token()
self.check_token_permission(token)
token.is_valid()
try:
protocol_data = self.get_client_protocol_data(token)
except ValueError as e:
@ -224,12 +208,6 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
instance.expire()
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):
data = self.request.query_params if self.request.method == 'GET' else self.request.data
serializer = self.get_serializer(data=data)
@ -259,6 +237,18 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
'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):
with tmp_to_root_org():
return super().dispatch(request, *args, **kwargs)
@ -296,9 +286,9 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
raise PermissionDenied('Expired')
if permed_account.has_secret:
serializer.validated_data['secret'] = ''
data['secret'] = ''
if permed_account.username != '@INPUT':
serializer.validated_data['username'] = ''
data['username'] = ''
return permed_account

View File

@ -16,6 +16,11 @@ class Migration(migrations.Migration):
old_name='account_username',
new_name='login'
),
migrations.AlterField(
model_name='connectiontoken',
name='login',
field=models.CharField(max_length=128, verbose_name='Login account'),
),
migrations.AddField(
model_name='connectiontoken',
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'))
]
@property
def is_valid(self):
return not self.is_expired
@property
def is_expired(self):
return self.date_expired < timezone.now()
@ -76,69 +72,71 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
self.date_expired = date_expired_default()
self.save()
# actions 和 expired_at 在 check_valid() 中赋值
actions = expire_at = None
@lazyproperty
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):
from perms.utils.account import PermAccountUtil
@lazyproperty
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:
is_valid = False
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:
is_valid = False
error = _('No user or invalid user')
return is_valid, error
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
if not self.login:
is_valid = False
error = _('No account')
return is_valid, error
raise PermissionDenied(error)
permed_account = PermAccountUtil().validate_permission(
self.user, self.asset, self.login
)
if not permed_account or not permed_account.actions:
if not self.permed_account or not self.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():
if self.permed_account.date_expired < timezone.now():
raise PermissionDenied('Expired')
is_valid, error = True, ''
return is_valid, error
return True
@lazyproperty
def platform(self):
return self.asset.platform
@lazyproperty
def accounts(self):
def account(self):
if not self.asset:
return None
data = []
if self.login == '@INPUT':
data.append({
account = self.asset.accounts.filter(name=self.login).first()
if self.login == '@INPUT' or not account:
return {
'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
return {
'name': account.name,
'username': account.username,
'secret_type': account.secret_type,
'secret': account.secret_type or self.secret
}
@lazyproperty
def domain(self):

View File

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

View File

@ -28,8 +28,16 @@ class ActionChoices(BitChoices):
)
@classmethod
def has_perm(cls, action_name, total):
action_value = getattr(cls, action_name)
def transfer(cls):
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
@classmethod