feat: 为rdp 添加一个api

pull/5660/head
ibuler 2021-02-23 14:37:42 +08:00 committed by 老广
parent 9be3cbb936
commit bb9790a50f
12 changed files with 290 additions and 91 deletions

View File

@ -3,6 +3,7 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin
from common.mixins import CommonModelMixin
from assets.models import Asset
from .. import const
@ -35,3 +36,35 @@ class Application(CommonModelMixin, OrgModelMixin):
@property
def category_remote_app(self):
return self.category == const.ApplicationCategoryChoices.remote_app.value
def get_rdp_remote_app_setting(self):
from applications.serializers.attrs import get_serializer_class_by_application_type
if not self.category_remote_app:
raise ValueError(f"Not a remote app application: {self.name}")
serializer_class = get_serializer_class_by_application_type(self.type)
fields = serializer_class().get_fields()
parameters = [self.type]
for field_name in list(fields.keys()):
if field_name in ['asset']:
continue
value = self.attrs.get(field_name)
if not value:
continue
if field_name == 'path':
value = '\"%s\"' % value
parameters.append(str(value))
parameters = ' '.join(parameters)
return {
'program': '||jmservisor',
'working_directory': '',
'parameters': parameters
}
def get_remote_app_asset(self):
asset_id = self.attrs.get('asset')
if not asset_id:
raise ValueError("Remote App not has asset attr")
asset = Asset.objects.filter(id=asset_id).first()
return asset

View File

@ -27,31 +27,5 @@ class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer):
return obj.attrs.get('asset')
@staticmethod
def get_parameters(obj):
"""
返回Guacamole需要的RemoteApp配置参数信息中的parameters参数
"""
from .attrs import get_serializer_class_by_application_type
serializer_class = get_serializer_class_by_application_type(obj.type)
fields = serializer_class().get_fields()
parameters = [obj.type]
for field_name in list(fields.keys()):
if field_name in ['asset']:
continue
value = obj.attrs.get(field_name)
if not value:
continue
if field_name == 'path':
value = '\"%s\"' % value
parameters.append(str(value))
parameters = ' '.join(parameters)
return parameters
def get_parameter_remote_app(self, obj):
return {
'program': '||jmservisor',
'working_directory': '',
'parameters': self.get_parameters(obj)
}
def get_parameter_remote_app(obj):
return obj.get_rdp_remote_app_setting()

View File

@ -77,8 +77,6 @@ class GatewayWithAuthSerializer(GatewaySerializer):
return fields
class DomainWithGatewaySerializer(BulkOrgResourceModelSerializer):
gateways = GatewayWithAuthSerializer(many=True, read_only=True)

View File

@ -1,40 +1,73 @@
# -*- coding: utf-8 -*-
#
import uuid
from django.conf import settings
from django.core.cache import cache
from django.shortcuts import get_object_or_404
from rest_framework.serializers import ValidationError
from rest_framework.response import Response
from rest_framework.generics import CreateAPIView
from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from common.utils import get_logger, random_string
from common.drf.api import SerializerMixin2
from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser
from common.utils import get_logger
from common.permissions import IsSuperUserOrAppUser
from orgs.mixins.api import RootOrgViewMixin
from ..serializers import ConnectionTokenSerializer
from ..serializers import ConnectionTokenSerializer, ConnectionTokenSecretSerializer
logger = get_logger(__name__)
__all__ = ['UserConnectionTokenApi']
__all__ = ['UserConnectionTokenViewSet']
class UserConnectionTokenApi(RootOrgViewMixin, CreateAPIView):
class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericViewSet):
permission_classes = (IsSuperUserOrAppUser,)
serializer_class = ConnectionTokenSerializer
serializer_classes = {
'default': ConnectionTokenSerializer,
'get_secret_detail': ConnectionTokenSecretSerializer
}
CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}'
def perform_create(self, serializer):
user = serializer.validated_data['user']
asset = serializer.validated_data['asset']
system_user = serializer.validated_data['system_user']
token = str(uuid.uuid4())
@staticmethod
def check_resource_permission(user, asset, application, system_user):
from perms.utils.asset import has_asset_system_permission
from perms.utils.application import has_application_system_permission
if asset and not has_asset_system_permission(user, asset, system_user):
error = f'User not has this asset and system user permission: ' \
f'user={user.id} system_user={system_user.id} asset={asset.id}'
raise PermissionDenied(error)
if application and not has_application_system_permission(user, application, system_user):
error = f'User not has this application and system user permission: ' \
f'user={user.id} system_user={system_user.id} application={application.id}'
raise PermissionDenied(error)
return True
def create_token(self, user, asset, application, system_user):
self.check_resource_permission(user, asset, application, system_user)
token = random_string(36)
value = {
'user': str(user.id),
'username': user.username,
'asset': str(asset.id),
'hostname': asset.hostname,
'system_user': str(system_user.id),
'system_user_name': system_user.name
}
cache.set(token, value, timeout=20)
if asset:
value.update({
'type': 'asset',
'asset': str(asset.id),
'hostname': asset.hostname,
})
elif application:
value.update({
'type': 'application',
'application': application.id,
'application_name': str(application)
})
key = self.CACHE_KEY_PREFIX.format(token)
cache.set(key, value, timeout=20)
return token
def create(self, request, *args, **kwargs):
@ -42,18 +75,107 @@ class UserConnectionTokenApi(RootOrgViewMixin, CreateAPIView):
data = {'error': 'Connection token disabled'}
return Response(data, status=400)
if not request.user.is_superuser:
data = {'error': 'Only super user can create token'}
return Response(data, status=403)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
token = self.perform_create(serializer)
user = serializer.validated_data.get('user', None)
if not request.user.is_superuser and user:
raise PermissionDenied('Only super user can create user token')
if not request.user.is_superuser:
user = self.request.user
asset = serializer.validated_data.get('asset')
application = serializer.validated_data.get('application')
system_user = serializer.validated_data['system_user']
token = self.create_token(user, asset, application, system_user)
return Response({"token": token}, status=201)
@staticmethod
def _get_application_secret_detail(value):
from applications.models import Application
from perms.models import Action
application = get_object_or_404(Application, id=value.get('application'))
gateway = None
if not application.category_remote_app:
actions = Action.NONE
remote_app = {}
asset = None
domain = application.domain
else:
remote_app = application.get_rdp_remote_app_setting()
actions = Action.CONNECT
asset = application.get_remote_app_asset()
domain = asset.domain
if domain and domain.has_gateway():
gateway = domain.random_gateway()
return {
'asset': asset,
'application': application,
'gateway': gateway,
'remote_app': remote_app,
'actions': actions
}
@staticmethod
def _get_asset_secret_detail(value, user, system_user):
from assets.models import Asset
from perms.utils.asset import get_asset_system_users_id_with_actions_by_user
asset = get_object_or_404(Asset, id=value.get('asset'))
systemuserid_actions_mapper = get_asset_system_users_id_with_actions_by_user(user, asset)
actions = systemuserid_actions_mapper.get(system_user.id, [])
gateway = None
if asset and asset.domain and asset.domain.has_gateway():
gateway = asset.domain.random_gateway()
return {
'asset': asset,
'application': None,
'gateway': gateway,
'remote_app': None,
'actions': actions,
}
@action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail')
def get_secret_detail(self, request, *args, **kwargs):
from users.models import User
from assets.models import SystemUser
token = request.data.get('token', '')
key = self.CACHE_KEY_PREFIX.format(token)
value = cache.get(key, None)
if not value:
return Response(status=404)
user = get_object_or_404(User, id=value.get('user'))
system_user = get_object_or_404(SystemUser, id=value.get('system_user'))
data = dict(user=user, system_user=system_user)
if value.get('type') == 'asset':
asset_detail = self._get_asset_secret_detail(value, user=user, system_user=system_user)
data['type'] = 'asset'
data.update(asset_detail)
else:
app_detail = self._get_application_secret_detail(value)
data['type'] = 'application'
data.update(app_detail)
serializer = self.get_serializer(data)
return Response(data=serializer.data, status=200)
def get_permissions(self):
if self.action == "create":
if self.request.data.get('user', None):
self.permission_classes = (IsSuperUser,)
else:
self.permission_classes = (IsValidUser,)
return super().get_permissions()
def get(self, request):
token = request.query_params.get('token')
value = cache.get(token, None)
key = self.CACHE_KEY_PREFIX.format(token)
value = cache.get(key, None)
if not value:
return Response('', status=404)

View File

@ -4,14 +4,17 @@ from rest_framework import serializers
from common.utils import get_object_or_none
from users.models import User
from assets.models import Asset, SystemUser, Gateway
from applications.models import Application
from users.serializers import UserProfileSerializer
from perms.serializers.asset.permission import ActionsField
from .models import AccessKey, LoginConfirmSetting, SSOToken
__all__ = [
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer',
'ConnectionTokenSerializer',
'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer'
]
@ -86,9 +89,10 @@ class SSOTokenSerializer(serializers.Serializer):
class ConnectionTokenSerializer(serializers.Serializer):
user = serializers.CharField(max_length=128, required=True)
user = serializers.CharField(max_length=128, required=False, allow_blank=True)
system_user = serializers.CharField(max_length=128, required=True)
asset = serializers.CharField(max_length=128, required=True)
asset = serializers.CharField(max_length=128, required=False)
application = serializers.CharField(max_length=128, required=False)
@staticmethod
def validate_user(user_id):
@ -113,3 +117,67 @@ class ConnectionTokenSerializer(serializers.Serializer):
if asset is None:
raise serializers.ValidationError('asset id not exist')
return asset
@staticmethod
def validate_application(app_id):
from applications.models import Application
app = Application.objects.filter(id=app_id).first()
if app is None:
raise serializers.ValidationError('app id not exist')
return app
def validate(self, attrs):
asset = attrs.get('asset')
application = attrs.get('application')
if not asset and not application:
raise serializers.ValidationError('asset or application required')
if asset and application:
raise serializers.ValidationError('asset and application should only one')
return super().validate(attrs)
class ConnectionTokenUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'name', 'username', 'email']
class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
class Meta:
model = Asset
fields = ['id', 'hostname', 'ip', 'port', 'org_id']
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
class Meta:
model = SystemUser
fields = ['id', 'name', 'username', 'password', 'private_key']
class ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
class Meta:
model = Gateway
fields = ['id', 'ip', 'port', 'username', 'password', 'private_key']
class ConnectionTokenRemoteAppSerializer(serializers.Serializer):
program = serializers.CharField()
working_directory = serializers.CharField()
parameters = serializers.CharField()
class ConnectionTokenApplicationSerializer(serializers.ModelSerializer):
class Meta:
model = Application
fields = ['id', 'name', 'category', 'type']
class ConnectionTokenSecretSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=[('application', 'Application'), ('asset', 'Asset')])
user = ConnectionTokenUserSerializer(read_only=True)
asset = ConnectionTokenAssetSerializer(read_only=True)
remote_app = ConnectionTokenRemoteAppSerializer(read_only=True)
application = ConnectionTokenApplicationSerializer(read_only=True)
system_user = ConnectionTokenSystemUserSerializer(read_only=True)
gateway = ConnectionTokenGatewaySerializer(read_only=True)
actions = ActionsField()

View File

@ -9,6 +9,7 @@ app_name = 'authentication'
router = DefaultRouter()
router.register('access-keys', api.AccessKeyViewSet, 'access-key')
router.register('sso', api.SSOViewSet, 'sso')
router.register('connection-token', api.UserConnectionTokenViewSet, 'connection-token')
urlpatterns = [
@ -16,8 +17,6 @@ urlpatterns = [
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'),
path('connection-token/',
api.UserConnectionTokenApi.as_view(), name='connection-token'),
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'),
path('login-confirm-settings/<uuid:user_id>/', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update')

View File

@ -64,7 +64,7 @@ msgstr "名称"
#: perms/serializers/application/user_permission.py:33
#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:20
msgid "Category"
msgstr "类"
msgstr "类"
#: applications/models/application.py:15
#: applications/serializers/application.py:48 assets/models/cmd_filter.py:52
@ -3160,7 +3160,7 @@ msgstr "关闭"
#: tickets/handler/apply_application.py:55
msgid "Applied category"
msgstr "申请的类"
msgstr "申请的类"
#: tickets/handler/apply_application.py:56
msgid "Applied type"
@ -3323,7 +3323,7 @@ msgstr "受理人 (显示名称)"
#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:24
msgid "Category display"
msgstr "类 (显示名称)"
msgstr "类 (显示名称)"
#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:31
#: tickets/serializers/ticket/ticket.py:19

View File

@ -11,6 +11,7 @@ from rest_framework.generics import (
from orgs.utils import tmp_to_root_org
from applications.models import Application
from perms.utils.application.permission import (
has_application_system_permission,
get_application_system_users_id
)
from perms.api.asset.user_permission.mixin import RoleAdminMixin, RoleUserMixin
@ -71,8 +72,7 @@ class ValidateUserApplicationPermissionApi(APIView):
application = get_object_or_404(Application, id=application_id)
system_user = get_object_or_404(SystemUser, id=system_user_id)
system_users_id = get_application_system_users_id(user, application)
if system_user.id in system_users_id:
if has_application_system_permission(user, application, system_user):
return Response({'msg': True}, status=200)
return Response({'msg': False}, status=403)

View File

@ -128,12 +128,10 @@ def on_asset_permission_user_groups_changed(instance, action, pk_set, model,
@receiver(m2m_changed, sender=ApplicationPermission.system_users.through)
def on_application_permission_system_users_changed(sender, instance: ApplicationPermission, action, reverse, pk_set, **kwargs):
if not instance.category_remote_app:
return
if reverse:
raise M2MReverseNotAllowed
if not instance.category_remote_app:
return
if action != POST_ADD:
return
@ -156,12 +154,12 @@ def on_application_permission_system_users_changed(sender, instance: Application
@receiver(m2m_changed, sender=ApplicationPermission.users.through)
def on_application_permission_users_changed(sender, instance, action, reverse, pk_set, **kwargs):
if not instance.category_remote_app:
return
if reverse:
raise M2MReverseNotAllowed
if not instance.category_remote_app:
return
if action != POST_ADD:
return
@ -176,12 +174,10 @@ def on_application_permission_users_changed(sender, instance, action, reverse, p
@receiver(m2m_changed, sender=ApplicationPermission.user_groups.through)
def on_application_permission_user_groups_changed(sender, instance, action, reverse, pk_set, **kwargs):
if not instance.category_remote_app:
return
if reverse:
raise M2MReverseNotAllowed
if not instance.category_remote_app:
return
if action != POST_ADD:
return
@ -196,12 +192,12 @@ def on_application_permission_user_groups_changed(sender, instance, action, reve
@receiver(m2m_changed, sender=ApplicationPermission.applications.through)
def on_application_permission_applications_changed(sender, instance, action, reverse, pk_set, **kwargs):
if not instance.category_remote_app:
return
if reverse:
raise M2MReverseNotAllowed
if not instance.category_remote_app:
return
if action != POST_ADD:
return

View File

@ -7,8 +7,14 @@ logger = get_logger(__file__)
def get_application_system_users_id(user, application):
queryset = ApplicationPermission.objects\
.filter(Q(users=user) | Q(user_groups__users=user), Q(applications=application))\
.valid()\
.values_list('system_users', flat=True)
queryset = ApplicationPermission.objects.valid()\
.filter(
Q(users=user) | Q(user_groups__users=user),
Q(applications=application)
).values_list('system_users', flat=True)
return queryset
def has_application_system_permission(user, application, system_user):
system_users_id = get_application_system_users_id(user, application)
return system_user.id in system_users_id

View File

@ -4,7 +4,7 @@ from django.db.models import Q
from common.utils import get_logger
from perms.models import AssetPermission
from perms.hands import Asset, User, UserGroup
from perms.hands import Asset, User, UserGroup, SystemUser
from perms.models.base import BasePermissionQuerySet
logger = get_logger(__file__)
@ -19,10 +19,8 @@ def get_asset_system_users_id_with_actions(asset_perm_queryset: BasePermissionQu
ancestor_keys = node.get_ancestor_keys(with_self=True)
node_keys.update(ancestor_keys)
queryset = AssetPermission.objects.filter(id__in=asset_perms_id).filter(
Q(assets=asset) |
Q(nodes__key__in=node_keys)
)
queryset = AssetPermission.objects.filter(id__in=asset_perms_id)\
.filter(Q(assets=asset) | Q(nodes__key__in=node_keys))
asset_protocols = asset.protocols_as_dict.keys()
values = queryset.filter(
@ -44,8 +42,14 @@ def get_asset_system_users_id_with_actions_by_user(user: User, asset: Asset):
return get_asset_system_users_id_with_actions(queryset, asset)
def has_asset_system_permission(user: User, asset: Asset, system_user: SystemUser):
systemuser_actions_mapper = get_asset_system_users_id_with_actions_by_user(user, asset)
actions = systemuser_actions_mapper.get(system_user.id, [])
if actions:
return True
return False
def get_asset_system_users_id_with_actions_by_group(group: UserGroup, asset: Asset):
queryset = AssetPermission.objects.filter(
user_groups=group
).valid()
queryset = AssetPermission.objects.filter(user_groups=group).valid()
return get_asset_system_users_id_with_actions(queryset, asset)

View File

@ -16,11 +16,10 @@ router.register(r'users', api.UserViewSet, 'user')
router.register(r'groups', api.UserGroupViewSet, 'user-group')
router.register(r'users-groups-relations', api.UserUserGroupRelationViewSet, 'users-groups-relation')
router.register(r'service-account-registrations', api.ServiceAccountRegistrationViewSet, 'service-account-registration')
router.register(r'connection-token', auth_api.UserConnectionTokenViewSet, 'connection-token')
urlpatterns = [
path('connection-token/', auth_api.UserConnectionTokenApi.as_view(),
name='connection-token'),
path('profile/', api.UserProfileApi.as_view(), name='user-profile'),
path('profile/password/', api.UserPasswordApi.as_view(), name='user-password'),
path('profile/public-key/', api.UserPublicKeyApi.as_view(), name='user-public-key'),