From f55869a449814f18453b463f8c2a0435ccd3101f Mon Sep 17 00:00:00 2001 From: wangruidong <940853815@qq.com> Date: Fri, 6 Sep 2024 17:41:09 +0800 Subject: [PATCH] feat: Support playbook, adhoc share --- apps/i18n/lina/en.json | 2 +- apps/i18n/lina/ja.json | 2 +- apps/i18n/lina/zh.json | 2 +- apps/i18n/lina/zh_hant.json | 2 +- apps/ops/api/adhoc.py | 29 +++++++++---- apps/ops/api/playbook.py | 26 +++++++++--- apps/ops/const.py | 7 +++- ...03_alter_adhoc_unique_together_and_more.py | 41 +++++++++++++++++++ apps/ops/models/adhoc.py | 10 ++--- apps/ops/models/playbook.py | 11 +++-- apps/ops/serializers/adhoc.py | 12 +++--- apps/ops/serializers/mixin.py | 11 +++++ apps/ops/serializers/playbook.py | 7 ++-- 13 files changed, 128 insertions(+), 34 deletions(-) create mode 100644 apps/ops/migrations/0003_alter_adhoc_unique_together_and_more.py create mode 100644 apps/ops/serializers/mixin.py diff --git a/apps/i18n/lina/en.json b/apps/i18n/lina/en.json index 26fe94167..6e454af30 100644 --- a/apps/i18n/lina/en.json +++ b/apps/i18n/lina/en.json @@ -69,7 +69,7 @@ "Address": "Address", "AdhocCreate": "Create the command", "AdhocDetail": "Command details", - "AdhocManage": "Command", + "AdhocManage": "Script", "AdhocUpdate": "Update the command", "Advanced": "Advanced settings", "AfterChange": "After changes", diff --git a/apps/i18n/lina/ja.json b/apps/i18n/lina/ja.json index d5dee7b62..a3edc71fd 100644 --- a/apps/i18n/lina/ja.json +++ b/apps/i18n/lina/ja.json @@ -69,7 +69,7 @@ "Address": "アドレス", "AdhocCreate": "アドホックコマンドを作成", "AdhocDetail": "コマンド詳細", - "AdhocManage": "コマンド", + "AdhocManage": "スクリプト管理", "AdhocUpdate": "コマンドを更新", "Advanced": "高度な設定", "AfterChange": "変更後", diff --git a/apps/i18n/lina/zh.json b/apps/i18n/lina/zh.json index 4df1b71c8..45106c025 100644 --- a/apps/i18n/lina/zh.json +++ b/apps/i18n/lina/zh.json @@ -69,7 +69,7 @@ "Address": "地址", "AdhocCreate": "创建命令", "AdhocDetail": "命令详情", - "AdhocManage": "命令管理", + "AdhocManage": "脚本管理", "AdhocUpdate": "更新命令", "Advanced": "高级设置", "AfterChange": "变更后", diff --git a/apps/i18n/lina/zh_hant.json b/apps/i18n/lina/zh_hant.json index 248b398d7..500c74591 100644 --- a/apps/i18n/lina/zh_hant.json +++ b/apps/i18n/lina/zh_hant.json @@ -87,7 +87,7 @@ "Addressee": "收件人", "AdhocCreate": "創建命令", "AdhocDetail": "命令詳情", - "AdhocManage": "命令管理", + "AdhocManage": "腳本管理", "AdhocUpdate": "更新命令", "Admin": "管理員", "AdminUser": "特權用戶", diff --git a/apps/ops/api/adhoc.py b/apps/ops/api/adhoc.py index 70b74bed7..77c989940 100644 --- a/apps/ops/api/adhoc.py +++ b/apps/ops/api/adhoc.py @@ -1,22 +1,37 @@ # -*- 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 ..const import Scope from ..models import AdHoc -from ..serializers import ( - AdHocSerializer -) +from ..serializers import AdHocSerializer __all__ = [ 'AdHocViewSet' ] -class AdHocViewSet(OrgBulkModelViewSet): +class AdHocViewSet(JMSBulkModelViewSet): + queryset = AdHoc.objects.all() serializer_class = AdHocSerializer permission_classes = (RBACPermission,) 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): 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 diff --git a/apps/ops/api/playbook.py b/apps/ops/api/playbook.py index e75841ff1..cab0c960d 100644 --- a/apps/ops/api/playbook.py +++ b/apps/ops/api/playbook.py @@ -4,13 +4,16 @@ 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 orgs.mixins.api import OrgBulkModelViewSet +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 @@ -28,11 +31,19 @@ def unzip_playbook(src, dist): fz.extract(file, dist) -class PlaybookViewSet(OrgBulkModelViewSet): +class PlaybookViewSet(JMSBulkModelViewSet): serializer_class = PlaybookSerializer permission_classes = (RBACPermission,) - model = Playbook + 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(): @@ -45,7 +56,11 @@ class PlaybookViewSet(OrgBulkModelViewSet): def get_queryset(self): 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 def perform_create(self, serializer): @@ -85,7 +100,8 @@ class PlaybookFileBrowserAPIView(APIView): def get(self, request, **kwargs): 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 file_key = request.query_params.get('key', '') if file_key: diff --git a/apps/ops/const.py b/apps/ops/const.py index 5676da4de..b2b1c63ca 100644 --- a/apps/ops/const.py +++ b/apps/ops/const.py @@ -1,5 +1,5 @@ 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): @@ -80,3 +80,8 @@ class JobStatus(models.TextChoices): CELERY_LOG_MAGIC_MARK = b'\x00\x00\x00\x00\x00' COMMAND_EXECUTION_DISABLED = _('Command execution disabled') + + +class Scope(models.TextChoices): + public = 'public', pgettext_lazy("scope", 'Public') + private = 'private', _('Private') diff --git a/apps/ops/migrations/0003_alter_adhoc_unique_together_and_more.py b/apps/ops/migrations/0003_alter_adhoc_unique_together_and_more.py new file mode 100644 index 000000000..610e19330 --- /dev/null +++ b/apps/ops/migrations/0003_alter_adhoc_unique_together_and_more.py @@ -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', + ), + ] diff --git a/apps/ops/models/adhoc.py b/apps/ops/models/adhoc.py index 3ca2fa281..f7e13f18d 100644 --- a/apps/ops/models/adhoc.py +++ b/apps/ops/models/adhoc.py @@ -8,14 +8,13 @@ from common.utils import get_logger __all__ = ["AdHoc"] -from ops.const import AdHocModules - -from orgs.mixins.models import JMSOrgBaseModel +from common.db.models import JMSBaseModel +from ops.const import AdHocModules, Scope logger = get_logger(__file__) -class AdHoc(JMSOrgBaseModel): +class AdHoc(JMSBaseModel): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name')) 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')) 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) + scope = models.CharField(max_length=64, default=Scope.public, verbose_name=_('Scope')) @property def row_count(self): @@ -40,5 +40,5 @@ class AdHoc(JMSOrgBaseModel): return "{}: {}".format(self.module, self.args) class Meta: - unique_together = [('name', 'org_id', 'creator')] + unique_together = [('name', 'creator')] verbose_name = _("Adhoc") diff --git a/apps/ops/models/playbook.py b/apps/ops/models/playbook.py index 2b703916a..ccc7223d2 100644 --- a/apps/ops/models/playbook.py +++ b/apps/ops/models/playbook.py @@ -6,9 +6,9 @@ from django.db import models from django.utils.translation import gettext_lazy as _ 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 orgs.mixins.models import JMSOrgBaseModel dangerous_keywords = ( 'hosts:localhost', @@ -23,7 +23,9 @@ dangerous_keywords = ( ) -class Playbook(JMSOrgBaseModel): + + +class Playbook(JMSBaseModel): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name'), null=True) 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) create_method = models.CharField(max_length=128, choices=CreateMethods.choices, default=CreateMethods.blank, 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) def __str__(self): @@ -84,6 +87,6 @@ class Playbook(JMSOrgBaseModel): return work_dir class Meta: - unique_together = [('name', 'org_id', 'creator')] + unique_together = [('name', 'creator')] verbose_name = _("Playbook") ordering = ['date_created'] diff --git a/apps/ops/serializers/adhoc.py b/apps/ops/serializers/adhoc.py index 58cbaad98..4b72860e5 100644 --- a/apps/ops/serializers/adhoc.py +++ b/apps/ops/serializers/adhoc.py @@ -1,17 +1,19 @@ # ~*~ coding: utf-8 ~*~ from __future__ import unicode_literals - +from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from common.serializers.fields import ReadableHiddenField -from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from common.serializers.fields import ReadableHiddenField, LabeledChoiceField +from common.serializers.mixin import CommonBulkModelSerializer +from .mixin import ScopeSerializerMixin +from ..const import Scope from ..models import AdHoc -class AdHocSerializer(BulkOrgResourceModelSerializer): +class AdHocSerializer(ScopeSerializerMixin, CommonBulkModelSerializer): creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) class Meta: model = AdHoc 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"] diff --git a/apps/ops/serializers/mixin.py b/apps/ops/serializers/mixin.py new file mode 100644 index 000000000..c1ee343ee --- /dev/null +++ b/apps/ops/serializers/mixin.py @@ -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") + ) diff --git a/apps/ops/serializers/playbook.py b/apps/ops/serializers/playbook.py index 1ff48906a..c157251a6 100644 --- a/apps/ops/serializers/playbook.py +++ b/apps/ops/serializers/playbook.py @@ -3,8 +3,9 @@ import os from rest_framework import serializers from common.serializers.fields import ReadableHiddenField +from common.serializers.mixin import CommonBulkModelSerializer from ops.models import Playbook -from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from .mixin import ScopeSerializerMixin def parse_playbook_name(path): @@ -12,7 +13,7 @@ def parse_playbook_name(path): return file_name.split(".")[-2] -class PlaybookSerializer(BulkOrgResourceModelSerializer): +class PlaybookSerializer(ScopeSerializerMixin, CommonBulkModelSerializer): creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) path = serializers.FileField(required=False) @@ -26,6 +27,6 @@ class PlaybookSerializer(BulkOrgResourceModelSerializer): model = Playbook read_only_fields = ["id", "date_created", "date_updated"] fields = read_only_fields + [ - "id", 'path', "name", "comment", "creator", + "id", 'path', 'scope', "name", "comment", "creator", 'create_method', 'vcs_url', ]