mirror of https://github.com/jumpserver/jumpserver
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
280 lines
10 KiB
280 lines
10 KiB
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.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,)
|
|
queryset = Playbook.objects.all()
|
|
search_fields = ('name', 'comment')
|
|
filterset_fields = ['scope', 'creator']
|
|
|
|
def check_object_permissions(self, request, obj):
|
|
if request.method != 'GET' and obj.creator != request.user:
|
|
self.permission_denied(
|
|
request, message={"detail": "Deleting other people's playbook is not allowed"}
|
|
)
|
|
return super().check_object_permissions(request, obj)
|
|
|
|
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()
|
|
if 'multipart/form-data' in self.request.headers['Content-Type']:
|
|
src_path = safe_join(settings.MEDIA_ROOT, instance.path.name)
|
|
dest_path = safe_join(settings.DATA_DIR, "ops", "playbook", instance.id.__str__())
|
|
|
|
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(settings.DATA_DIR, "ops", "playbook", instance.id.__str__())
|
|
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
|