mirror of https://github.com/jumpserver/jumpserver
				
				
				
			
		
			
				
	
	
		
			290 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			290 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
import os
 | 
						|
import shutil
 | 
						|
import zipfile
 | 
						|
 | 
						|
from django.conf import settings
 | 
						|
from django.core.exceptions import SuspiciousFileOperation
 | 
						|
from django.db.models import Q
 | 
						|
from django.shortcuts import get_object_or_404
 | 
						|
from django.utils.translation import gettext_lazy as _
 | 
						|
from rest_framework import status
 | 
						|
 | 
						|
from common.api.generic import JMSBulkModelViewSet
 | 
						|
from common.exceptions import JMSException
 | 
						|
from common.permissions import IsOwnerOrAdminWritable
 | 
						|
from common.utils.http import is_true
 | 
						|
from rbac.permissions import RBACPermission
 | 
						|
from ..const import Scope
 | 
						|
from ..exception import PlaybookNoValidEntry
 | 
						|
from ..models import Playbook
 | 
						|
from ..serializers.playbook import PlaybookSerializer
 | 
						|
 | 
						|
__all__ = ["PlaybookViewSet", "PlaybookFileBrowserAPIView"]
 | 
						|
 | 
						|
from rest_framework.views import APIView
 | 
						|
from rest_framework.response import Response
 | 
						|
from django.utils._os import safe_join
 | 
						|
 | 
						|
 | 
						|
def unzip_playbook(src, dist):
 | 
						|
    fz = zipfile.ZipFile(src, 'r')
 | 
						|
    for file in fz.namelist():
 | 
						|
        fz.extract(file, dist)
 | 
						|
 | 
						|
 | 
						|
class PlaybookViewSet(JMSBulkModelViewSet):
 | 
						|
    serializer_class = PlaybookSerializer
 | 
						|
    permission_classes = (RBACPermission, IsOwnerOrAdminWritable)
 | 
						|
    queryset = Playbook.objects.all()
 | 
						|
    search_fields = ('name', 'comment')
 | 
						|
    filterset_fields = ['scope', 'creator']
 | 
						|
 | 
						|
    def allow_bulk_destroy(self, qs, filtered):
 | 
						|
        for obj in filtered:
 | 
						|
            self.check_object_permissions(self.request, obj)
 | 
						|
        return True
 | 
						|
 | 
						|
    def perform_destroy(self, instance):
 | 
						|
        if instance.job_set.exists():
 | 
						|
            raise JMSException(code='playbook_has_job', detail={"msg": _("Currently playbook is being used in a job")})
 | 
						|
        instance_id = instance.id
 | 
						|
        super().perform_destroy(instance)
 | 
						|
        dest_path = safe_join(settings.DATA_DIR, "ops", "playbook", instance_id.__str__())
 | 
						|
        if os.path.exists(dest_path):
 | 
						|
            shutil.rmtree(dest_path)
 | 
						|
 | 
						|
    def get_queryset(self):
 | 
						|
        queryset = super().get_queryset()
 | 
						|
        user = self.request.user
 | 
						|
        if is_true(self.request.query_params.get('only_mine')):
 | 
						|
            queryset = queryset.filter(creator=user)
 | 
						|
        else:
 | 
						|
            queryset = queryset.filter(Q(creator=user) | Q(scope=Scope.public))
 | 
						|
        return queryset
 | 
						|
 | 
						|
    def perform_create(self, serializer):
 | 
						|
        instance = serializer.save()
 | 
						|
        base_path = safe_join(settings.DATA_DIR, "ops", "playbook")
 | 
						|
        clone_id = self.request.query_params.get('clone_from')
 | 
						|
 | 
						|
        if clone_id:
 | 
						|
            src_path = safe_join(base_path, clone_id)
 | 
						|
            dest_path = safe_join(base_path, str(instance.id))
 | 
						|
            if not os.path.exists(src_path):
 | 
						|
                raise JMSException(code='invalid_playbook_id', detail={"msg": "clone playbook file not found"})
 | 
						|
            shutil.copytree(src_path, dest_path)
 | 
						|
            return
 | 
						|
 | 
						|
        if 'multipart/form-data' in self.request.headers['Content-Type']:
 | 
						|
            src_path = safe_join(settings.MEDIA_ROOT, instance.path.name)
 | 
						|
            dest_path = safe_join(base_path, str(instance.id))
 | 
						|
 | 
						|
            try:
 | 
						|
                unzip_playbook(src_path, dest_path)
 | 
						|
            except RuntimeError:
 | 
						|
                raise JMSException(code='invalid_playbook_file', detail={"msg": "Unzip failed"})
 | 
						|
 | 
						|
            if 'main.yml' not in os.listdir(dest_path):
 | 
						|
                raise PlaybookNoValidEntry
 | 
						|
 | 
						|
        elif instance.create_method == 'blank':
 | 
						|
            dest_path = safe_join(base_path, str(instance.id))
 | 
						|
            os.makedirs(dest_path)
 | 
						|
            with open(safe_join(dest_path, 'main.yml'), 'w') as f:
 | 
						|
                f.write('## write your playbook here')
 | 
						|
 | 
						|
 | 
						|
class PlaybookFileBrowserAPIView(APIView):
 | 
						|
    permission_classes = (RBACPermission,)
 | 
						|
    rbac_perms = {
 | 
						|
        'GET': 'ops.view_playbook',
 | 
						|
        'POST': 'ops.change_playbook',
 | 
						|
        'DELETE': 'ops.change_playbook',
 | 
						|
        'PATCH': 'ops.change_playbook',
 | 
						|
    }
 | 
						|
    protected_files = ['root', 'main.yml']
 | 
						|
 | 
						|
    def get_playbook(self, playbook_id):
 | 
						|
        playbook = get_object_or_404(Playbook, id=playbook_id, creator=self.request.user)
 | 
						|
        return playbook
 | 
						|
 | 
						|
    def get(self, request, **kwargs):
 | 
						|
        playbook_id = kwargs.get('pk')
 | 
						|
        user = self.request.user
 | 
						|
        playbook = get_object_or_404(Playbook, Q(creator=user) | Q(scope=Scope.public), id=playbook_id)
 | 
						|
        work_path = playbook.work_dir
 | 
						|
        file_key = request.query_params.get('key', '')
 | 
						|
        if file_key:
 | 
						|
            try:
 | 
						|
                file_path = safe_join(work_path, file_key)
 | 
						|
                with open(file_path, 'r') as f:
 | 
						|
                    content = f.read()
 | 
						|
            except UnicodeDecodeError:
 | 
						|
                content = _('Unsupported file content')
 | 
						|
            except (SuspiciousFileOperation, FileNotFoundError):
 | 
						|
                raise JMSException(code='invalid_file_path', detail={"msg": _("Invalid file path")})
 | 
						|
            return Response({'content': content})
 | 
						|
        else:
 | 
						|
            expand_key = request.query_params.get('expand', '')
 | 
						|
            nodes = self.generate_tree(playbook, work_path, expand_key)
 | 
						|
            return Response(nodes)
 | 
						|
 | 
						|
    def post(self, request, **kwargs):
 | 
						|
        playbook_id = kwargs.get('pk')
 | 
						|
        playbook = self.get_playbook(playbook_id)
 | 
						|
        work_path = playbook.work_dir
 | 
						|
 | 
						|
        parent_key = request.data.get('key', '')
 | 
						|
        if parent_key == 'root':
 | 
						|
            parent_key = ''
 | 
						|
        if os.path.dirname(parent_key) == 'root':
 | 
						|
            parent_key = os.path.basename(parent_key)
 | 
						|
 | 
						|
        full_path = safe_join(work_path, parent_key)
 | 
						|
 | 
						|
        is_directory = request.data.get('is_directory', False)
 | 
						|
        content = request.data.get('content', '')
 | 
						|
        name = request.data.get('name', '')
 | 
						|
 | 
						|
        def find_new_name(p, is_file=False):
 | 
						|
            if not p:
 | 
						|
                if is_file:
 | 
						|
                    p = 'new_file.yml'
 | 
						|
                else:
 | 
						|
                    p = 'new_dir'
 | 
						|
            np = safe_join(full_path, p)
 | 
						|
            n = 0
 | 
						|
            while os.path.exists(np):
 | 
						|
                n += 1
 | 
						|
                np = safe_join(full_path, '{}({})'.format(p, n))
 | 
						|
            return np
 | 
						|
 | 
						|
        try:
 | 
						|
            if is_directory:
 | 
						|
                new_file_path = find_new_name(name)
 | 
						|
                os.makedirs(new_file_path)
 | 
						|
            else:
 | 
						|
                new_file_path = find_new_name(name, True)
 | 
						|
                with open(new_file_path, 'w') as f:
 | 
						|
                    f.write(content)
 | 
						|
        except SuspiciousFileOperation:
 | 
						|
            raise JMSException(code='invalid_file_path', detail={"msg": _("Invalid file path")})
 | 
						|
 | 
						|
        relative_path = os.path.relpath(os.path.dirname(new_file_path), work_path)
 | 
						|
        new_node = {
 | 
						|
            "name": os.path.basename(new_file_path),
 | 
						|
            "title": os.path.basename(new_file_path),
 | 
						|
            "id": safe_join(relative_path, os.path.basename(new_file_path))
 | 
						|
            if not safe_join(relative_path, os.path.basename(new_file_path)).startswith('.')
 | 
						|
            else os.path.basename(new_file_path),
 | 
						|
            "isParent": is_directory,
 | 
						|
            "pId": relative_path if not relative_path.startswith('.') else 'root',
 | 
						|
            "open": True,
 | 
						|
        }
 | 
						|
        if not is_directory:
 | 
						|
            new_node['iconSkin'] = 'file'
 | 
						|
        return Response(new_node)
 | 
						|
 | 
						|
    def patch(self, request, **kwargs):
 | 
						|
        playbook_id = kwargs.get('pk')
 | 
						|
        playbook = self.get_playbook(playbook_id)
 | 
						|
        work_path = playbook.work_dir
 | 
						|
 | 
						|
        file_key = request.data.get('key', '')
 | 
						|
        new_name = request.data.get('new_name', '')
 | 
						|
 | 
						|
        if file_key in self.protected_files and new_name:
 | 
						|
            raise JMSException(code='this_file_can_not_be_rename', detail={"msg": _("This file can not be rename")})
 | 
						|
 | 
						|
        if os.path.dirname(file_key) == 'root':
 | 
						|
            file_key = os.path.basename(file_key)
 | 
						|
 | 
						|
        content = request.data.get('content', '')
 | 
						|
        is_directory = request.data.get('is_directory', False)
 | 
						|
 | 
						|
        if not file_key or file_key == 'root':
 | 
						|
            return Response(status=status.HTTP_400_BAD_REQUEST)
 | 
						|
        file_path = safe_join(work_path, file_key)
 | 
						|
 | 
						|
        # rename
 | 
						|
        if new_name:
 | 
						|
            try:
 | 
						|
                new_file_path = safe_join(os.path.dirname(file_path), new_name)
 | 
						|
                if new_file_path == file_path:
 | 
						|
                    return Response(status=status.HTTP_200_OK)
 | 
						|
                if os.path.exists(new_file_path):
 | 
						|
                    raise JMSException(code='file_already exists', detail={"msg": _("File already exists")})
 | 
						|
                os.rename(file_path, new_file_path)
 | 
						|
            except SuspiciousFileOperation:
 | 
						|
                raise JMSException(code='invalid_file_path', detail={"msg": _("Invalid file path")})
 | 
						|
 | 
						|
        # edit content
 | 
						|
        else:
 | 
						|
            if not is_directory:
 | 
						|
                with open(file_path, 'w') as f:
 | 
						|
                    f.write(content)
 | 
						|
        return Response(status=status.HTTP_200_OK)
 | 
						|
 | 
						|
    def delete(self, request, **kwargs):
 | 
						|
        playbook_id = kwargs.get('pk')
 | 
						|
        playbook = self.get_playbook(playbook_id)
 | 
						|
        work_path = playbook.work_dir
 | 
						|
        file_key = request.query_params.get('key', '')
 | 
						|
        if not file_key:
 | 
						|
            raise JMSException(code='file_key_is_required', detail={"msg": _("File key is required")})
 | 
						|
 | 
						|
        if file_key in self.protected_files:
 | 
						|
            raise JMSException(code='This file can not be delete', detail={"msg": _("This file can not be delete")})
 | 
						|
        file_path = safe_join(work_path, file_key)
 | 
						|
        if os.path.isdir(file_path):
 | 
						|
            shutil.rmtree(file_path)
 | 
						|
        else:
 | 
						|
            os.remove(file_path)
 | 
						|
        return Response({'msg': 'ok'})
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def generate_tree(playbook, root_path, expand_key=None):
 | 
						|
        nodes = [{
 | 
						|
            "name": playbook.name,
 | 
						|
            "title": playbook.name,
 | 
						|
            "id": 'root',
 | 
						|
            "isParent": True,
 | 
						|
            "open": True,
 | 
						|
            "pId": '',
 | 
						|
            "temp": False
 | 
						|
        }]
 | 
						|
        for path, dirs, files in os.walk(root_path):
 | 
						|
            dirs.sort()
 | 
						|
            files.sort()
 | 
						|
 | 
						|
            relative_path = os.path.relpath(path, root_path)
 | 
						|
            for d in dirs:
 | 
						|
                dir_id = os.path.relpath(safe_join(path, d), root_path)
 | 
						|
 | 
						|
                node = {
 | 
						|
                    "name": d,
 | 
						|
                    "title": d,
 | 
						|
                    "id": dir_id,
 | 
						|
                    "isParent": True,
 | 
						|
                    "open": True,
 | 
						|
                    "pId": relative_path if not relative_path.startswith('.') else 'root',
 | 
						|
                    "temp": False
 | 
						|
                }
 | 
						|
                if expand_key == node['id']:
 | 
						|
                    node['open'] = True
 | 
						|
                nodes.append(node)
 | 
						|
            for f in files:
 | 
						|
                file_id = os.path.relpath(safe_join(path, f), root_path)
 | 
						|
                node = {
 | 
						|
                    "name": f,
 | 
						|
                    "title": f,
 | 
						|
                    "iconSkin": 'file',
 | 
						|
                    "id": file_id,
 | 
						|
                    "isParent": False,
 | 
						|
                    "open": False,
 | 
						|
                    "pId": relative_path if not relative_path.startswith('.') else 'root',
 | 
						|
                    "temp": False
 | 
						|
                }
 | 
						|
                nodes.append(node)
 | 
						|
        return nodes
 |