mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-09-03 08:25:04 +00:00
feat: 支持 virtual app (#12199)
* feat: 支持 virtual app * perf: 增加 virtual host * perf: 新增 virtual app 上传接口 * perf: 更名为 app provider * perf: 优化代码 --------- Co-authored-by: Eric <xplzv@126.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .session import *
|
||||
from .component import *
|
||||
from .applet import *
|
||||
from .component import *
|
||||
from .db_listen_port import *
|
||||
from .session import *
|
||||
from .virtualapp import *
|
||||
|
3
apps/terminal/api/virtualapp/__init__.py
Normal file
3
apps/terminal/api/virtualapp/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .provider import *
|
||||
from .relation import *
|
||||
from .virtualapp import *
|
63
apps/terminal/api/virtualapp/provider.py
Normal file
63
apps/terminal/api/virtualapp/provider.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from django.core.cache import cache
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.api import JMSBulkModelViewSet
|
||||
from common.permissions import IsServiceAccount
|
||||
from orgs.utils import tmp_to_builtin_org
|
||||
from terminal.models import AppProvider
|
||||
from terminal.serializers import (
|
||||
AppProviderSerializer, AppProviderContainerSerializer
|
||||
)
|
||||
|
||||
__all__ = ['AppProviderViewSet', ]
|
||||
|
||||
|
||||
class AppProviderViewSet(JMSBulkModelViewSet):
|
||||
serializer_class = AppProviderSerializer
|
||||
queryset = AppProvider.objects.all()
|
||||
search_fields = ['name', 'hostname', ]
|
||||
rbac_perms = {
|
||||
'containers': 'terminal.view_appprovider',
|
||||
'status': 'terminal.view_appprovider',
|
||||
}
|
||||
|
||||
cache_status_key_prefix = 'virtual_host_{}_status'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
with tmp_to_builtin_org(system=1):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action == 'create':
|
||||
return [IsServiceAccount()]
|
||||
return super().get_permissions()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
request_terminal = getattr(self.request.user, 'terminal', None)
|
||||
if not request_terminal:
|
||||
raise ValidationError('Request user has no terminal')
|
||||
data = dict()
|
||||
data['terminal'] = request_terminal
|
||||
data['id'] = self.request.user.id
|
||||
serializer.save(**data)
|
||||
|
||||
@action(detail=True, methods=['get'], serializer_class=AppProviderContainerSerializer)
|
||||
def containers(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
key = self.cache_status_key_prefix.format(instance.id)
|
||||
data = cache.get(key)
|
||||
if not data:
|
||||
data = []
|
||||
return self.get_paginated_response_from_queryset(data)
|
||||
|
||||
@action(detail=True, methods=['post'], serializer_class=AppProviderContainerSerializer)
|
||||
def status(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(data=request.data, many=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
validated_data = serializer.validated_data
|
||||
key = self.cache_status_key_prefix.format(instance.id)
|
||||
cache.set(key, validated_data, 60 * 3)
|
||||
return Response({'msg': 'ok'})
|
64
apps/terminal/api/virtualapp/relation.py
Normal file
64
apps/terminal/api/virtualapp/relation.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from typing import Callable
|
||||
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.request import Request
|
||||
|
||||
from common.api import JMSModelViewSet
|
||||
from common.permissions import IsServiceAccount
|
||||
from common.utils import is_uuid
|
||||
from rbac.permissions import RBACPermission
|
||||
from terminal.models import AppProvider
|
||||
from terminal.serializers import (
|
||||
VirtualAppPublicationSerializer
|
||||
)
|
||||
|
||||
|
||||
class ProviderMixin:
|
||||
request: Request
|
||||
permission_denied: Callable
|
||||
kwargs: dict
|
||||
rbac_perms = (
|
||||
('list', 'terminal.view_appprovider'),
|
||||
('retrieve', 'terminal.view_appprovider'),
|
||||
)
|
||||
|
||||
def get_permissions(self):
|
||||
if self.kwargs.get('host') and settings.DEBUG:
|
||||
return [RBACPermission()]
|
||||
else:
|
||||
return [IsServiceAccount()]
|
||||
|
||||
def self_provider(self):
|
||||
try:
|
||||
return self.request.user.terminal.app_provider
|
||||
except AttributeError:
|
||||
raise self.permission_denied(self.request, 'User has no app provider')
|
||||
|
||||
def pk_provider(self):
|
||||
return get_object_or_404(AppProvider, id=self.kwargs.get('provider'))
|
||||
|
||||
@property
|
||||
def provider(self):
|
||||
if self.kwargs.get('provider'):
|
||||
host = self.pk_provider()
|
||||
else:
|
||||
host = self.self_provider()
|
||||
return host
|
||||
|
||||
|
||||
class AppProviderAppViewSet(ProviderMixin, JMSModelViewSet):
|
||||
provider: AppProvider
|
||||
serializer_class = VirtualAppPublicationSerializer
|
||||
filterset_fields = ['provider__name', 'app__name', 'status']
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
if not is_uuid(pk):
|
||||
return self.provider.publications.get(app__name=pk)
|
||||
else:
|
||||
return self.provider.publications.get(id=pk)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.provider.publications.all()
|
||||
return queryset
|
77
apps/terminal/api/virtualapp/virtualapp.py
Normal file
77
apps/terminal/api/virtualapp/virtualapp.py
Normal file
@@ -0,0 +1,77 @@
|
||||
import os.path
|
||||
import shutil
|
||||
import zipfile
|
||||
from typing import Callable
|
||||
|
||||
from django.core.files.storage import default_storage
|
||||
from django.utils._os import safe_join
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from common.api import JMSBulkModelViewSet
|
||||
from common.serializers import FileSerializer
|
||||
from terminal import serializers
|
||||
from terminal.models import VirtualAppPublication, VirtualApp
|
||||
|
||||
__all__ = ['VirtualAppViewSet', 'VirtualAppPublicationViewSet']
|
||||
|
||||
|
||||
class UploadMixin:
|
||||
get_serializer: Callable
|
||||
request: Request
|
||||
get_object: Callable
|
||||
|
||||
def extract_zip_pkg(self):
|
||||
serializer = self.get_serializer(data=self.request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
file = serializer.validated_data['file']
|
||||
save_to = 'virtual_apps/{}'.format(file.name + '.tmp.zip')
|
||||
if default_storage.exists(save_to):
|
||||
default_storage.delete(save_to)
|
||||
rel_path = default_storage.save(save_to, file)
|
||||
path = default_storage.path(rel_path)
|
||||
extract_to = default_storage.path('virtual_apps/{}.tmp'.format(file.name))
|
||||
if os.path.exists(extract_to):
|
||||
shutil.rmtree(extract_to)
|
||||
try:
|
||||
with zipfile.ZipFile(path) as zp:
|
||||
if zp.testzip() is not None:
|
||||
raise ValidationError({'error': _('Invalid zip file')})
|
||||
zp.extractall(extract_to)
|
||||
except RuntimeError as e:
|
||||
raise ValidationError({'error': _('Invalid zip file') + ': {}'.format(e)})
|
||||
tmp_dir = safe_join(extract_to, file.name.replace('.zip', ''))
|
||||
return tmp_dir
|
||||
|
||||
@action(detail=False, methods=['post'], serializer_class=FileSerializer)
|
||||
def upload(self, request, *args, **kwargs):
|
||||
tmp_dir = self.extract_zip_pkg()
|
||||
manifest = VirtualApp.validate_pkg(tmp_dir)
|
||||
name = manifest['name']
|
||||
instance = VirtualApp.objects.filter(name=name).first()
|
||||
if instance:
|
||||
return Response({'error': 'virtual app already exists: {}'.format(name)}, status=400)
|
||||
|
||||
app, serializer = VirtualApp.install_from_dir(tmp_dir)
|
||||
return Response(serializer.data, status=201)
|
||||
|
||||
|
||||
class VirtualAppViewSet(UploadMixin, JMSBulkModelViewSet):
|
||||
queryset = VirtualApp.objects.all()
|
||||
serializer_class = serializers.VirtualAppSerializer
|
||||
filterset_fields = ['name', 'image_name', 'is_active']
|
||||
search_fields = ['name', ]
|
||||
rbac_perms = {
|
||||
'upload': 'terminal.add_virtualapp',
|
||||
}
|
||||
|
||||
|
||||
class VirtualAppPublicationViewSet(viewsets.ModelViewSet):
|
||||
queryset = VirtualAppPublication.objects.all()
|
||||
serializer_class = serializers.VirtualAppPublicationSerializer
|
||||
filterset_fields = ['app__name', 'provider__name', 'status']
|
||||
search_fields = ['app__name', 'provider__name', ]
|
Reference in New Issue
Block a user