diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index b3b4abdad..57c41d03e 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- # import urllib.parse +import json import base64 +from typing import Callable from django.conf import settings from django.core.cache import cache @@ -9,6 +11,7 @@ from django.shortcuts import get_object_or_404 from django.http import HttpResponse from django.utils.translation import ugettext as _ from rest_framework.response import Response +from rest_framework.request import Request from rest_framework.viewsets import GenericViewSet from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied @@ -18,71 +21,32 @@ from authentication.signals import post_auth_failed, post_auth_success from common.utils import get_logger, random_string from common.drf.api import SerializerMixin from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser - from orgs.mixins.api import RootOrgViewMixin from ..serializers import ( ConnectionTokenSerializer, ConnectionTokenSecretSerializer, - RDPFileSerializer ) logger = get_logger(__name__) __all__ = ['UserConnectionTokenViewSet'] -class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin, GenericViewSet): - permission_classes = (IsSuperUserOrAppUser,) - serializer_classes = { - 'default': ConnectionTokenSerializer, - 'get_secret_detail': ConnectionTokenSecretSerializer, - 'get_rdp_file': RDPFileSerializer - } - CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}' +class ClientProtocolMixin: + request: Request + get_serializer: Callable + create_token: Callable - @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 get_request_resource(self, serializer): + asset = serializer.validated_data.get('asset') + application = serializer.validated_data.get('application') + system_user = serializer.validated_data['system_user'] - def create_token(self, user, asset, application, system_user, ttl=5 * 60): - if not self.request.user.is_superuser and user != self.request.user: - raise PermissionDenied('Only super user can create user token') - self.check_resource_permission(user, asset, application, system_user) - token = random_string(36) - value = { - 'user': str(user.id), - 'username': user.username, - 'system_user': str(system_user.id), - 'system_user_name': system_user.name - } + user = serializer.validated_data.get('user') + if not user or not self.request.user.is_superuser: + user = self.request.user + return asset, application, system_user, user - 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=ttl) - return token - - def create_rdp_file(self): + def get_rdp_file_content(self, serializer): options = { 'full address:s': '', 'username:s': '', @@ -114,24 +78,15 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin, GenericViewS # 'remoteapplicationname:s': 'Firefox', # 'remoteapplicationcmdline:s': '', } - if self.request.method == 'GET': - data = self.request.query_params - else: - data = request.data - serializer = self.get_serializer(data=data) - serializer.is_valid(raise_exception=True) - asset = serializer.validated_data.get('asset') - application = serializer.validated_data.get('application') - system_user = serializer.validated_data['system_user'] - height = serializer.validated_data.get('height') - width = serializer.validated_data.get('width') - user = self.request.user + asset, application, system_user, user = self.get_request_resource(serializer) + height = self.request.query_params.get('height') + width = self.request.query_params.get('width') token = self.create_token(user, asset, application, system_user) address = settings.TERMINAL_RDP_ADDR if not address or address == 'localhost:3389': - address = request.get_host().split(':')[0] + ':3389' + address = self.request.get_host().split(':')[0] + ':3389' options['full address:s'] = address options['username:s'] = '{}|{}'.format(user.username, token) if system_user.ad_domain: @@ -152,30 +107,61 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin, GenericViewS name = '*' return name, content - def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - - asset = serializer.validated_data.get('asset') - application = serializer.validated_data.get('application') - system_user = serializer.validated_data['system_user'] - user = serializer.validated_data.get('user') - token = self.create_token(user, asset, application, system_user) - return Response({"token": token}, status=201) - @action(methods=['POST', 'GET'], detail=False, url_path='rdp/file', permission_classes=[IsValidUser]) def get_rdp_file(self, request, *args, **kwargs): - name, data = self.create_rdp_file() + if self.request.method == 'GET': + data = self.request.query_params + else: + data = self.request.data + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + name, data = self.get_rdp_file_content(serializer) response = HttpResponse(data, content_type='application/octet-stream') filename = "{}-{}-jumpserver.rdp".format(self.request.user.username, name) filename = urllib.parse.quote(filename) response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename return response - @action(methods=['POST', 'GET'], detail=False, url_path='rdp/rouse', permission_classes=[IsValidUser]) - def get_rdp_rouse(self, request, *args, **kwargs): - data = self.create_rdp_file()[1] - return Response(data=dict(data=base64.b64encode(data.encode()))) + def get_valid_serializer(self): + if self.request.method == 'GET': + data = self.request.query_params + else: + data = self.request.data + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + return serializer + + def get_client_protocol_data(self, serializer): + asset, application, system_user, user = self.get_request_resource(serializer) + protocol = system_user.protocol + if protocol == 'rdp': + name, config = self.get_rdp_file_content(serializer) + elif protocol == 'vnc': + raise HttpResponse(status=404, data={"error": "VNC not support"}) + else: + config = 'ssh://system_user@asset@user@jumpserver-ssh' + data = { + "protocol": system_user.protocol, + "username": user.username, + "config": config + } + return data + + @action(methods=['POST', 'GET'], detail=False, url_path='client-url', permission_classes=[IsValidUser]) + def get_client_protocol_url(self, request, *args, **kwargs): + serializer = self.get_valid_serializer() + protocol_data = self.get_client_protocol_data(serializer) + protocol_data = base64.b64encode(json.dumps(protocol_data).encode()).decode() + data = { + 'url': 'jms://{}'.format(protocol_data), + } + return Response(data=data) + + +class SecretDetailMixin: + valid_token: Callable + request: Request + get_serializer: Callable @staticmethod def _get_application_secret_detail(application): @@ -222,6 +208,100 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin, GenericViewS 'actions': actions, } + @action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail') + def get_secret_detail(self, request, *args, **kwargs): + token = request.data.get('token', '') + try: + value, user, system_user, asset, app, expired_at = self.valid_token(token) + except serializers.ValidationError as e: + post_auth_failed.send( + sender=self.__class__, username='', request=self.request, + reason=_('Invalid token') + ) + raise e + + data = dict(user=user, system_user=system_user, expired_at=expired_at) + if asset: + asset_detail = self._get_asset_secret_detail(asset, user=user, system_user=system_user) + system_user.load_asset_more_auth(asset.id, user.username, user.id) + data['type'] = 'asset' + data.update(asset_detail) + else: + app_detail = self._get_application_secret_detail(app) + system_user.load_app_more_auth(app.id, user.id) + data['type'] = 'application' + data.update(app_detail) + + self.request.session['auth_backend'] = settings.AUTH_BACKEND_AUTH_TOKEN + post_auth_success.send(sender=self.__class__, user=user, request=self.request, login_type='T') + + serializer = self.get_serializer(data) + return Response(data=serializer.data, status=200) + + +class UserConnectionTokenViewSet( + RootOrgViewMixin, SerializerMixin, ClientProtocolMixin, + SecretDetailMixin, GenericViewSet +): + permission_classes = (IsSuperUserOrAppUser,) + serializer_classes = { + 'default': ConnectionTokenSerializer, + 'get_secret_detail': ConnectionTokenSecretSerializer, + } + CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}' + + @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, ttl=5 * 60): + if not self.request.user.is_superuser and user != self.request.user: + raise PermissionDenied('Only super user can create user token') + self.check_resource_permission(user, asset, application, system_user) + token = random_string(36) + value = { + 'user': str(user.id), + 'username': user.username, + 'system_user': str(system_user.id), + 'system_user_name': system_user.name + } + + 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=ttl) + return token + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + asset, application, system_user, user = self.get_request_resource(serializer) + token = self.create_token(user, asset, application, system_user) + return Response({"token": token}, status=201) + def valid_token(self, token): from users.models import User from assets.models import SystemUser, Asset @@ -254,39 +334,8 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin, GenericViewS if not has_perm: raise serializers.ValidationError('Permission expired or invalid') - return value, user, system_user, asset, app, expired_at - @action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail') - def get_secret_detail(self, request, *args, **kwargs): - token = request.data.get('token', '') - try: - value, user, system_user, asset, app, expired_at = self.valid_token(token) - except serializers.ValidationError as e: - post_auth_failed.send( - sender=self.__class__, username='', request=self.request, - reason=_('Invalid token') - ) - raise e - - data = dict(user=user, system_user=system_user, expired_at=expired_at) - if asset: - asset_detail = self._get_asset_secret_detail(asset, user=user, system_user=system_user) - system_user.load_asset_more_auth(asset.id, user.username, user.id) - data['type'] = 'asset' - data.update(asset_detail) - else: - app_detail = self._get_application_secret_detail(app) - system_user.load_app_more_auth(app.id, user.id) - data['type'] = 'application' - data.update(app_detail) - - self.request.session['auth_backend'] = settings.AUTH_BACKEND_AUTH_TOKEN - post_auth_success.send(sender=self.__class__, user=user, request=self.request, login_type='T') - - serializer = self.get_serializer(data) - return Response(data=serializer.data, status=200) - def get_permissions(self): if self.action in ["create", "get_rdp_file"]: if self.request.data.get('user', None): diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py index e6932388b..989d94d62 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -198,7 +198,3 @@ class ConnectionTokenSecretSerializer(serializers.Serializer): actions = ActionsField() expired_at = serializers.IntegerField() - -class RDPFileSerializer(ConnectionTokenSerializer): - width = serializers.IntegerField(allow_null=True, max_value=3112, min_value=100, required=False) - height = serializers.IntegerField(allow_null=True, max_value=4096, min_value=100, required=False)