From c4727e1eba8538a69983c20e587eb80e8330f14c Mon Sep 17 00:00:00 2001
From: "fghbng@qq.com" <fghbng@qq.com>
Date: Sun, 25 Apr 2021 14:58:06 +0800
Subject: [PATCH 1/8] =?UTF-8?q?=E3=80=90=E4=BB=AA=E8=A1=A8=E7=9B=98?=
 =?UTF-8?q?=E3=80=91=E5=9C=A8=E7=BA=BF=E7=94=A8=E6=88=B7=E6=95=B0=E4=B8=8D?=
 =?UTF-8?q?=E5=AF=B9=EF=BC=8C=EF=BC=88=E8=BF=9E=E4=B8=8Awindows=E8=B5=84?=
 =?UTF-8?q?=E4=BA=A7=E4=B9=8B=E5=90=8E=EF=BC=8C=E5=9C=A8=E7=BA=BF=E7=94=A8?=
 =?UTF-8?q?=E6=88=B7=E6=95=B0=E5=B0=B1=E4=B8=8D=E5=AF=B9=E4=BA=86=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/jumpserver/api.py | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py
index bda2537c8..0f0193c49 100644
--- a/apps/jumpserver/api.py
+++ b/apps/jumpserver/api.py
@@ -99,7 +99,7 @@ class DatesLoginMetricMixin:
         if count is not None:
             return count
         ds, de = self.get_date_start_2_end(date)
-        count = len(set(Session.objects.filter(date_start__range=(ds, de)).values_list('user', flat=True)))
+        count = len(set(Session.objects.filter(date_start__range=(ds, de)).values_list('user_id', flat=True)))
         self.__set_data_to_cache(date, tp, count)
         return count
 
@@ -129,7 +129,7 @@ class DatesLoginMetricMixin:
 
     @lazyproperty
     def dates_total_count_active_users(self):
-        count = len(set(self.sessions_queryset.values_list('user', flat=True)))
+        count = len(set(self.sessions_queryset.values_list('user_id', flat=True)))
         return count
 
     @lazyproperty
@@ -161,10 +161,10 @@ class DatesLoginMetricMixin:
     @lazyproperty
     def dates_total_count_disabled_assets(self):
         return Asset.objects.filter(is_active=False).count()
-    
+
     # 以下是从week中而来
     def get_dates_login_times_top5_users(self):
-        users = self.sessions_queryset.values_list('user', flat=True)
+        users = self.sessions_queryset.values_list('user_id', flat=True)
         users = [
             {'user': user, 'total': total}
             for user, total in Counter(users).most_common(5)
@@ -172,7 +172,7 @@ class DatesLoginMetricMixin:
         return users
 
     def get_dates_total_count_login_users(self):
-        return len(set(self.sessions_queryset.values_list('user', flat=True)))
+        return len(set(self.sessions_queryset.values_list('user_id', flat=True)))
 
     def get_dates_total_count_login_times(self):
         return self.sessions_queryset.count()
@@ -186,8 +186,8 @@ class DatesLoginMetricMixin:
         return list(assets)
 
     def get_dates_login_times_top10_users(self):
-        users = self.sessions_queryset.values("user") \
-                    .annotate(total=Count("user")) \
+        users = self.sessions_queryset.values("user_id") \
+                    .annotate(total=Count("user_id")) \
                     .annotate(last=Max("date_start")).order_by("-total")[:10]
         for user in users:
             user['last'] = str(user['last'])
@@ -221,7 +221,7 @@ class TotalCountMixin:
 
     @staticmethod
     def get_total_count_online_users():
-        count = len(set(Session.objects.filter(is_finished=False).values_list('user', flat=True)))
+        count = len(set(Session.objects.filter(is_finished=False).values_list('user_id', flat=True)))
         return count
 
     @staticmethod

From 99cce185dd01a931181067af6a3e0a3ab9dabeca Mon Sep 17 00:00:00 2001
From: xinwen <coderWen@126.com>
Date: Tue, 27 Apr 2021 14:21:48 +0800
Subject: [PATCH 2/8] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E5=90=AF=E5=8A=A8?=
 =?UTF-8?q?=E5=A4=B1=E6=95=88=E7=BC=93=E5=AD=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../management/commands/expire_caches.py      | 19 +++++++++++++++++++
 jms                                           |  9 +++++++++
 2 files changed, 28 insertions(+)
 create mode 100644 apps/common/management/commands/expire_caches.py

diff --git a/apps/common/management/commands/expire_caches.py b/apps/common/management/commands/expire_caches.py
new file mode 100644
index 000000000..fb09f47eb
--- /dev/null
+++ b/apps/common/management/commands/expire_caches.py
@@ -0,0 +1,19 @@
+from django.core.management.base import BaseCommand
+
+from assets.signals_handler.node_assets_mapping import expire_node_assets_mapping_for_memory
+from orgs.models import Organization
+
+
+def expire_node_assets_mapping():
+    org_ids = Organization.objects.all().values_list('id', flat=True)
+    org_ids = [*org_ids, '00000000-0000-0000-0000-000000000000']
+
+    for org_id in org_ids:
+        expire_node_assets_mapping_for_memory(org_id)
+
+
+class Command(BaseCommand):
+    help = 'Expire caches'
+
+    def handle(self, *args, **options):
+        expire_node_assets_mapping()
diff --git a/jms b/jms
index 24b71f4e4..6bd71849d 100755
--- a/jms
+++ b/jms
@@ -97,6 +97,14 @@ def check_migrations():
     # sys.exit(1)
 
 
+def expire_caches():
+    apps_dir = os.path.join(BASE_DIR, 'apps')
+    code = subprocess.call("python manage.py expire_caches", shell=True, cwd=apps_dir)
+
+    if code == 1:
+        return
+
+
 def perform_db_migrate():
     logging.info("Check database structure change ...")
     os.chdir(os.path.join(BASE_DIR, 'apps'))
@@ -116,6 +124,7 @@ def prepare():
     check_database_connection()
     check_migrations()
     upgrade_db()
+    expire_caches()
 
 
 def check_pid(pid):

From e9b174f342195268b5dbd8c5e9e27b64db13ac2a Mon Sep 17 00:00:00 2001
From: Bai <bugatti_it@163.com>
Date: Mon, 26 Apr 2021 15:33:51 +0800
Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E5=91=BD?=
 =?UTF-8?q?=E4=BB=A4=E8=BF=87=E6=BB=A4=E8=A7=84=E5=88=99Model:=20=E6=B7=BB?=
 =?UTF-8?q?=E5=8A=A0Action-reconfirm;=20=E6=B7=BB=E5=8A=A0field-reviewers?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../migrations/0070_auto_20210426_1515.py     | 25 +++++++++++++++++++
 apps/assets/models/cmd_filter.py              | 25 ++++++++++++-------
 apps/assets/models/user.py                    |  4 +--
 apps/assets/serializers/cmd_filter.py         |  2 +-
 4 files changed, 44 insertions(+), 12 deletions(-)
 create mode 100644 apps/assets/migrations/0070_auto_20210426_1515.py

diff --git a/apps/assets/migrations/0070_auto_20210426_1515.py b/apps/assets/migrations/0070_auto_20210426_1515.py
new file mode 100644
index 000000000..ca6ff4273
--- /dev/null
+++ b/apps/assets/migrations/0070_auto_20210426_1515.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.1 on 2021-04-26 07:15
+
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('assets', '0069_change_node_key0_to_key1'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='commandfilterrule',
+            name='reviewers',
+            field=models.ManyToManyField(blank=True, related_name='review_cmd_filter_rules', to=settings.AUTH_USER_MODEL, verbose_name='Reviewers'),
+        ),
+        migrations.AlterField(
+            model_name='commandfilterrule',
+            name='action',
+            field=models.IntegerField(choices=[(0, 'Deny'), (1, 'Allow'), (2, 'Reconfirm')], default=0, verbose_name='Action'),
+        ),
+    ]
diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py
index c1242fd7e..e0826da5d 100644
--- a/apps/assets/models/cmd_filter.py
+++ b/apps/assets/models/cmd_filter.py
@@ -41,11 +41,12 @@ class CommandFilterRule(OrgModelMixin):
         (TYPE_COMMAND, _('Command')),
     )
 
-    ACTION_DENY, ACTION_ALLOW, ACTION_UNKNOWN = range(3)
-    ACTION_CHOICES = (
-        (ACTION_DENY, _('Deny')),
-        (ACTION_ALLOW, _('Allow')),
-    )
+    ACTION_UNKNOWN = 10
+
+    class ActionChoices(models.IntegerChoices):
+        deny = 0, _('Deny')
+        allow = 1, _('Allow')
+        confirm = 2, _('Reconfirm')
 
     id = models.UUIDField(default=uuid.uuid4, primary_key=True)
     filter = models.ForeignKey('CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules')
@@ -53,7 +54,13 @@ class CommandFilterRule(OrgModelMixin):
     priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"),
                                    validators=[MinValueValidator(1), MaxValueValidator(100)])
     content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command"))
-    action = models.IntegerField(default=ACTION_DENY, choices=ACTION_CHOICES, verbose_name=_("Action"))
+    action = models.IntegerField(default=ActionChoices.deny, choices=ActionChoices.choices, verbose_name=_("Action"))
+    # 动作: 附加字段
+    # - confirm: 命令复核人
+    reviewers = models.ManyToManyField(
+        'users.User', related_name='review_cmd_filter_rules', blank=True,
+        verbose_name=_("Reviewers")
+    )
     comment = models.CharField(max_length=64, blank=True, default='', verbose_name=_("Comment"))
     date_created = models.DateTimeField(auto_now_add=True)
     date_updated = models.DateTimeField(auto_now=True)
@@ -89,10 +96,10 @@ class CommandFilterRule(OrgModelMixin):
         if not found:
             return self.ACTION_UNKNOWN, ''
 
-        if self.action == self.ACTION_ALLOW:
-            return self.ACTION_ALLOW, found.group()
+        if self.action == self.ActionChoices.allow:
+            return self.ActionChoices.allow, found.group()
         else:
-            return self.ACTION_DENY, found.group()
+            return self.ActionChoices.deny, found.group()
 
     def __str__(self):
         return '{} % {}'.format(self.type, self.content)
diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py
index 1640c7f32..37af8ea86 100644
--- a/apps/assets/models/user.py
+++ b/apps/assets/models/user.py
@@ -196,9 +196,9 @@ class SystemUser(BaseUser):
     def is_command_can_run(self, command):
         for rule in self.cmd_filter_rules:
             action, matched_cmd = rule.match(command)
-            if action == rule.ACTION_ALLOW:
+            if action == rule.ActionChoices.allow:
                 return True, None
-            elif action == rule.ACTION_DENY:
+            elif action == rule.ActionChoices.deny:
                 return False, matched_cmd
         return True, None
 
diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py
index 0c8eca3d4..e7059d04a 100644
--- a/apps/assets/serializers/cmd_filter.py
+++ b/apps/assets/serializers/cmd_filter.py
@@ -34,7 +34,7 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
         fields_mini = ['id']
         fields_small = fields_mini + [
            'type', 'type_display', 'content', 'priority',
-           'action', 'action_display',
+           'action', 'action_display', 'reviewers',
            'comment', 'created_by', 'date_created', 'date_updated'
         ]
         fields_fk = ['filter']

From 50918a3dd2b9e4c5684db1ff4d7d09e0917e9b66 Mon Sep 17 00:00:00 2001
From: Bai <bugatti_it@163.com>
Date: Mon, 26 Apr 2021 17:56:06 +0800
Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=91=BD?=
 =?UTF-8?q?=E4=BB=A4=E5=A4=8D=E6=A0=B8=E9=80=BB=E8=BE=91;=20=E6=B7=BB?=
 =?UTF-8?q?=E5=8A=A0=E5=91=BD=E4=BB=A4=E5=A4=8D=E6=A0=B8=E5=B7=A5=E5=8D=95?=
 =?UTF-8?q?;?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/acls/api/login_asset_check.py            | 36 ++---------
 apps/assets/api/cmd_filter.py                 | 63 ++++++++++++++++++-
 apps/assets/models/cmd_filter.py              | 21 +++++++
 apps/assets/serializers/cmd_filter.py         | 35 +++++++++++
 apps/assets/urls/api_urls.py                  |  3 +
 apps/terminal/models/session.py               |  5 ++
 apps/tickets/api/__init__.py                  |  1 +
 apps/tickets/api/common.py                    | 44 +++++++++++++
 apps/tickets/const.py                         |  1 +
 apps/tickets/handler/command_confirm.py       | 30 +++++++++
 .../migrations/0009_auto_20210426_1720.py     | 18 ++++++
 .../meta/ticket_type/command_confirm.py       | 25 ++++++++
 12 files changed, 248 insertions(+), 34 deletions(-)
 create mode 100644 apps/tickets/api/common.py
 create mode 100644 apps/tickets/handler/command_confirm.py
 create mode 100644 apps/tickets/migrations/0009_auto_20210426_1720.py
 create mode 100644 apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py

diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py
index ed525e6cf..d689675b0 100644
--- a/apps/acls/api/login_asset_check.py
+++ b/apps/acls/api/login_asset_check.py
@@ -5,7 +5,7 @@ from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView
 from common.permissions import IsAppUser
 from common.utils import reverse, lazyproperty
 from orgs.utils import tmp_to_org, tmp_to_root_org
-from tickets.models import Ticket
+from tickets.api import GenericTicketStatusRetrieveCloseAPI
 from ..models import LoginAssetACL
 from .. import serializers
 
@@ -48,7 +48,7 @@ class LoginAssetCheckAPI(CreateAPIView):
             org_id=self.serializer.org.id
         )
         confirm_status_url = reverse(
-            view_name='acls:login-asset-confirm-status',
+            view_name='api-acls:login-asset-confirm-status',
             kwargs={'pk': str(ticket.id)}
         )
         ticket_detail_url = reverse(
@@ -72,34 +72,6 @@ class LoginAssetCheckAPI(CreateAPIView):
         return serializer
 
 
-class LoginAssetConfirmStatusAPI(RetrieveDestroyAPIView):
-    permission_classes = (IsAppUser, )
+class LoginAssetConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
+    pass
 
-    def retrieve(self, request, *args, **kwargs):
-        if self.ticket.action_open:
-            status = 'await'
-        elif self.ticket.action_approve:
-            status = 'approve'
-        else:
-            status = 'reject'
-        data = {
-            'status': status,
-            'action': self.ticket.action,
-            'processor': self.ticket.processor_display
-        }
-        return Response(data=data, status=200)
-
-    def destroy(self, request, *args, **kwargs):
-        if self.ticket.status_open:
-            self.ticket.close(processor=self.ticket.applicant)
-        data = {
-            'action': self.ticket.action,
-            'status': self.ticket.status,
-            'processor': self.ticket.processor_display
-        }
-        return Response(data=data, status=200)
-
-    @lazyproperty
-    def ticket(self):
-        with tmp_to_root_org():
-            return get_object_or_404(Ticket, pk=self.kwargs['pk'])
diff --git a/apps/assets/api/cmd_filter.py b/apps/assets/api/cmd_filter.py
index 95aac8af9..c7ffb792f 100644
--- a/apps/assets/api/cmd_filter.py
+++ b/apps/assets/api/cmd_filter.py
@@ -1,15 +1,25 @@
 # -*- coding: utf-8 -*-
 #
 
+from rest_framework.response import Response
+from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView
 from django.shortcuts import get_object_or_404
 
+from common.utils import reverse
+from common.utils import lazyproperty
 from orgs.mixins.api import OrgBulkModelViewSet
-from ..hands import IsOrgAdmin
+from orgs.utils import tmp_to_root_org
+from tickets.models import Ticket
+from tickets.api import GenericTicketStatusRetrieveCloseAPI
+from ..hands import IsOrgAdmin, IsAppUser
 from ..models import CommandFilter, CommandFilterRule
 from .. import serializers
 
 
-__all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet']
+__all__ = [
+    'CommandFilterViewSet', 'CommandFilterRuleViewSet', 'CommandConfirmAPI',
+    'CommandConfirmStatusAPI'
+]
 
 
 class CommandFilterViewSet(OrgBulkModelViewSet):
@@ -35,3 +45,52 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet):
         return cmd_filter.rules.all()
 
 
+class CommandConfirmAPI(CreateAPIView):
+    permission_classes = ()
+    # permission_classes = (IsAppUser, )
+    serializer_class = serializers.CommandConfirmSerializer
+
+    def create(self, request, *args, **kwargs):
+        ticket = self.create_command_confirm_ticket()
+        response_data = self.get_response_data(ticket)
+        return Response(data=response_data, status=200)
+
+    def create_command_confirm_ticket(self):
+        ticket = self.serializer.cmd_filter_rule.create_command_confirm_ticket(
+            run_command=self.serializer.data.get('run_command'),
+            session=self.serializer.session,
+            cmd_filter_rule=self.serializer.cmd_filter_rule,
+            org_id=self.serializer.org.id
+        )
+        return ticket
+
+    @staticmethod
+    def get_response_data(ticket):
+        confirm_status_url = reverse(
+            view_name='api-assets:command-confirm-status',
+            kwargs={'pk': str(ticket.id)}
+        )
+        ticket_detail_url = reverse(
+            view_name='api-tickets:ticket-detail',
+            kwargs={'pk': str(ticket.id)},
+            external=True, api_to_ui=True
+        )
+        ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
+        return {
+            'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
+            'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
+            'ticket_detail_url': ticket_detail_url,
+            'reviewers': [str(user) for user in ticket.assignees.all()]
+        }
+
+    @lazyproperty
+    def serializer(self):
+        serializer = self.get_serializer(data=self.request.data)
+        serializer.is_valid(raise_exception=True)
+        return serializer
+
+
+class CommandConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
+    permission_classes = ()
+    pass
+
diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py
index e0826da5d..dff99298c 100644
--- a/apps/assets/models/cmd_filter.py
+++ b/apps/assets/models/cmd_filter.py
@@ -103,3 +103,24 @@ class CommandFilterRule(OrgModelMixin):
 
     def __str__(self):
         return '{} % {}'.format(self.type, self.content)
+
+    def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
+        from tickets.const import TicketTypeChoices
+        from tickets.models import Ticket
+        data = {
+            'title': _('Command confirm') + ' ({})'.format(session.user),
+            'type': TicketTypeChoices.command_confirm,
+            'meta': {
+                'apply_run_user': session.user,
+                'apply_run_asset': session.asset,
+                'apply_run_system_user': session.system_user,
+                'apply_run_command': run_command,
+                'apply_from_session': str(session.id),
+                'apply_from_cmd_filter_rule': str(cmd_filter_rule.id),
+            },
+            'org_id': org_id,
+        }
+        ticket = Ticket.objects.create(**data)
+        ticket.assignees.set(self.reviewers.all())
+        ticket.open(applicant=session.user_obj)
+        return ticket
diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py
index e7059d04a..2e60b31d7 100644
--- a/apps/assets/serializers/cmd_filter.py
+++ b/apps/assets/serializers/cmd_filter.py
@@ -6,6 +6,9 @@ from rest_framework import serializers
 from common.drf.serializers import AdaptedBulkListSerializer
 from ..models import CommandFilter, CommandFilterRule, SystemUser
 from orgs.mixins.serializers import BulkOrgResourceModelSerializer
+from orgs.utils import tmp_to_root_org
+from common.utils import get_object_or_none, lazyproperty
+from terminal.models import Session
 
 
 class CommandFilterSerializer(BulkOrgResourceModelSerializer):
@@ -50,3 +53,35 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
     #         msg = _("Content should not be contain: {}").format(invalid_char)
     #         raise serializers.ValidationError(msg)
     #     return content
+
+
+class CommandConfirmSerializer(serializers.Serializer):
+    session_id = serializers.UUIDField(required=True, allow_null=False)
+    cmd_filter_rule_id = serializers.UUIDField(required=True, allow_null=False)
+    run_command = serializers.CharField(required=True, allow_null=False)
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.session = None
+        self.cmd_filter_rule = None
+
+    def validate_session_id(self, session_id):
+        self.session = self.validate_object_exist(Session, session_id)
+        return session_id
+
+    def validate_cmd_filter_rule_id(self, cmd_filter_rule_id):
+        self.cmd_filter_rule = self.validate_object_exist(CommandFilterRule, cmd_filter_rule_id)
+        return cmd_filter_rule_id
+
+    @staticmethod
+    def validate_object_exist(model, field_id):
+        with tmp_to_root_org():
+            obj = get_object_or_none(model, id=field_id)
+        if not obj:
+            error = '{} Model object does not exist'.format(model.__name__)
+            raise serializers.ValidationError(error)
+        return obj
+
+    @lazyproperty
+    def org(self):
+        return self.session.org
diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py
index 5a5e6d803..c8413f83e 100644
--- a/apps/assets/urls/api_urls.py
+++ b/apps/assets/urls/api_urls.py
@@ -63,6 +63,9 @@ urlpatterns = [
 
     path('gateways/<uuid:pk>/test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
 
+    path('cmd-filters/command-confirm/', api.CommandConfirmAPI.as_view(), name='command-confirm'),
+    path('cmd-filters/command-confirm/<uuid:pk>/status/', api.CommandConfirmStatusAPI.as_view(), name='command-confirm-status')
+
 ]
 
 old_version_urlpatterns = [
diff --git a/apps/terminal/models/session.py b/apps/terminal/models/session.py
index 89e338143..6d85759af 100644
--- a/apps/terminal/models/session.py
+++ b/apps/terminal/models/session.py
@@ -11,6 +11,7 @@ from django.core.files.storage import default_storage
 from django.core.cache import cache
 
 from assets.models import Asset
+from users.models import User
 from orgs.mixins.models import OrgModelMixin
 from common.db.models import ChoiceSet
 from ..backends import get_multi_command_storage
@@ -79,6 +80,10 @@ class Session(OrgModelMixin):
     def asset_obj(self):
         return Asset.objects.get(id=self.asset_id)
 
+    @property
+    def user_obj(self):
+        return User.objects.get(id=self.user_id)
+
     @property
     def _date_start_first_has_replay_rdp_session(self):
         if self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION is None:
diff --git a/apps/tickets/api/__init__.py b/apps/tickets/api/__init__.py
index 6b519ef80..a6b5e39c6 100644
--- a/apps/tickets/api/__init__.py
+++ b/apps/tickets/api/__init__.py
@@ -3,3 +3,4 @@
 from .ticket import *
 from .assignee import *
 from .comment import *
+from .common import *
diff --git a/apps/tickets/api/common.py b/apps/tickets/api/common.py
new file mode 100644
index 000000000..fe5a5d1e9
--- /dev/null
+++ b/apps/tickets/api/common.py
@@ -0,0 +1,44 @@
+from django.shortcuts import get_object_or_404
+from rest_framework.response import Response
+from rest_framework.generics import RetrieveDestroyAPIView
+
+from common.permissions import IsAppUser
+from common.utils import lazyproperty
+from orgs.utils import tmp_to_root_org
+from ..models import Ticket
+
+
+__all__ = ['GenericTicketStatusRetrieveCloseAPI']
+
+
+class GenericTicketStatusRetrieveCloseAPI(RetrieveDestroyAPIView):
+    permission_classes = (IsAppUser, )
+
+    def retrieve(self, request, *args, **kwargs):
+        if self.ticket.action_open:
+            status = 'await'
+        elif self.ticket.action_approve:
+            status = 'approve'
+        else:
+            status = 'reject'
+        data = {
+            'status': status,
+            'action': self.ticket.action,
+            'processor': self.ticket.processor_display
+        }
+        return Response(data=data, status=200)
+
+    def destroy(self, request, *args, **kwargs):
+        if self.ticket.status_open:
+            self.ticket.close(processor=self.ticket.applicant)
+        data = {
+            'action': self.ticket.action,
+            'status': self.ticket.status,
+            'processor': self.ticket.processor_display
+        }
+        return Response(data=data, status=200)
+
+    @lazyproperty
+    def ticket(self):
+        with tmp_to_root_org():
+            return get_object_or_404(Ticket, pk=self.kwargs['pk'])
diff --git a/apps/tickets/const.py b/apps/tickets/const.py
index 591ead607..3397353d4 100644
--- a/apps/tickets/const.py
+++ b/apps/tickets/const.py
@@ -10,6 +10,7 @@ class TicketTypeChoices(TextChoices):
     apply_asset = 'apply_asset', _('Apply for asset')
     apply_application = 'apply_application', _('Apply for application')
     login_asset_confirm = 'login_asset_confirm', _('Login asset confirm')
+    command_confirm = 'command_confirm', _('Command confirm')
 
 
 class TicketActionChoices(TextChoices):
diff --git a/apps/tickets/handler/command_confirm.py b/apps/tickets/handler/command_confirm.py
new file mode 100644
index 000000000..33d6739e3
--- /dev/null
+++ b/apps/tickets/handler/command_confirm.py
@@ -0,0 +1,30 @@
+from django.utils.translation import ugettext as _
+from .base import BaseHandler
+
+
+class Handler(BaseHandler):
+
+    # body
+    def _construct_meta_body_of_open(self):
+        apply_run_user = self.ticket.meta.get('apply_run_user')
+        apply_run_asset = self.ticket.meta.get('apply_run_asset')
+        apply_run_system_user = self.ticket.meta.get('apply_run_system_user')
+        apply_run_command = self.ticket.meta.get('apply_run_command')
+        apply_from_session = self.ticket.meta.get('apply_from_session')
+        apply_from_cmd_filter_rule = self.ticket.meta.get('apply_from_cmd_filter_rule')
+
+        applied_body = '''{}: {},
+            {}: {},
+            {}: {},
+            {}: {},
+            {}: {},
+            {}: {},
+        '''.format(
+            _("Applied run user"), apply_run_user,
+            _("Applied run asset"), apply_run_asset,
+            _("Applied run system user"), apply_run_system_user,
+            _("Applied run command"), apply_run_command,
+            _("Applied from session"), apply_from_session,
+            _("Applied from command filter rules"), apply_from_cmd_filter_rule,
+        )
+        return applied_body
diff --git a/apps/tickets/migrations/0009_auto_20210426_1720.py b/apps/tickets/migrations/0009_auto_20210426_1720.py
new file mode 100644
index 000000000..e584c2560
--- /dev/null
+++ b/apps/tickets/migrations/0009_auto_20210426_1720.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1 on 2021-04-26 09:20
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('tickets', '0008_auto_20210311_1113'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='ticket',
+            name='type',
+            field=models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm'), ('apply_asset', 'Apply for asset'), ('apply_application', 'Apply for application'), ('login_asset_confirm', 'Login asset confirm'), ('command_confirm', 'Command confirm')], default='general', max_length=64, verbose_name='Type'),
+        ),
+    ]
diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py b/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py
new file mode 100644
index 000000000..92c72f920
--- /dev/null
+++ b/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py
@@ -0,0 +1,25 @@
+from rest_framework import serializers
+from django.utils.translation import ugettext_lazy as _
+
+
+__all__ = [
+    'ApplySerializer', 'CommandConfirmSerializer',
+]
+
+
+class ApplySerializer(serializers.Serializer):
+    # 申请信息
+    apply_run_user = serializers.CharField(required=True, label=_('Run user'))
+    apply_run_asset = serializers.CharField(required=True, label=_('Run asset'))
+    apply_run_system_user = serializers.CharField(
+        required=True, max_length=64, label=_('Run system user')
+    )
+    apply_run_command = serializers.CharField(required=True, label=_('Run command'))
+    apply_from_session = serializers.UUIDField(required=False, label=_('From session'))
+    apply_from_cmd_filter_rule = serializers.UUIDField(
+        required=False, label=_('From cmd filter rule')
+    )
+
+
+class CommandConfirmSerializer(ApplySerializer):
+    pass

From 5a3c67989b7ff58f81e7830f90573494661a7a46 Mon Sep 17 00:00:00 2001
From: Bai <bugatti_it@163.com>
Date: Mon, 26 Apr 2021 18:52:25 +0800
Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=91=BD?=
 =?UTF-8?q?=E4=BB=A4=E5=A4=8D=E6=A0=B8=E9=80=BB=E8=BE=91;=20=E6=B7=BB?=
 =?UTF-8?q?=E5=8A=A0=E5=91=BD=E4=BB=A4=E5=A4=8D=E6=A0=B8=E5=B7=A5=E5=8D=95?=
 =?UTF-8?q?;=202?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/assets/models/cmd_filter.py                         | 4 ++--
 apps/tickets/handler/command_confirm.py                  | 8 ++++----
 apps/tickets/serializers/ticket/meta/meta.py             | 9 ++++++++-
 .../ticket/meta/ticket_type/command_confirm.py           | 4 ++--
 4 files changed, 16 insertions(+), 9 deletions(-)

diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py
index dff99298c..72547da19 100644
--- a/apps/assets/models/cmd_filter.py
+++ b/apps/assets/models/cmd_filter.py
@@ -115,8 +115,8 @@ class CommandFilterRule(OrgModelMixin):
                 'apply_run_asset': session.asset,
                 'apply_run_system_user': session.system_user,
                 'apply_run_command': run_command,
-                'apply_from_session': str(session.id),
-                'apply_from_cmd_filter_rule': str(cmd_filter_rule.id),
+                'apply_from_session_id': str(session.id),
+                'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
             },
             'org_id': org_id,
         }
diff --git a/apps/tickets/handler/command_confirm.py b/apps/tickets/handler/command_confirm.py
index 33d6739e3..0e1fd0d6a 100644
--- a/apps/tickets/handler/command_confirm.py
+++ b/apps/tickets/handler/command_confirm.py
@@ -10,8 +10,8 @@ class Handler(BaseHandler):
         apply_run_asset = self.ticket.meta.get('apply_run_asset')
         apply_run_system_user = self.ticket.meta.get('apply_run_system_user')
         apply_run_command = self.ticket.meta.get('apply_run_command')
-        apply_from_session = self.ticket.meta.get('apply_from_session')
-        apply_from_cmd_filter_rule = self.ticket.meta.get('apply_from_cmd_filter_rule')
+        apply_from_session_id = self.ticket.meta.get('apply_from_session_id')
+        apply_from_cmd_filter_rule_id = self.ticket.meta.get('apply_from_cmd_filter_rule_id')
 
         applied_body = '''{}: {},
             {}: {},
@@ -24,7 +24,7 @@ class Handler(BaseHandler):
             _("Applied run asset"), apply_run_asset,
             _("Applied run system user"), apply_run_system_user,
             _("Applied run command"), apply_run_command,
-            _("Applied from session"), apply_from_session,
-            _("Applied from command filter rules"), apply_from_cmd_filter_rule,
+            _("Applied from session"), apply_from_session_id,
+            _("Applied from command filter rules"), apply_from_cmd_filter_rule_id,
         )
         return applied_body
diff --git a/apps/tickets/serializers/ticket/meta/meta.py b/apps/tickets/serializers/ticket/meta/meta.py
index ee8a402dd..12b576857 100644
--- a/apps/tickets/serializers/ticket/meta/meta.py
+++ b/apps/tickets/serializers/ticket/meta/meta.py
@@ -1,5 +1,7 @@
 from tickets import const
-from .ticket_type import apply_asset, apply_application, login_confirm, login_asset_confirm
+from .ticket_type import (
+    apply_asset, apply_application, login_confirm, login_asset_confirm, command_confirm
+)
 
 __all__ = [
     'type_serializer_classes_mapping',
@@ -35,5 +37,10 @@ type_serializer_classes_mapping = {
         'default': login_asset_confirm.LoginAssetConfirmSerializer,
         action_open: login_asset_confirm.ApplySerializer,
         action_approve: login_asset_confirm.LoginAssetConfirmSerializer(read_only=True),
+    },
+    const.TicketTypeChoices.command_confirm.value: {
+        'default': command_confirm.CommandConfirmSerializer,
+        action_open: command_confirm.ApplySerializer,
+        action_approve: command_confirm.CommandConfirmSerializer(read_only=True)
     }
 }
diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py b/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py
index 92c72f920..4dbdc08fc 100644
--- a/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py
+++ b/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py
@@ -15,8 +15,8 @@ class ApplySerializer(serializers.Serializer):
         required=True, max_length=64, label=_('Run system user')
     )
     apply_run_command = serializers.CharField(required=True, label=_('Run command'))
-    apply_from_session = serializers.UUIDField(required=False, label=_('From session'))
-    apply_from_cmd_filter_rule = serializers.UUIDField(
+    apply_from_session_id = serializers.UUIDField(required=False, label=_('From session'))
+    apply_from_cmd_filter_rule_id = serializers.UUIDField(
         required=False, label=_('From cmd filter rule')
     )
 

From 74c7b18dc4ac5b9c3d85ba68f55d1afc743b099b Mon Sep 17 00:00:00 2001
From: Bai <bugatti_it@163.com>
Date: Mon, 26 Apr 2021 18:54:08 +0800
Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=91=BD?=
 =?UTF-8?q?=E4=BB=A4=E5=A4=8D=E6=A0=B8=E9=80=BB=E8=BE=91;=20=E6=B7=BB?=
 =?UTF-8?q?=E5=8A=A0=E5=91=BD=E4=BB=A4=E5=A4=8D=E6=A0=B8=E5=B7=A5=E5=8D=95?=
 =?UTF-8?q?;=203?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/assets/api/cmd_filter.py | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/apps/assets/api/cmd_filter.py b/apps/assets/api/cmd_filter.py
index c7ffb792f..56cbbd6c3 100644
--- a/apps/assets/api/cmd_filter.py
+++ b/apps/assets/api/cmd_filter.py
@@ -46,8 +46,7 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet):
 
 
 class CommandConfirmAPI(CreateAPIView):
-    permission_classes = ()
-    # permission_classes = (IsAppUser, )
+    permission_classes = (IsAppUser, )
     serializer_class = serializers.CommandConfirmSerializer
 
     def create(self, request, *args, **kwargs):
@@ -91,6 +90,5 @@ class CommandConfirmAPI(CreateAPIView):
 
 
 class CommandConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
-    permission_classes = ()
     pass
 

From 7712c1659e8e971c4fb59453070afc5527237f5e Mon Sep 17 00:00:00 2001
From: Bai <bugatti_it@163.com>
Date: Mon, 26 Apr 2021 19:39:00 +0800
Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=91=BD?=
 =?UTF-8?q?=E4=BB=A4=E5=A4=8D=E6=A0=B8=E9=80=BB=E8=BE=91;=20=E6=B7=BB?=
 =?UTF-8?q?=E5=8A=A0=E5=91=BD=E4=BB=A4=E5=A4=8D=E6=A0=B8=E5=B7=A5=E5=8D=95?=
 =?UTF-8?q?;=204?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/locale/zh/LC_MESSAGES/django.mo | Bin 74719 -> 75634 bytes
 apps/locale/zh/LC_MESSAGES/django.po | 136 +++++++++++++++++++--------
 2 files changed, 96 insertions(+), 40 deletions(-)

diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo
index 4e146d83b81d1f910f5ed64ed4565a09bf68c181..f47606a0d8ee91e55b19356e5a7572a55bcd1b20 100644
GIT binary patch
delta 22848
zcmZ|X1#}hHyZ7-)NCF|jf&?eHyM`7o?ocSDI1Nsqc(GHgNYUa}+?^nW;DzEY1sWWJ
zm*Nx(yx-rMhu++~);sHNK6^jg_spD}K>zpJLBD4w{5)4f{H8iw3H=-=J$4CmoCtr%
z*-@SXSG~rLlhfC6>f_gxzi;9=<M1r@$JPYX_&81sHm2RHrj8R#xps5M8AE;37LM}+
z-oZbyT}#I~PJP8zj<elyJWkR!j+26pNAVO5u7B$|DRD!D;{@PAOoS&e2xBl6-owoJ
z(ahY=aWYY^glVuXvL|N{7RI5NA2*@;-Nf9?@A$QMoQwntU>>ZE1+bsF3@=i?fLC!{
z2geD)@f{r}2`<2dxB-K4hk4k%fLi!1OpZ@51;*>-IPsa^Nliu#!cYU`Gs96Gs$c*%
z#l+Ye<70P=_d}hK2eqJa7>aXI<88)3+=ub-7$(3|=+Quz$Rx!ZsFgj#gc!fGYY1xK
z45*3nV-EZbb7LFSiHyZ=I1fu=k}huhs;G$@VJ`d{bK=l0oWFLsl7K(%!mn^Y>Yk<V
z>JCr`bxWFIGW-d(gSn`kEw%W145GZx$|q18yN-Hho?<X2h;$d6GLrLGgIol(!*JBY
zQymp=i#m~Bs2z?*Jv?(Pz6&+*SycN6s13bCwNJuvs6Gp7+;CL9A!;M-JY>|NAL^cs
zMGdqV)nPMg#V1j3MGPjvXQ*4}+udDYLe%S<2@~VzsEKQ0I&6d5$Y4y0lTqV&7L(C{
z+bwVewV>-(et~+e5`O2#^I#&%Wl^tJEliE|Q3H0uwD<#R!r4~8-s%rq{2Fr0J<ba<
zx|eTJ1IO#(-os>=jdBLm!&njXV{OzuAAmZU5ttNzM!hAAP~)t$`d?Aw96<GZfXVO$
z`s)1;=;?M0MC~Xk>gZCUCdg;yaLh`%9u~)5SQJ-cX}pTsd757CIK@yChNDin5*EUS
zsD%#4gnIvfBBK>gLmlB_)V<w~8eku4VaHJIPNSZcYp8)<n;%g9{Cjh1m<YA=yqE&(
zp~i_ojoS%58lV>$4LAUGl#@|MwAkEY9x^Xua_S#p2s(Y-g(pYd%G{_4ikP39Rm^&*
zc1`<m{<^nqEf9$+_ce#27BUXQaUN=cmrx6TfVuD~YUgSDx&voHwabg@|GAaxpl(r1
z)Z5j+FXx|<%oGAz@k-P{n^5;`hj|POQjWn;4DRO+kPY>^l|>zKZB)NDs1LLrsD<`J
z-8v8InHq<hXO;rm@j_I`HCC|`vrs-@@q4J9yg^Nv@O$?bq(Sw|Z<ayryf*5HTc9Rt
zkGfUeP~!}>xMvC(4LAqYag|l<z|@rYqdrovpjLk0>R+MyeXw$({_Y8;M77I=`lQT<
zdQ0k{PGTtP18FpJ;vQ!S8BMSb(_plD9<|aZs9WSSz&)u@<g0>{1=W58YNzv16D&ua
z$U4-K??&C)<K`b2M)?LN(EIN@&|N?f>ZuOJRG1g_x>Q8%s48lrFRk1hH9;rT2Ku2U
z9*#PhDX3e!2-D(PD<4KJ=o|*?{l7{^13yIV_yy{o1q^ZrOpIDsM$`_ASUemxQFY9P
zO)xJG!BV&ewSidFcn?t<d2Tv`Ie$%@h>Rvkg<4r2vlwazWl{ICI@Z9>sD4{e6YNGU
zY(MHGj+%d<+TFq?_y~1kmDzQAZ0O<qE0XDMfha6b`4p;SvLD>nC@tou9ELjL8mI}I
zq8{4TsE4%&YNtP;`cF3Jpx&k>sQ!mgZ^OkOm?#~YhXit9{2}g7y8>94avju>{(w56
z)u@hpP!pd;4R{-MD_)rKhPv%SPz%bAs?U$Q#h+Wej)#nP(ipYk=2p?p;*pqvcyFtp
zjJk(&th@r%e<Nz5ZKzwY*W$-f<DRql6)Z;iK4wNw#$oP&<x%&hHfn|6pmx?3wevw(
z5hr0|Jcmm$?{HpOJcIfK96!PxZxQO*S&v#^G-`qSP$zK&8Q0@nBBP^xjOy?KwUEFc
z-43Cs_dFBowG2lstRCu~w?aKk-=lUu4fPN&M2)i=OX7Ca#LrL<t<Olw{SP3chC!%-
z(s(Nzrvz%C&#YV-H9-y3Eoh8k*cNrf!%-8Av-&xxlU#xR7>!!^UW=bb@B4p?j4u^`
zo6k`@dxaV}$tZWA)To8#!7NzH{07TV9*miA2kMr^U;zG&8Syo)#8ji*XKF8czyDt&
zqZQu8`1sh$FR>uycUTwmj&Z-i3_#tA)u<Es4HMu=)CpWfE%YwxL|>xbnvkE|cG=AW
zKXLx*SdxGyE|0mf8YaXZsE^J;7>F}0z7T^buR%??4fRwXLiKx(8aKgM_w`MNikCs%
z`)a6XuJ%~&e^xRr2*ksosH6H3b;L`|Ll{K)A?iq9qjvZKbrQkj+zGOxepnSiE$j=_
zI1N!7?1TEe8H_rSSsrV!2G!vd2IDnUhexPe^TK?K+L<%nT~HwEUZ=!Fm>adzl2)#W
zT3|iY#O+ZV8HS0`Gl5JFGQXfY9zb<Cg*x)Ps0p81`7NrQ-voCDNl**Uf_mBuqUtM{
zwNMNE3iZvYGwK9~VPd`iW60=}XeR0wM589&jhgr{>X|r${GanLe`p6|C%OaAKy6?(
z>PXLGc8o=}3!daIC^aUboE5duqL@<ee`Txq2E(Z6jM~vy)Iw$=D|Hs3R=&~PhMHgx
z>Y+M@y7#Bi`!<=6QP0$K)JJ*9&+hA8NHV`uos2#@+n@&Miy=4;b<gIb>UW@S&1oy&
zGvA|j7Bbmga2C{p@}b%nMxA6e)HwC6+zvh3X*V+JFc6F25X^_0urS_0y~oL?xNl1#
z)P!H6`hAPqaYyuyhk6DEVksPn8fOpc1fQW^>wl(j{yO3mQ{6yD)WG4WPsHk|x1bhk
zz%Nk)w6u6T)P#{3f&);uYCNXExtI($qQ>2iYJU#3@EcP(fBkTXC7^*bPID*Bj>#$K
zN8RHJR<4aY%GRibbwW+t!{Q^%@u-QWqjtI&wV>UY3eTbDd*~sfojpS}c#oPOWV-u4
zr$a3yH-=&vvk~T?+!d4J6myByZ^1C)Cr~Hy1PkH^)QJ?B;p!<*Mn}^C^{}-;{kR>B
z+Tje;1oO=esP_9X8=gVk``6}s)CS_sbT<%;I)SvPlPYfIYRG&Zr!5%`<iXUq7}MZR
z%#VMdI{MFY?^$Zp(Pls`Fb`_NBB)Qm&rr9lI_lYKgxYaSRQvBx?Z3xVdjE%#(bGK>
z)nSV@*kk3xs9SUvwezc31ph(}oO!nU^}8r$qTCF%z=5cvAB`G!2I`(KwDJn{{{3GR
z869D?HQ1{P%7;)BoU-^$)XMLp27ZRxsm~mDfr(HPrAGD3f_kQkp(d`5+CV+jiMB%T
z@BclmL4VZ1Lof^{qHe(k)B>-eCb)wd_z4z9zqxL|5}1l|Wz2`oFawUZ_%hT6cA;+7
z!MU8j8k{Gf6+SoLq9*eB#obvDYT#6;h2%%QzeQ0GXDRgV5OwtRP&;gn8mBwzr23;y
z;75y3`-S_j70tK63e*5mR^EeJ=@BbmMBSR37Jq>1{{}U%&ph`elA+3BsD%|WOQLQ~
zS*x$-A)}|g73v}Dk9lwe>SWfVc6tK!cAUj8@DZlN!t>qFh1#f{MxYkl4YklgsBuT2
zZtX-&i}O&o&a<6NN-~d7EB0OBHVi@SEHi4LJgAc?V&!sHUlnyr>R}FSj9Ty@)Pjbh
z#u<yc1yivYE<jGk<6N+cE2x#-NA1*ap?hyLU~b9<umm<i?Pvn(XlJ2zwg7d{*P|Bj
zo0Sit+Wm=YchkyGF}~jaH)J%Sv&h|P8q7s8Gisn3sFSH{^(|3H+#WSyZ`8s^p?16o
zHQ^d_6KdztsJG@IY9TS0i20p|*5Dtj@L%jcBq>k>=Ro}osDRpeBeNCyQSOYoHQiB1
zJqWdsAFVvm>Sv<*FGbz54d~GhwvzEa<){HJVotn)VHmi?H4o~fYGQi)7PY`3<{Z>R
zyA}0Xp2y-Ci&-%JQg`R&P$ybvDd(@FZb2Y3jzmqo0-NJ$v*0qmTu>f|nea81#L(q@
z$Y5Qph^tZ0$P>(iIacuV0PABv9ETAYu+pvXvXb-H0MiNNz!#_;q+8|wPzuLFlv`jg
zoPc@JZ#BO>U?B{}!KjI+VSPM~YcbOrxBdj`HIGG|m@2i=bRIIAxDskdoiH31VtkCj
zM0gu@q|Z>#LcDeE!xV~&=R<w4l);Qx*W!_={vHg*aTtWNP~&@+lJO(676UN~hu}_p
ziRIS26Bgg#eyy&K6^M7k{I~&g;Wey+2{!WAGFTT&;Y#$!Td0NKM?FLDk(2W{siNG^
z_5!FCR>maQ0JYLKs0Bt^eNWVme?aYQ66ynN9wx@ks9Ulh)h@<-jN0)B)I0&3v|;Xl
zax!`bGN2mt$7DDZwSXz8dp92gaJ#t|6H-2eTEJP<y}ggR1%8{|hczqap<D^Iu}IYT
zgH_i1Kaorr&c_tE7qx>6s0I9mn&>TR0sdRufr3%>=}`;MWfnr+%2L=GD`HAqj5^8f
zsD8WAql%+sG{6n>zBPD?s(+8#S@Ny!LJFXcJ{<KrRmHrx1`FdQ)C57ly0;`0wc{KZ
ziltC*S-oF5|Dt3f3FuxeMC~*R)nONEpi>xvF{pd|1a*`jQ2i2bb9bBxwUFFaZiJ~R
zw?wt;hZ<)zCcrt{IDf5Z2?4nii{p9JJq+6JKD~KR4^1H~hh<O$46^zkQMYC+YGIR6
zC%Oo=fmNtmvK2eyUd)F1J<;xp8ldh?1ZtvAW?w8zc{pl<-!L4HU=j4&;eK+K#0`|c
z#;h2$)3qS#nQDmHu>-3Acr1;cLuAsDiMPwW*BMbAieUk)ZsmTc1&+h?KKyeVYNB<!
z-4i;C+TkVCc(+hT{sDC&N%pw!e<su|EQ*ZB-~W=)2SioWJ+F;w&=Pg8B2fbk!EBls
zpWxQL?nm&l-!uvzR`@OU-p@U!{xXL7@}2SkCrUZRA-?ldpZPGaHqOSK>Y4EfuL2EX
za1|AEj`6Bdu6CT?fGJlx=`Q3A`cV!#<=)ezsAnKOYT?;YZ%awDHfEyS*6N3$+E2wi
z_zM=%$`6ytfd66+Oncfr!U`Bhxf!Z{5SGB%$Yz`~$cLFT`;2=+b<et=q^(e|XJ2y!
zCZ#;t%8OCY*hcjJ{=eG-$50R1B`aSyADeH@_~+bqDNs93XXV1EXP^{nf{JEc)N9zx
z%DqwThMwd6y|0Z`Oh%RGo6F3#R=?TYhk7<nq6WH&I?_j&3q#Ml3oDOJDK|zv^&3#_
zAEM@Yd7ksnN#<VyGTQ~$TBruSunrEuqId+gvk#aNgZ^-z>O81;S=0nIQ9EvA@komg
zu<{7=XOCs(nX6Gx|2EWXa|pH5i&lP!DJj1*lU;Ndk`vXg3ab8VOorX8Jj9%CuEBKF
zdw#RPHS?tzbjfXy88u)TOpOgu3+sZ~@j!Dl>cpm5{an-ut+DbkE5~3q;_r}gJx<tV
zcfb;66|*sF;&$dh^Jh#>e7U&;^|YTtE#N9@p?Ay=sD*_7>BdW-c3#(8=KgoKis7gM
z=A!Q5V$@F8T6q^{rhEvs^9QJh?X{J|V%&IsvovbFs#dOJeuG+22MpBvKaz}AJPG6B
zO4I~vQ3FI{I^1XRtL8&YOZ*My#pGAqui53W0OdBQ_kI#;oQ0@ySD0JSqm}$-4Q^V+
zebhtv%JjeL4v@l3XJ$bSm>c!*m9Thq)JE!|PV!3(#TJ+m`=B;F^{T!9YYAwe&8QCh
zQ1|{AYT%ow1-&%=uDRvpW){>lP}nSI@!F{Inwjm)?q>gM9=E}80y?7csCz!uTw-oE
z518k%JninF7MkO_+y8Ua2T>)|#F40R`=Q!-%&DjiuTnre{M9^aUPTS?6g9v*R6E}r
zt|_qu<y@$vY-#pGZD0~=!Ua~|iW=vjc>*=Q=R6s$<eF7Hvhs6GNZh&Ub_~KC6hlz$
z!_6wF2^*po*w*5G%wgt4b1rHjE8Vik*+51s*<}@n+zRKU8DsJLsGa_eY9DyZ9Vj`f
zoEEjPY!)wU<x*y4)JJ>+tcv|H!13^~k<rQzqXvpW4HS#o*>fw$yX}^fnqjE+g|Io6
zM!k+RP&<sW`t7K3_M^r<XZ1H!&-~6kG8yri0+{TM+aVol$9Ylr@(a|!O;H22HY3de
z<|uP2YGVt{%@#j|8uvVUfByev74OUhvF=1EQ4^)Na&FXXRtUADvKFs{YS+kYgPAFJ
zGbdqw%261AH_UsnoWD9gAt3*?iom;e)TnrvncpmpS&3K0aO`aHC8!BDn%gYC$2@Lc
z#)`DNdzbUq1bObcM^zFFQLbU-0ahN5+WBtO1jkW3Jcl}oR~8Sr?~aoQy$eUR&u`^o
zR<3}Vh*$GiMF$Kd&>OR%;`wkfYC)$_@4N2<_caX1RFqqx?qy%pGcpl%&zG9p%wwpZ
zA=fYv-=P-h@qg%MLd;C473VXHqB@ke@)xKHYFfMnYM>5Q?rnNd<BT>Zn$yi+kbeC4
zKV)*!a3gBxm)#2IqZ$0jU3nVRM8#1@UJ-Tg8(@7Li*xWQuEPP3?d|!??e_(0LG`WN
z#9QY6M_8Z>>L|ZQ?eJ%dFEKZw2H1@m@ORYvejD}E?IUVop-<dfl@o(0x4?wh*~)#e
zB;{e~{rx|hj0Qexo<ViIgj!H6>K1&o`oO>4_DNCgLd|TbhpI5DeSM36gGnfNF#DnA
z8-<>vWG0YFgY!`xx1%raHxHs7t|RD=*D*cDVq%Q<)cwb$5VImSAl?IYk_XI-sEHq<
z=6m*(`@fJ(@H2OUb*Kgh%wy(R^D<VT{sz{>tk2zVKHV^c@@&+JtVN9zZRLIDNz|>1
zvGR-O+<)!Z=Y`uL$P6*lU>@qjFag%Y6!;}-f{v&OyPJc|F{p=mx|MgJ7I+x-xe;UW
z4<0faDCnizAO!VLWyg3}9(7b-U>LSQz4smr!fB`lEJRJP4%L32#bYdf-^%}@e%1uO
zayRbDMJ6SIBB%kXp&q(#%w85BhdQbG=1SDUH=#}@8nux9sBuoC9=h8We_{H(cIy)(
z^&TfP8Fegdmc?9@Yg)M%YQ;m)y8zULE3H1t+=FU&+{%wo3;D;&!EfAo(qK~JIlOZJ
zOIk%WREI{Wd)@)HgUP7(cM+z>wO9fV;spGFI>GV(xF1xDu@L1wR(^^_DJOdC)>lBa
zZ;m;c-|1-;vlXDc6*ciS)Id*A9m3wZ1C&6WSOe58Xlb^ycvsX(^+#=JxW%WV+Ap^F
zD)j#S|1JxhHgBUA@Y;<3uRBmE>WK58PNKTm#_9)Hc|2-?%gjxviT9#T_7G};&;RBA
zCnxjX0?FRH73s|Es1qq@euerX;X%D#YfuY1gWBm+)I@$CT!T;x2{ALFZe2difo(r<
z{^~G>fCiXk4Hsh?${SE8an#~}n)l3?sQ$hmT?0`QCPj^x0oA{dmCIYXA*x+lk7c@}
z1{#EUak!P&n_E#wz6;gy5A!zaD4(MGzr%Fs>-c!f8By)CTe%2oLE)ySIvK6>YirO2
zHDFIng@Y_U1vSuYb2+NtX4HU(P!nIY@(a|0{e0YZ8Bm{mg;3)(z>IqTTUuZ+YM`I2
zJR3FOa@0WkQ9Hb1^|#HZ<_A>!AYXTaoT&E2to)f-4fQrP@|HdPTqdK5`<bIqE1iLA
zxE=M7o<cp0w^2I?j^|F022~ETa(>ha7BkD5)yxK{4K+jWfB)B;j8-}n^~p3Eb+ofk
z1Fb<#uod;mx8LH&E&c~`l+JA{$Mf^?es@fYS%{ZG^>2l0*A+GHSoHq;|1>gMz+7`N
z>O@vqIojNZ8tAZ<FQ5jxV)47?3)9EnZJ!u*lIc(zD~y`&3x6N?`+rSq*aS6Tgf-}9
z@jmz&@nNWYyxZ#kL=Aiswct0X_d7v=J8?PGgtbr~JS|a2KL&M+7X^6Sl^h_TogG6B
zcnP(_C*~{E!rr6W#gFeEWeQZeD5`yV)Pz+n-qdW5I{IFyg-$jXdB|wODD)n!Ra`**
z!V!!5Isd|R61e4HGd1dmQ#RDV1yKtvXI3{Gq8{SrsD*xKdis&k#2#}jY6ml|!D`fz
zMVZGee#gp>umkn}fo{7<b0lgZ^Uam0i8rAh;%H=EkF%eQR(2ZonSKW~VZwy&!m^r$
z%rdAGt6}9<sDZm$xfkk7>JZd8BdvbExeRquYcV17JG;o}hst5pNxZfOenIX4Da`z+
z9aTX+JoT^~_CWR9h+4>Q)WpA=*DU@9)$Swet6ebvD&c+qlaf)#3}!ae51+hNu8i7o
zE%d`Ss9V$?wUhp+fyY>UI%)wcE&dy7;ipjZT(<ZP^k{&;$jA?<hKUoo1ExmBvzx^+
zJ>|-%1++o6?`?We6O2YJXrejSTxo7K_b0OV|1<%0yo#FO4r*ZE#BMwob5Tx?`b7KO
zY>C?GP*lI!7>d857H|sF;uX{<>l@TKfk}M4zbO?-!uzisHXxu5ZBPxmp(dJP<!G!+
z`8HO;Vo80xzkCir-I~p)pAAQ`G(N{tSRk4Efz{p|hI-cK;a7OvLuM$M>>)nh|14%X
z>J#h=YTy*f-38`Bl}lq7)<X^S9qM76hx)F#7j+`fP;XOE3Lo#^1sB4fDYr!3x@V@x
zKcyQ;jrw_?*Q|)uD1VKWaUtrK+(VtzGt|j_L=BiEm1`DMyttJsqaNla7Vm-TKh)LZ
zOt8Qla|P;I*n*mHCw9YwcmPX=y7&4$W}uvwf9%sjOJO(;L7hxA*24SP1WWM0Qr6Fq
z38?XpVu;@VGh|W_xP!^?tr?uwU1>(t4)a>MENXz7s9Vwib>tnaJOp+0(@~%KJ5bNY
zpQs;R@zS{)%Y*^U@8l+<2?|+7CG1bRwi@6e)YE&?>aU}2)dMU0@b9r|7lc|+2<jH(
zGE1S}j%sEE>fs)W-oO7_LPqywqq!TkqhnUSgPQP}l>;-lJ4l7<7iQ)+OQU`$RW%!+
z7T6ps;kT$~VtEGMe^u<W28U56aSC<sZ<&v+{<YPAuz0eJ?gG-G`sKng_!*YM-_4Jx
zg_g?Xo=jy_`$m~~|MkPCg9Qd+YswQ)C-4-tkc44w#}ue?HY*n}%bIn}7N`j$Q4iw)
zE3ZI}yT!^UJ!EvRVo(D=Lapo-s$pPeALnz-f_mCpq6QvhjzJyibks@yiTdGk6E$8e
zs{P+q|K9Y=;<on$k?BH%5Y&Vo)B<Lp23mz0XdP;xZK%I``5iT3NLKfQCk<w&Tm$ui
z)EyV$&!~^!64~6RzdmaGuE-~?$N7PbzMHMUqIeGVnk2~XHq3^4s!O6inrovL(g(GG
zVaP{=GY0kAEy&?6{CCvCkDw+zXWm8ie~U@<{>RVhHVj1#m>IS6T&QOwAL<sBM=hit
z>ML10WM1#p%`)A5`305E&8=LL+NZ>JXh5$1s8jxibcy`09vc2b1HGRs$QLFBP?43G
zt|sPO^2KdX#c$bw$|vAUK|k8{B%jLK_oVGD(o))uC!UvhdD3O8SHH2;c@EO(CmT2m
zfj}B0C+VT9Y>lVJnd}5@2as-Ctgxw{X5C49C?};~dHhH@l6*mIi{D|WjY-z&%jb`C
z(I&r4pq~xK_YkKD4JVLpswG!8>OSCO>ds&T($D1e2^wXC-n0JsDJLNQ1LZ!n*+h9h
zcBgzCvr}H?ZA<TC)|vLsMS{HYP6g5t%3CRSCAA`XNV=w4`$x3V^$qb|q&}prl&=vV
zL7QK_weBCuR7b2eX$~oZHtC2})#`QCCT%1wBB-mi4L+OlNjg-tvg*=P{)_lgYdel|
zW>wh5mnY{k$(MA&+Ni1p?M6|qO8nF1X+Yt*mGY5)XdTueZ;10_oB`sIA57{%x<NWl
zI!Buk^dE{FuqTtmy)x2<Z}i?*v^N$1Smm#@EAQj9bAF-n({+mAAqtI1XNWiXRAIKp
zJC=Vy{hy@v^f}2Saj#u*3Zu!#n_Ry^a!*EE+9JG2>P)ZwjPMQlowTYOr&oeFBW5Na
zMH)ui@uZuy-$1#Jwb_DGiLb?#`02_`yN0Cu1Xji4L9IZc25AC~h7&A>W$@9b9{4Y@
zLJU;OViDwDkW!MGS*%!`_R9M50ibIsef=3<S1_(7tsuXbzJv6l+$WHl!q22$<jc`P
zR|%{{tSGLf+?BdAq&&o)5bI4|S3Am!tdASv9uT`n!3RrPyKdxlt)p!qi+f8xjmIig
z@j3bLsjNwQM1D6N^!4@A^^VLg;vvMl*kJc)%dcim2jcgMEhew)3}yX9I*lWUm89;9
z^;tno*JJdIb@NUG3fV~ctnwk9a*-cnoz(0uu?wVz#Jb}#QeyHm;<Wjm{6C~n(r5Gu
zV5jvMXBKULx3ME|3n|F^|7wCp14yq3d~XdXdM~~lI>Q*~3hL4i`#t1;CDw(uXDR0(
zWg|rspN9G|rYk9JbUigA$?v026WY}!cEdX+_rC)b)#6M%-CMzTYZ@*16#p~`_99)U
zO=tRbB2^^SA>|^SpnV%_OVO!l`I+R;lkQUPXyd=9?GO5>n_+<rRL&tkmWtuzzr(ha
zPtztn<?ZS~nV;$2s|$ZFB(7^9Hl|+JZqgoQxbl(st;R`1{br0$n?A&TBz2<fIZwq+
z8!!g{CMBRz9ZW*qEgHrr>AFPBACC~PLeh1EwtSKF{(`6SFSdwdDsVNXPG9zPowh!)
zm_u{u{?~Bpy#E6ZeiL;PQPGG7scoRqW=l+A1C*uRrz<b9!IYN}$U>X_q^{(9TH8XD
zH<J2FVtdh_htiJ}r1xLfbb{$HkOumH`ou4q|6R4n9kB9z+EuZ<zNLgw)|Yl&cj!BX
zG?08e;%!JJyfysS3AEi{{eo$u>x0z%*Hq&AnpBNoR~nYU?hH`D8mA^U)&@}9Z!NB0
zi58J!Xxr9es*lGeK3!2{rdwTEe9k!f9jpq;Q;t9nGNl>dYbtJ#*Oij;3hD}yMv|X|
z|B`N!Do~%u+lqhm!!XN>xr`A_dwsptRfT*(V$E?OZYS1)G3@g2H_f~Oq~fG?7Tt{c
zni^x}W1l)Oz+Pe<N&4MuA88h8Cw<#crz<h}<h1vv9AGiUuM#gnc@SwX`AXh8&cC@0
zUW33CD$Y^9PKOWVb%m1pkgretDAvYy*orv6=X<}O-?g??@h9RfN#BxxPg{M&HzSoL
z)&n<?4wB!Dqjdl4G01BQo5)Wk4I=-ScXh0q@=D4bN&6VgkJOR0n{rOlVp37k=ft|u
z=Q!;zlOIg%OOmeMHr90VBgubl@xzooulVDT1uoz`Yy1cK)eJO)dR>*t|Bkh&KY{Zp
z>#AgZ|0SQDbdgkmr0WmbuED;fSlSJteAfD@+jsi;uWKafM=JY~o{_HEpsJfphf<`0
z#C0{bx)a3yCjCM>N&FFZ!xYr7u{Oo%v&{1OsJ}pc1Dt?)N#6IbtTmWNaS!=JbSg}q
zzmas#(D(qU6!DhW2~%Ms>!TrbJtqA_`<ldmw7S}qKO+sGoP_$B<aJHNiFj0H?f(F&
zE{(=udV*gwfv&^E0!WF;FGXEnSY3o$boSbyN$?%%E86A8`BuM{{5kT6>E}n?P#fog
zjpIH4A~ecG#T6QEBekUb(Hee3ej%|ZbSOi981cL~n3%3SF5W-K5dU<2M!VY-Ued1v
z@!xI2PxX_jZ|Lry_mRr0aXPC}5mFXn^+}zrt{9zW*+Au~zeRa}oPpaAdqa6UsXh5m
z*BA8nrR^mG4^h`_(t7@?#W_#qOoDF+gwgRJ`Rmqbmrbk=DM{r>zp21gleW61#A)+_
z@_JH2>V}YBGEOw*b@(&t3UTrNIZWr@ivgbD1Uj6j;WW}4bs&9CU4K$R@{Qwk{FHq`
zydQ;^wul9c@t*P#;+aX4iQgfWA<o~bI-^PVh@ZwNlK1{=u#Ys*)eLV_agtP&{BQV_
z6izBlIh@YAij!KAUrg*f>V|rw|MN#e>Zo#j;`|`$MBor<l#P)|?|*LsKM)9~@fG}u
z^ga1p465s2%1_;_vx&BkNq<|pIyRxK>pksD&~_QNvcCN(A0UmVO%GBnYdeeB@7^}-
ze=UWQ)*&r<UBhuXW+T4X8{s6Zvx?eOD0P*st|IN%lNu7sVeMkcXD00+9-r6(+BPR0
zCKhgOu6xL&Ah??J75SGqiUyyqtHeG~-i%3c3+X<oJ>^L>3b2OpiDe-_8+AQn(jeMg
z!mM~4dsy8xa}aS)e74fU?c+?MQ4GQDG<ZvDM*4!758kyl8_AELjV@nIM}2+rt;zc+
zPYR=rt_<YkUK44vir8D6ql!=8|5sV7HB<~C-+}?2(=Z9?HTiKipqlL<|EIMV=_v0b
zeM#Tvq*J71^lOZ-sh>xF8+l!M$RD9?8d6hY=X|;UM^(cql%q24Rl_>{L9l6@id@8>
z#3^TrlUH^*gSRJ*q+MxTL%I`Z?3|R_ktPybulv7=LUJ2qCFOCXFNx(wUCZ$g(kWu&
z>GYOVo$@NmzfkXo#V9`@Uysy_^fzsm;c)6cU8~7&p)iiLllIlnGnqdcQ4x!}62$57
zghA%ea2iQhR?=-^pRNMLlTnz7e-VF0-_OXOB9<L%6Dv$wqIzOyssD;Jk8%;xZv9o)
z5E?wR5w8)<N`o=P{_i!Ey2{i|BK0M|1Xt4NC-Mn#C*=b43$eb>h!-bSr<@3rQ?KhC
z<yxeH-gb5Q(MoW?w~G6M52*NPjWgg`Z$r1iZ{({o*c+>_M}7)bNl7`$Z}tA2$}4jI
zq-ateYpMQ)EWeylUQ<_z*fxt*Hy2X3gZK-|18AdbHnBfRx^k1gr7ou=<tM*{y0o;7
zBwyG4M?3~5SdG8}(mNYyG`4l3K783Yb%Az0x^)d}+pTNY){)<awd>fWS45An9=*Fn
z^mHn8@7|?j1VOdv8PT(6$8M36Yj&NJt4Ht1u+}|$M)dlxi6S;eoY>@DT~qn?=pA*j
zYsJ9$ZM#Lb>)4}fRI<J^g2TLXcKu)TIaT6J6SZtu;&>U`M|SHG5mqUpN3V|UI<{@y
zD<Z6ML=QL1Qlg@M92*?au3eXokr7d4CYKHPZ;F01_xToc7uX=q!j$j%ea~JIUBi0!
zjOgL0+^b_(cB-60QRU{02+tckcgekxEALF&c{_UXz1<^Y`E%LuJELYgcc(<%TesD%
zzP)4Cy>)XZ?>xV#&b{3e?=2X6ch>NGn}^@tzRa5a7j*~vztOu<(RZWg-JP;LcGTAY
zRTow7!m9)UcNUL}o&Rf8xofdO+5f-miMx`hY!7?K3%Rp<=iRAG?yVbgd)v}`YZsG=
zn(<eWL;-h~M&I4OJL>Se3I6`xk+(MX@%h`g!GDXirT<s9MgLd+U(1ahy^IaVu3iv3
Tb-nXHO}u;A+Q83eT&DjA(~;-5

delta 22039
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

diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index 49694c4a0..d1fac4de6 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: JumpServer 0.3.3\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-04-14 17:52+0800\n"
+"POT-Creation-Date: 2021-04-26 19:33+0800\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: ibuler <ibuler@qq.com>\n"
 "Language-Team: JumpServer team<ibuler@qq.com>\n"
@@ -43,12 +43,12 @@ msgstr ""
 msgid "Name"
 msgstr "名称"
 
-#: acls/models/base.py:27 assets/models/cmd_filter.py:53
+#: acls/models/base.py:27 assets/models/cmd_filter.py:54
 #: assets/models/user.py:122
 msgid "Priority"
 msgstr "优先级"
 
-#: acls/models/base.py:28 assets/models/cmd_filter.py:53
+#: acls/models/base.py:28 assets/models/cmd_filter.py:54
 #: assets/models/user.py:122
 msgid "1-100, the lower the value will be match first"
 msgstr "优先级可选范围为 1-100 (数值越小越优先)"
@@ -66,7 +66,7 @@ msgstr "激活中"
 #: acls/models/base.py:32 applications/models/application.py:24
 #: assets/models/asset.py:147 assets/models/asset.py:223
 #: assets/models/base.py:255 assets/models/cluster.py:29
-#: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:57
+#: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:64
 #: assets/models/domain.py:22 assets/models/domain.py:56
 #: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37
 #: orgs/models.py:26 perms/models/base.py:57 settings/models.py:34
@@ -84,11 +84,11 @@ msgstr "激活中"
 msgid "Comment"
 msgstr "备注"
 
-#: acls/models/login_acl.py:16 tickets/const.py:18
+#: acls/models/login_acl.py:16 tickets/const.py:19
 msgid "Reject"
 msgstr "拒绝"
 
-#: acls/models/login_acl.py:17 assets/models/cmd_filter.py:47
+#: acls/models/login_acl.py:17 assets/models/cmd_filter.py:48
 msgid "Allow"
 msgstr "允许"
 
@@ -98,7 +98,7 @@ msgstr "登录IP"
 
 #: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:26
 #: acls/serializers/login_acl.py:34 acls/serializers/login_asset_acl.py:75
-#: assets/models/cmd_filter.py:56 audits/models.py:57
+#: assets/models/cmd_filter.py:57 audits/models.py:57
 #: authentication/templates/authentication/_access_key_modal.html:34
 #: tickets/models/ticket.py:43 users/templates/users/_granted_assets.html:29
 #: users/templates/users/user_asset_permission.html:44
@@ -117,7 +117,7 @@ msgstr "动作"
 #: authentication/models.py:97 orgs/models.py:18 orgs/models.py:418
 #: perms/models/base.py:50 templates/index.html:78
 #: terminal/backends/command/models.py:18
-#: terminal/backends/command/serializers.py:12 terminal/models/session.py:37
+#: terminal/backends/command/serializers.py:12 terminal/models/session.py:38
 #: tickets/models/comment.py:17 users/models/user.py:159
 #: users/models/user.py:707 users/serializers/group.py:20
 #: users/templates/users/user_asset_permission.html:38
@@ -149,7 +149,7 @@ msgstr "系统用户"
 #: assets/serializers/system_user.py:192 audits/models.py:38
 #: perms/models/asset_permission.py:99 templates/index.html:82
 #: terminal/backends/command/models.py:19
-#: terminal/backends/command/serializers.py:13 terminal/models/session.py:39
+#: terminal/backends/command/serializers.py:13 terminal/models/session.py:40
 #: users/templates/users/user_asset_permission.html:40
 #: users/templates/users/user_asset_permission.html:70
 #: users/templates/users/user_granted_remote_app.html:36
@@ -158,12 +158,12 @@ msgstr "系统用户"
 msgid "Asset"
 msgstr "资产"
 
-#: acls/models/login_asset_acl.py:32 authentication/models.py:45
-#: users/templates/users/user_detail.html:258
+#: acls/models/login_asset_acl.py:32 assets/models/cmd_filter.py:62
+#: authentication/models.py:45 users/templates/users/user_detail.html:258
 msgid "Reviewers"
 msgstr "审批人"
 
-#: acls/models/login_asset_acl.py:86 tickets/const.py:12
+#: acls/models/login_asset_acl.py:89 tickets/const.py:12
 msgid "Login asset confirm"
 msgstr "登录资产复核"
 
@@ -281,7 +281,7 @@ msgstr "自定义"
 msgid "Category"
 msgstr "类别"
 
-#: applications/models/application.py:16 assets/models/cmd_filter.py:52
+#: applications/models/application.py:16 assets/models/cmd_filter.py:53
 #: perms/models/application_permission.py:23
 #: perms/serializers/application/permission.py:17
 #: perms/serializers/application/user_permission.py:34
@@ -348,7 +348,7 @@ msgstr "目标URL"
 #: applications/serializers/attrs/application_type/mysql_workbench.py:34
 #: applications/serializers/attrs/application_type/vmware_client.py:30
 #: assets/models/base.py:252 assets/serializers/asset_user.py:71
-#: audits/signals_handler.py:46 authentication/forms.py:22
+#: audits/signals_handler.py:58 authentication/forms.py:22
 #: authentication/templates/authentication/login.html:155
 #: settings/serializers/settings.py:93 users/forms/profile.py:21
 #: users/templates/users/user_otp_check_password.html:13
@@ -516,7 +516,7 @@ msgstr "标签管理"
 
 #: assets/models/asset.py:221 assets/models/base.py:258
 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:26
-#: assets/models/cmd_filter.py:60 assets/models/group.py:21
+#: assets/models/cmd_filter.py:67 assets/models/group.py:21
 #: common/db/models.py:70 common/mixins/models.py:49 orgs/models.py:24
 #: orgs/models.py:422 perms/models/base.py:55 users/models/user.py:571
 #: users/serializers/group.py:35 users/templates/users/user_detail.html:97
@@ -624,30 +624,38 @@ msgid "Regex"
 msgstr "正则表达式"
 
 #: assets/models/cmd_filter.py:41 ops/models/command.py:25
-#: terminal/backends/command/serializers.py:15 terminal/models/session.py:48
+#: terminal/backends/command/serializers.py:15 terminal/models/session.py:49
 msgid "Command"
 msgstr "命令"
 
-#: assets/models/cmd_filter.py:46
+#: assets/models/cmd_filter.py:47
 msgid "Deny"
 msgstr "拒绝"
 
-#: assets/models/cmd_filter.py:51
+#: assets/models/cmd_filter.py:49
+msgid "Reconfirm"
+msgstr "复核"
+
+#: assets/models/cmd_filter.py:52
 msgid "Filter"
 msgstr "过滤器"
 
-#: assets/models/cmd_filter.py:55 xpack/plugins/license/models.py:29
+#: assets/models/cmd_filter.py:56 xpack/plugins/license/models.py:29
 msgid "Content"
 msgstr "内容"
 
-#: assets/models/cmd_filter.py:55
+#: assets/models/cmd_filter.py:56
 msgid "One line one command"
 msgstr "每行一个命令"
 
-#: assets/models/cmd_filter.py:64
+#: assets/models/cmd_filter.py:71
 msgid "Command filter rule"
 msgstr "命令过滤规则"
 
+#: assets/models/cmd_filter.py:111 tickets/const.py:13
+msgid "Command confirm"
+msgstr "命令复核"
+
 #: assets/models/domain.py:64
 msgid "Gateway"
 msgstr "网关"
@@ -775,7 +783,7 @@ msgstr "用户组"
 #: perms/models/application_permission.py:31
 #: perms/models/asset_permission.py:101 templates/_nav.html:45
 #: terminal/backends/command/models.py:20
-#: terminal/backends/command/serializers.py:14 terminal/models/session.py:41
+#: terminal/backends/command/serializers.py:14 terminal/models/session.py:42
 #: users/templates/users/_granted_assets.html:27
 #: users/templates/users/user_asset_permission.html:42
 #: users/templates/users/user_asset_permission.html:76
@@ -984,25 +992,25 @@ msgid ""
 "The task of self-checking is already running and cannot be started repeatedly"
 msgstr "自检程序已经在运行,不能重复启动"
 
-#: assets/tasks/push_system_user.py:192
+#: assets/tasks/push_system_user.py:193
 #: assets/tasks/system_user_connectivity.py:89
 msgid "System user is dynamic: {}"
 msgstr "系统用户是动态的: {}"
 
-#: assets/tasks/push_system_user.py:232
+#: assets/tasks/push_system_user.py:233
 msgid "Start push system user for platform: [{}]"
 msgstr "推送系统用户到平台: [{}]"
 
-#: assets/tasks/push_system_user.py:233
+#: assets/tasks/push_system_user.py:234
 #: assets/tasks/system_user_connectivity.py:81
 msgid "Hosts count: {}"
 msgstr "主机数量: {}"
 
-#: assets/tasks/push_system_user.py:272 assets/tasks/push_system_user.py:298
+#: assets/tasks/push_system_user.py:273 assets/tasks/push_system_user.py:299
 msgid "Push system users to assets: {}"
 msgstr "推送系统用户到入资产: {}"
 
-#: assets/tasks/push_system_user.py:284
+#: assets/tasks/push_system_user.py:285
 msgid "Push system users to asset: {}({}) => {}"
 msgstr "推送系统用户到入资产: {}({}) => {}"
 
@@ -1076,7 +1084,7 @@ msgid "Symlink"
 msgstr "建立软链接"
 
 #: audits/models.py:37 audits/models.py:60 audits/models.py:71
-#: terminal/models/session.py:44
+#: terminal/models/session.py:45
 msgid "Remote addr"
 msgstr "远端地址"
 
@@ -1094,7 +1102,7 @@ msgid "Success"
 msgstr "成功"
 
 #: audits/models.py:43 ops/models/command.py:30 perms/models/base.py:53
-#: terminal/models/session.py:51
+#: terminal/models/session.py:52
 #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:43
 #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:74
 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:40
@@ -1250,11 +1258,11 @@ msgstr "运行用户(显示名称)"
 msgid "User for display"
 msgstr "用户(显示名称)"
 
-#: audits/signals_handler.py:45
+#: audits/signals_handler.py:57
 msgid "SSH Key"
 msgstr "SSH 密钥"
 
-#: audits/signals_handler.py:47
+#: audits/signals_handler.py:59
 msgid "SSO"
 msgstr ""
 
@@ -1486,7 +1494,7 @@ msgstr "删除成功"
 
 #: authentication/templates/authentication/_access_key_modal.html:155
 #: authentication/templates/authentication/_mfa_confirm_modal.html:53
-#: templates/_modal.html:22 tickets/const.py:19
+#: templates/_modal.html:22 tickets/const.py:20
 msgid "Close"
 msgstr "关闭"
 
@@ -3036,7 +3044,7 @@ msgstr "正常"
 
 #: terminal/const.py:34
 msgid "Offline"
-msgstr ""
+msgstr "离线"
 
 #: terminal/exceptions.py:8
 msgid "Bulk create not support"
@@ -3046,15 +3054,15 @@ msgstr "不支持批量创建"
 msgid "Storage is invalid"
 msgstr "存储无效"
 
-#: terminal/models/session.py:43
+#: terminal/models/session.py:44
 msgid "Login from"
 msgstr "登录来源"
 
-#: terminal/models/session.py:47
+#: terminal/models/session.py:48
 msgid "Replay"
 msgstr "回放"
 
-#: terminal/models/session.py:52
+#: terminal/models/session.py:53
 msgid "Date end"
 msgstr "结束日期"
 
@@ -3201,7 +3209,7 @@ msgstr "文档类型"
 
 #: terminal/serializers/storage.py:185
 msgid "Ignore Certificate Verification"
-msgstr ""
+msgstr "忽略证书认证"
 
 #: terminal/serializers/terminal.py:66 terminal/serializers/terminal.py:74
 msgid "Not found"
@@ -3291,15 +3299,15 @@ msgstr "申请资产"
 msgid "Apply for application"
 msgstr "申请应用"
 
-#: tickets/const.py:16 tickets/const.py:23
+#: tickets/const.py:17 tickets/const.py:24
 msgid "Open"
 msgstr "打开"
 
-#: tickets/const.py:17
+#: tickets/const.py:18
 msgid "Approve"
 msgstr "同意"
 
-#: tickets/const.py:24
+#: tickets/const.py:25
 msgid "Closed"
 msgstr "关闭"
 
@@ -3414,6 +3422,30 @@ msgstr "工单申请信息"
 msgid "Ticket approved info"
 msgstr "工单批准信息"
 
+#: tickets/handler/command_confirm.py:23
+msgid "Applied run user"
+msgstr "申请运行的用户"
+
+#: tickets/handler/command_confirm.py:24
+msgid "Applied run asset"
+msgstr "申请运行的资产"
+
+#: tickets/handler/command_confirm.py:25
+msgid "Applied run system user"
+msgstr "申请运行的系统用户"
+
+#: tickets/handler/command_confirm.py:26
+msgid "Applied run command"
+msgstr "申请运行的命令"
+
+#: tickets/handler/command_confirm.py:27
+msgid "Applied from session"
+msgstr "申请来自会话"
+
+#: tickets/handler/command_confirm.py:28
+msgid "Applied from command filter rules"
+msgstr "申请来自命令过滤规则"
+
 #: tickets/handler/login_asset_confirm.py:16
 msgid "Applied login user"
 msgstr "申请登录的用户"
@@ -3551,6 +3583,30 @@ msgstr "批准的资产"
 msgid "No `Asset` are found under Organization `{}`"
 msgstr "在组织 `{}` 下没有发现 `资产`"
 
+#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:12
+msgid "Run user"
+msgstr "运行的用户"
+
+#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:13
+msgid "Run asset"
+msgstr "运行的资产"
+
+#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:15
+msgid "Run system user"
+msgstr "运行的系统用户"
+
+#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:17
+msgid "Run command"
+msgstr "运行的命令"
+
+#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:18
+msgid "From session"
+msgstr "来自会话"
+
+#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:20
+msgid "From cmd filter rule"
+msgstr "来自命令过滤规则"
+
 #: tickets/serializers/ticket/meta/ticket_type/common.py:11
 msgid "Created by ticket ({}-{})"
 msgstr "通过工单创建 ({}-{})"

From 4a9e83ba151ce3f4afbec055042e8732961a4463 Mon Sep 17 00:00:00 2001
From: Bai <bugatti_it@163.com>
Date: Tue, 27 Apr 2021 17:48:07 +0800
Subject: [PATCH 8/8] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=91=BD?=
 =?UTF-8?q?=E4=BB=A4=E5=A4=8D=E6=A0=B8=E9=80=BB=E8=BE=91;=20=E6=B7=BB?=
 =?UTF-8?q?=E5=8A=A0=E5=91=BD=E4=BB=A4=E5=A4=8D=E6=A0=B8=E5=B7=A5=E5=8D=95?=
 =?UTF-8?q?;=205?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/assets/models/cmd_filter.py                                | 1 +
 apps/tickets/handler/command_confirm.py                         | 2 ++
 .../serializers/ticket/meta/ticket_type/command_confirm.py      | 1 +
 3 files changed, 4 insertions(+)

diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py
index 72547da19..1ef14bad0 100644
--- a/apps/assets/models/cmd_filter.py
+++ b/apps/assets/models/cmd_filter.py
@@ -117,6 +117,7 @@ class CommandFilterRule(OrgModelMixin):
                 'apply_run_command': run_command,
                 'apply_from_session_id': str(session.id),
                 'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
+                'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id)
             },
             'org_id': org_id,
         }
diff --git a/apps/tickets/handler/command_confirm.py b/apps/tickets/handler/command_confirm.py
index 0e1fd0d6a..2d66db2d8 100644
--- a/apps/tickets/handler/command_confirm.py
+++ b/apps/tickets/handler/command_confirm.py
@@ -12,6 +12,7 @@ class Handler(BaseHandler):
         apply_run_command = self.ticket.meta.get('apply_run_command')
         apply_from_session_id = self.ticket.meta.get('apply_from_session_id')
         apply_from_cmd_filter_rule_id = self.ticket.meta.get('apply_from_cmd_filter_rule_id')
+        apply_from_cmd_filter_id = self.ticket.meta.get('apply_from_cmd_filter_id')
 
         applied_body = '''{}: {},
             {}: {},
@@ -26,5 +27,6 @@ class Handler(BaseHandler):
             _("Applied run command"), apply_run_command,
             _("Applied from session"), apply_from_session_id,
             _("Applied from command filter rules"), apply_from_cmd_filter_rule_id,
+            _("Applied from command filter"), apply_from_cmd_filter_id,
         )
         return applied_body
diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py b/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py
index 4dbdc08fc..eb631fe98 100644
--- a/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py
+++ b/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py
@@ -19,6 +19,7 @@ class ApplySerializer(serializers.Serializer):
     apply_from_cmd_filter_rule_id = serializers.UUIDField(
         required=False, label=_('From cmd filter rule')
     )
+    apply_from_cmd_filter_id = serializers.UUIDField(required=False, label=_('From cmd filter'))
 
 
 class CommandConfirmSerializer(ApplySerializer):