mirror of https://github.com/jumpserver/jumpserver
feat: Support playbook, adhoc share
parent
b6f3c23787
commit
f55869a449
|
@ -69,7 +69,7 @@
|
||||||
"Address": "Address",
|
"Address": "Address",
|
||||||
"AdhocCreate": "Create the command",
|
"AdhocCreate": "Create the command",
|
||||||
"AdhocDetail": "Command details",
|
"AdhocDetail": "Command details",
|
||||||
"AdhocManage": "Command",
|
"AdhocManage": "Script",
|
||||||
"AdhocUpdate": "Update the command",
|
"AdhocUpdate": "Update the command",
|
||||||
"Advanced": "Advanced settings",
|
"Advanced": "Advanced settings",
|
||||||
"AfterChange": "After changes",
|
"AfterChange": "After changes",
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
"Address": "アドレス",
|
"Address": "アドレス",
|
||||||
"AdhocCreate": "アドホックコマンドを作成",
|
"AdhocCreate": "アドホックコマンドを作成",
|
||||||
"AdhocDetail": "コマンド詳細",
|
"AdhocDetail": "コマンド詳細",
|
||||||
"AdhocManage": "コマンド",
|
"AdhocManage": "スクリプト管理",
|
||||||
"AdhocUpdate": "コマンドを更新",
|
"AdhocUpdate": "コマンドを更新",
|
||||||
"Advanced": "高度な設定",
|
"Advanced": "高度な設定",
|
||||||
"AfterChange": "変更後",
|
"AfterChange": "変更後",
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
"Address": "地址",
|
"Address": "地址",
|
||||||
"AdhocCreate": "创建命令",
|
"AdhocCreate": "创建命令",
|
||||||
"AdhocDetail": "命令详情",
|
"AdhocDetail": "命令详情",
|
||||||
"AdhocManage": "命令管理",
|
"AdhocManage": "脚本管理",
|
||||||
"AdhocUpdate": "更新命令",
|
"AdhocUpdate": "更新命令",
|
||||||
"Advanced": "高级设置",
|
"Advanced": "高级设置",
|
||||||
"AfterChange": "变更后",
|
"AfterChange": "变更后",
|
||||||
|
|
|
@ -87,7 +87,7 @@
|
||||||
"Addressee": "收件人",
|
"Addressee": "收件人",
|
||||||
"AdhocCreate": "創建命令",
|
"AdhocCreate": "創建命令",
|
||||||
"AdhocDetail": "命令詳情",
|
"AdhocDetail": "命令詳情",
|
||||||
"AdhocManage": "命令管理",
|
"AdhocManage": "腳本管理",
|
||||||
"AdhocUpdate": "更新命令",
|
"AdhocUpdate": "更新命令",
|
||||||
"Admin": "管理員",
|
"Admin": "管理員",
|
||||||
"AdminUser": "特權用戶",
|
"AdminUser": "特權用戶",
|
||||||
|
|
|
@ -1,22 +1,37 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from common.api.generic import JMSBulkModelViewSet
|
||||||
|
from common.utils.http import is_true
|
||||||
from rbac.permissions import RBACPermission
|
from rbac.permissions import RBACPermission
|
||||||
|
from ..const import Scope
|
||||||
from ..models import AdHoc
|
from ..models import AdHoc
|
||||||
from ..serializers import (
|
from ..serializers import AdHocSerializer
|
||||||
AdHocSerializer
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'AdHocViewSet'
|
'AdHocViewSet'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class AdHocViewSet(OrgBulkModelViewSet):
|
class AdHocViewSet(JMSBulkModelViewSet):
|
||||||
|
queryset = AdHoc.objects.all()
|
||||||
serializer_class = AdHocSerializer
|
serializer_class = AdHocSerializer
|
||||||
permission_classes = (RBACPermission,)
|
permission_classes = (RBACPermission,)
|
||||||
search_fields = ('name', 'comment')
|
search_fields = ('name', 'comment')
|
||||||
model = AdHoc
|
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 script is not allowed"}
|
||||||
|
)
|
||||||
|
return super().check_object_permissions(request, obj)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
return queryset.filter(creator=self.request.user)
|
user = self.request.user
|
||||||
|
if is_true(self.request.query_params.get('only_myself')):
|
||||||
|
queryset = queryset.filter(creator=user)
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(Q(creator=user) | Q(scope=Scope.public))
|
||||||
|
return queryset
|
||||||
|
|
|
@ -4,13 +4,16 @@ import zipfile
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import SuspiciousFileOperation
|
from django.core.exceptions import SuspiciousFileOperation
|
||||||
|
from django.db.models import Q
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
|
from common.api.generic import JMSBulkModelViewSet
|
||||||
from common.exceptions import JMSException
|
from common.exceptions import JMSException
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from common.utils.http import is_true
|
||||||
from rbac.permissions import RBACPermission
|
from rbac.permissions import RBACPermission
|
||||||
|
from ..const import Scope
|
||||||
from ..exception import PlaybookNoValidEntry
|
from ..exception import PlaybookNoValidEntry
|
||||||
from ..models import Playbook
|
from ..models import Playbook
|
||||||
from ..serializers.playbook import PlaybookSerializer
|
from ..serializers.playbook import PlaybookSerializer
|
||||||
|
@ -28,11 +31,19 @@ def unzip_playbook(src, dist):
|
||||||
fz.extract(file, dist)
|
fz.extract(file, dist)
|
||||||
|
|
||||||
|
|
||||||
class PlaybookViewSet(OrgBulkModelViewSet):
|
class PlaybookViewSet(JMSBulkModelViewSet):
|
||||||
serializer_class = PlaybookSerializer
|
serializer_class = PlaybookSerializer
|
||||||
permission_classes = (RBACPermission,)
|
permission_classes = (RBACPermission,)
|
||||||
model = Playbook
|
queryset = Playbook.objects.all()
|
||||||
search_fields = ('name', 'comment')
|
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):
|
def perform_destroy(self, instance):
|
||||||
if instance.job_set.exists():
|
if instance.job_set.exists():
|
||||||
|
@ -45,7 +56,11 @@ class PlaybookViewSet(OrgBulkModelViewSet):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
queryset = queryset.filter(creator=self.request.user)
|
user = self.request.user
|
||||||
|
if is_true(self.request.query_params.get('only_myself')):
|
||||||
|
queryset = queryset.filter(creator=user)
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(Q(creator=user) | Q(scope=Scope.public))
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
|
@ -85,7 +100,8 @@ class PlaybookFileBrowserAPIView(APIView):
|
||||||
|
|
||||||
def get(self, request, **kwargs):
|
def get(self, request, **kwargs):
|
||||||
playbook_id = kwargs.get('pk')
|
playbook_id = kwargs.get('pk')
|
||||||
playbook = self.get_playbook(playbook_id)
|
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
|
work_path = playbook.work_dir
|
||||||
file_key = request.query_params.get('key', '')
|
file_key = request.query_params.get('key', '')
|
||||||
if file_key:
|
if file_key:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
|
|
||||||
|
|
||||||
class StrategyChoice(models.TextChoices):
|
class StrategyChoice(models.TextChoices):
|
||||||
|
@ -80,3 +80,8 @@ class JobStatus(models.TextChoices):
|
||||||
CELERY_LOG_MAGIC_MARK = b'\x00\x00\x00\x00\x00'
|
CELERY_LOG_MAGIC_MARK = b'\x00\x00\x00\x00\x00'
|
||||||
|
|
||||||
COMMAND_EXECUTION_DISABLED = _('Command execution disabled')
|
COMMAND_EXECUTION_DISABLED = _('Command execution disabled')
|
||||||
|
|
||||||
|
|
||||||
|
class Scope(models.TextChoices):
|
||||||
|
public = 'public', pgettext_lazy("scope", 'Public')
|
||||||
|
private = 'private', _('Private')
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Generated by Django 4.1.13 on 2024-09-06 08:32
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('ops', '0002_celerytask'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='adhoc',
|
||||||
|
unique_together={('name', 'creator')},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='playbook',
|
||||||
|
unique_together={('name', 'creator')},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='adhoc',
|
||||||
|
name='scope',
|
||||||
|
field=models.CharField(default='public', max_length=64, verbose_name='Scope'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='playbook',
|
||||||
|
name='scope',
|
||||||
|
field=models.CharField(default='public', max_length=64, verbose_name='Scope'),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='adhoc',
|
||||||
|
name='org_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='playbook',
|
||||||
|
name='org_id',
|
||||||
|
),
|
||||||
|
]
|
|
@ -8,14 +8,13 @@ from common.utils import get_logger
|
||||||
|
|
||||||
__all__ = ["AdHoc"]
|
__all__ = ["AdHoc"]
|
||||||
|
|
||||||
from ops.const import AdHocModules
|
from common.db.models import JMSBaseModel
|
||||||
|
from ops.const import AdHocModules, Scope
|
||||||
from orgs.mixins.models import JMSOrgBaseModel
|
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class AdHoc(JMSOrgBaseModel):
|
class AdHoc(JMSBaseModel):
|
||||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||||
pattern = models.CharField(max_length=1024, verbose_name=_("Pattern"), default='all')
|
pattern = models.CharField(max_length=1024, verbose_name=_("Pattern"), default='all')
|
||||||
|
@ -24,6 +23,7 @@ class AdHoc(JMSOrgBaseModel):
|
||||||
args = models.CharField(max_length=8192, default='', verbose_name=_('Args'))
|
args = models.CharField(max_length=8192, default='', verbose_name=_('Args'))
|
||||||
creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
|
creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
|
||||||
comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True)
|
comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True)
|
||||||
|
scope = models.CharField(max_length=64, default=Scope.public, verbose_name=_('Scope'))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def row_count(self):
|
def row_count(self):
|
||||||
|
@ -40,5 +40,5 @@ class AdHoc(JMSOrgBaseModel):
|
||||||
return "{}: {}".format(self.module, self.args)
|
return "{}: {}".format(self.module, self.args)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [('name', 'org_id', 'creator')]
|
unique_together = [('name', 'creator')]
|
||||||
verbose_name = _("Adhoc")
|
verbose_name = _("Adhoc")
|
||||||
|
|
|
@ -6,9 +6,9 @@ from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from private_storage.fields import PrivateFileField
|
from private_storage.fields import PrivateFileField
|
||||||
|
|
||||||
from ops.const import CreateMethods
|
from common.db.models import JMSBaseModel
|
||||||
|
from ops.const import CreateMethods, Scope
|
||||||
from ops.exception import PlaybookNoValidEntry
|
from ops.exception import PlaybookNoValidEntry
|
||||||
from orgs.mixins.models import JMSOrgBaseModel
|
|
||||||
|
|
||||||
dangerous_keywords = (
|
dangerous_keywords = (
|
||||||
'hosts:localhost',
|
'hosts:localhost',
|
||||||
|
@ -23,7 +23,9 @@ dangerous_keywords = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Playbook(JMSOrgBaseModel):
|
|
||||||
|
|
||||||
|
class Playbook(JMSBaseModel):
|
||||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
name = models.CharField(max_length=128, verbose_name=_('Name'), null=True)
|
name = models.CharField(max_length=128, verbose_name=_('Name'), null=True)
|
||||||
path = PrivateFileField(upload_to='playbooks/')
|
path = PrivateFileField(upload_to='playbooks/')
|
||||||
|
@ -31,6 +33,7 @@ class Playbook(JMSOrgBaseModel):
|
||||||
comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True)
|
comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True)
|
||||||
create_method = models.CharField(max_length=128, choices=CreateMethods.choices, default=CreateMethods.blank,
|
create_method = models.CharField(max_length=128, choices=CreateMethods.choices, default=CreateMethods.blank,
|
||||||
verbose_name=_('CreateMethod'))
|
verbose_name=_('CreateMethod'))
|
||||||
|
scope = models.CharField(max_length=64, default=Scope.public, verbose_name=_('Scope'))
|
||||||
vcs_url = models.CharField(max_length=1024, default='', verbose_name=_('VCS URL'), null=True, blank=True)
|
vcs_url = models.CharField(max_length=1024, default='', verbose_name=_('VCS URL'), null=True, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -84,6 +87,6 @@ class Playbook(JMSOrgBaseModel):
|
||||||
return work_dir
|
return work_dir
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [('name', 'org_id', 'creator')]
|
unique_together = [('name', 'creator')]
|
||||||
verbose_name = _("Playbook")
|
verbose_name = _("Playbook")
|
||||||
ordering = ['date_created']
|
ordering = ['date_created']
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
# ~*~ coding: utf-8 ~*~
|
# ~*~ coding: utf-8 ~*~
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from common.serializers.fields import ReadableHiddenField
|
from common.serializers.fields import ReadableHiddenField, LabeledChoiceField
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from common.serializers.mixin import CommonBulkModelSerializer
|
||||||
|
from .mixin import ScopeSerializerMixin
|
||||||
|
from ..const import Scope
|
||||||
from ..models import AdHoc
|
from ..models import AdHoc
|
||||||
|
|
||||||
|
|
||||||
class AdHocSerializer(BulkOrgResourceModelSerializer):
|
class AdHocSerializer(ScopeSerializerMixin, CommonBulkModelSerializer):
|
||||||
creator = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
creator = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AdHoc
|
model = AdHoc
|
||||||
read_only_field = ["id", "creator", "date_created", "date_updated"]
|
read_only_field = ["id", "creator", "date_created", "date_updated"]
|
||||||
fields = read_only_field + ["id", "name", "module", "args", "comment"]
|
fields = read_only_field + ["id", "name", "scope", "module", "args", "comment"]
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from common.serializers.fields import LabeledChoiceField
|
||||||
|
from ..const import Scope
|
||||||
|
|
||||||
|
|
||||||
|
class ScopeSerializerMixin(serializers.Serializer):
|
||||||
|
scope = LabeledChoiceField(
|
||||||
|
choices=Scope.choices, default=Scope.public, label=_("Scope")
|
||||||
|
)
|
|
@ -3,8 +3,9 @@ import os
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from common.serializers.fields import ReadableHiddenField
|
from common.serializers.fields import ReadableHiddenField
|
||||||
|
from common.serializers.mixin import CommonBulkModelSerializer
|
||||||
from ops.models import Playbook
|
from ops.models import Playbook
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from .mixin import ScopeSerializerMixin
|
||||||
|
|
||||||
|
|
||||||
def parse_playbook_name(path):
|
def parse_playbook_name(path):
|
||||||
|
@ -12,7 +13,7 @@ def parse_playbook_name(path):
|
||||||
return file_name.split(".")[-2]
|
return file_name.split(".")[-2]
|
||||||
|
|
||||||
|
|
||||||
class PlaybookSerializer(BulkOrgResourceModelSerializer):
|
class PlaybookSerializer(ScopeSerializerMixin, CommonBulkModelSerializer):
|
||||||
creator = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
creator = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
||||||
path = serializers.FileField(required=False)
|
path = serializers.FileField(required=False)
|
||||||
|
|
||||||
|
@ -26,6 +27,6 @@ class PlaybookSerializer(BulkOrgResourceModelSerializer):
|
||||||
model = Playbook
|
model = Playbook
|
||||||
read_only_fields = ["id", "date_created", "date_updated"]
|
read_only_fields = ["id", "date_created", "date_updated"]
|
||||||
fields = read_only_fields + [
|
fields = read_only_fields + [
|
||||||
"id", 'path', "name", "comment", "creator",
|
"id", 'path', 'scope', "name", "comment", "creator",
|
||||||
'create_method', 'vcs_url',
|
'create_method', 'vcs_url',
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue