From 50918a3dd2b9e4c5684db1ff4d7d09e0917e9b66 Mon Sep 17 00:00:00 2001 From: Bai Date: Mon, 26 Apr 2021 17:56:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E5=A4=8D=E6=A0=B8=E9=80=BB=E8=BE=91;=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E5=A4=8D=E6=A0=B8=E5=B7=A5=E5=8D=95;?= 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//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//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