From bb9790a50f9549383baeafd1891dad993a506e8a Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 23 Feb 2021 14:37:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BArdp=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=B8=80=E4=B8=AAapi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/applications/models/application.py | 33 ++++ apps/applications/serializers/remote_app.py | 30 +--- apps/assets/serializers/domain.py | 2 - apps/authentication/api/connection_token.py | 168 +++++++++++++++--- apps/authentication/serializers.py | 74 +++++++- apps/authentication/urls/api_urls.py | 3 +- apps/locale/zh/LC_MESSAGES/django.po | 6 +- .../api/application/user_permission/common.py | 4 +- apps/perms/signals_handler/common.py | 24 ++- apps/perms/utils/application/permission.py | 14 +- apps/perms/utils/asset/permission.py | 20 ++- apps/users/urls/api_urls.py | 3 +- 12 files changed, 290 insertions(+), 91 deletions(-) diff --git a/apps/applications/models/application.py b/apps/applications/models/application.py index 12bc4ff0a..f7b541580 100644 --- a/apps/applications/models/application.py +++ b/apps/applications/models/application.py @@ -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 diff --git a/apps/applications/serializers/remote_app.py b/apps/applications/serializers/remote_app.py index 13343ee53..d036a8c65 100644 --- a/apps/applications/serializers/remote_app.py +++ b/apps/applications/serializers/remote_app.py @@ -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() diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index 00df0b91d..f7b402f0e 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -77,8 +77,6 @@ class GatewayWithAuthSerializer(GatewaySerializer): return fields - - class DomainWithGatewaySerializer(BulkOrgResourceModelSerializer): gateways = GatewayWithAuthSerializer(many=True, read_only=True) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 0b24950e2..ec902a21d 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -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) diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py index d50173ff9..fe56d6fa7 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -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() diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index 3027fdad0..40eb2912e 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -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//', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update') diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 7b24b53d4..6cd533e47 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -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 diff --git a/apps/perms/api/application/user_permission/common.py b/apps/perms/api/application/user_permission/common.py index 272f84378..999a09087 100644 --- a/apps/perms/api/application/user_permission/common.py +++ b/apps/perms/api/application/user_permission/common.py @@ -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) diff --git a/apps/perms/signals_handler/common.py b/apps/perms/signals_handler/common.py index c714de834..804d5beaf 100644 --- a/apps/perms/signals_handler/common.py +++ b/apps/perms/signals_handler/common.py @@ -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 diff --git a/apps/perms/utils/application/permission.py b/apps/perms/utils/application/permission.py index 6908db5a3..2c92e5bee 100644 --- a/apps/perms/utils/application/permission.py +++ b/apps/perms/utils/application/permission.py @@ -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 diff --git a/apps/perms/utils/asset/permission.py b/apps/perms/utils/asset/permission.py index 54272c50d..38a3b929d 100644 --- a/apps/perms/utils/asset/permission.py +++ b/apps/perms/utils/asset/permission.py @@ -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) diff --git a/apps/users/urls/api_urls.py b/apps/users/urls/api_urls.py index e6b6974c2..8b9c538bd 100644 --- a/apps/users/urls/api_urls.py +++ b/apps/users/urls/api_urls.py @@ -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'),