From bd84edea62295bca09c162ad1c2298eed40ca80e Mon Sep 17 00:00:00 2001 From: xinwen Date: Wed, 29 Dec 2021 20:07:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B5=84=E4=BA=A7=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E5=B7=A5=E5=8D=95=E9=A1=B5=E9=9D=A2=E5=A2=9E=E5=8A=A0=E7=9B=91?= =?UTF-8?q?=E6=8E=A7=E4=B8=8E=E4=B8=AD=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/acls/api/login_asset_check.py | 3 +- apps/terminal/api/session.py | 10 +++- apps/terminal/api/task.py | 55 ++++++++++++++----- .../migrations/0042_auto_20211229_1619.py | 18 ++++++ apps/terminal/urls/api_urls.py | 1 + apps/terminal/utils.py | 11 ++++ apps/tickets/api/__init__.py | 1 + apps/tickets/api/relation.py | 30 ++++++++++ apps/tickets/migrations/0012_ticketsession.py | 23 ++++++++ apps/tickets/models/__init__.py | 2 +- apps/tickets/models/relation.py | 14 +++++ apps/tickets/models/ticket.py | 2 +- apps/tickets/serializers/__init__.py | 1 + apps/tickets/serializers/relation.py | 10 ++++ apps/tickets/urls/api_urls.py | 7 ++- 15 files changed, 168 insertions(+), 20 deletions(-) create mode 100644 apps/terminal/migrations/0042_auto_20211229_1619.py create mode 100644 apps/tickets/api/relation.py create mode 100644 apps/tickets/migrations/0012_ticketsession.py create mode 100644 apps/tickets/models/relation.py create mode 100644 apps/tickets/serializers/relation.py diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py index 91c0b5b49..f45f4f2ff 100644 --- a/apps/acls/api/login_asset_check.py +++ b/apps/acls/api/login_asset_check.py @@ -60,7 +60,8 @@ class LoginAssetCheckAPI(CreateAPIView): '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(ticket_assignee.assignee) for ticket_assignee in ticket_assignees] + 'reviewers': [str(ticket_assignee.assignee) for ticket_assignee in ticket_assignees], + 'ticket_id': str(ticket.id) } return data diff --git a/apps/terminal/api/session.py b/apps/terminal/api/session.py index 22b650f02..28918a32b 100644 --- a/apps/terminal/api/session.py +++ b/apps/terminal/api/session.py @@ -6,7 +6,7 @@ import tarfile from django.shortcuts import get_object_or_404, reverse from django.utils.translation import ugettext as _ from django.utils.encoding import escape_uri_path -from django.http import FileResponse, HttpResponse +from django.http import FileResponse from django.core.files.storage import default_storage from rest_framework import viewsets, views from rest_framework.response import Response @@ -15,7 +15,7 @@ from rest_framework.decorators import action from common.utils import model_to_json from .. import utils from common.const.http import GET -from common.utils import is_uuid, get_logger, get_object_or_none +from common.utils import get_logger, get_object_or_none from common.mixins.api import AsyncApiMixin from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser from common.drf.filters import DatetimeRangeFilter @@ -24,13 +24,14 @@ from orgs.mixins.api import OrgBulkModelViewSet from orgs.utils import tmp_to_root_org, tmp_to_org from users.models import User from ..utils import find_session_replay_local, download_session_replay -from ..hands import SystemUser from ..models import Session from .. import serializers +from terminal.utils import is_session_approver __all__ = [ 'SessionViewSet', 'SessionReplayViewSet', 'SessionJoinValidateAPI' ] + logger = get_logger(__name__) @@ -197,6 +198,9 @@ class SessionJoinValidateAPI(views.APIView): msg = _('User does not exist: {}'.format(user_id)) return Response({'ok': False, 'msg': msg}, status=401) with tmp_to_org(session.org): + if is_session_approver(session_id, user_id): + return Response({'ok': True, 'msg': ''}, status=200) + if not user.admin_or_audit_orgs: msg = _('User does not have permission') return Response({'ok': False, 'msg': msg}, status=401) diff --git a/apps/terminal/api/task.py b/apps/terminal/api/task.py index 137b8481b..711707c95 100644 --- a/apps/terminal/api/task.py +++ b/apps/terminal/api/task.py @@ -3,14 +3,18 @@ import logging from rest_framework.views import APIView, Response from rest_framework_bulk import BulkModelViewSet +from rest_framework import status +from rest_framework.permissions import IsAuthenticated from common.utils import get_object_or_none from common.permissions import IsOrgAdminOrAppUser from ..models import Session, Task from .. import serializers +from terminal.utils import is_session_approver +from orgs.utils import tmp_to_root_org -__all__ = ['TaskViewSet', 'KillSessionAPI'] +__all__ = ['TaskViewSet', 'KillSessionAPI', 'KillSessionForTicketAPI'] logger = logging.getLogger(__file__) @@ -21,20 +25,45 @@ class TaskViewSet(BulkModelViewSet): permission_classes = (IsOrgAdminOrAppUser,) +def kill_sessions(session_ids, user): + validated_session = [] + + for session_id in session_ids: + session = get_object_or_none(Session, id=session_id) + if session and not session.is_finished: + validated_session.append(session_id) + Task.objects.create( + name="kill_session", args=session.id, terminal=session.terminal, + kwargs={ + 'terminated_by': str(user) + } + ) + return validated_session + + class KillSessionAPI(APIView): permission_classes = (IsOrgAdminOrAppUser,) - model = Task def post(self, request, *args, **kwargs): - validated_session = [] - for session_id in request.data: - session = get_object_or_none(Session, id=session_id) - if session and not session.is_finished: - validated_session.append(session_id) - self.model.objects.create( - name="kill_session", args=session.id, terminal=session.terminal, - kwargs={ - 'terminated_by': str(request.user) - } - ) + session_ids = request.data + user = request.user + validated_session = kill_sessions(session_ids, user) return Response({"ok": validated_session}) + + +class KillSessionForTicketAPI(APIView): + permission_classes = (IsAuthenticated, ) + + def post(self, request, *args, **kwargs): + session_ids = request.data + user_id = request.user.id + + for session_id in session_ids: + if not is_session_approver(session_id, user_id): + return Response({}, status=status.HTTP_403_FORBIDDEN) + + with tmp_to_root_org(): + validated_session = kill_sessions(session_ids, request.user) + + return Response({"ok": validated_session}) + diff --git a/apps/terminal/migrations/0042_auto_20211229_1619.py b/apps/terminal/migrations/0042_auto_20211229_1619.py new file mode 100644 index 000000000..bca4c638a --- /dev/null +++ b/apps/terminal/migrations/0042_auto_20211229_1619.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.13 on 2021-12-29 08:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0041_auto_20211105_1605'), + ] + + operations = [ + migrations.AlterField( + model_name='session', + name='protocol', + field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('vnc', 'vnc'), ('telnet', 'telnet'), ('mysql', 'mysql'), ('redis', 'redis'), ('oracle', 'oracle'), ('mariadb', 'mariadb'), ('sqlserver', 'sqlserver'), ('postgresql', 'postgresql'), ('k8s', 'kubernetes')], db_index=True, default='ssh', max_length=16), + ), + ] diff --git a/apps/terminal/urls/api_urls.py b/apps/terminal/urls/api_urls.py index edbf9db23..ebf6fa47f 100644 --- a/apps/terminal/urls/api_urls.py +++ b/apps/terminal/urls/api_urls.py @@ -30,6 +30,7 @@ urlpatterns = [ api.SessionReplayViewSet.as_view({'get': 'retrieve', 'post': 'create'}), name='session-replay'), path('tasks/kill-session/', api.KillSessionAPI.as_view(), name='kill-session'), + path('tasks/kill-session-for-ticket/', api.KillSessionForTicketAPI.as_view(), name='kill-session-for-ticket'), path('terminals/config/', api.TerminalConfig.as_view(), name='terminal-config'), path('commands/export/', api.CommandExportApi.as_view(), name="command-export"), path('commands/insecure-command/', api.InsecureCommandAlertAPI.as_view(), name="command-alert"), diff --git a/apps/terminal/utils.py b/apps/terminal/utils.py index dbdf98c51..e6fa17068 100644 --- a/apps/terminal/utils.py +++ b/apps/terminal/utils.py @@ -11,6 +11,9 @@ import jms_storage from common.utils import get_logger from . import const from .models import ReplayStorage +from tickets.models import TicketSession, TicketStep, TicketAssignee +from tickets.const import ProcessStatus + logger = get_logger(__name__) @@ -247,3 +250,11 @@ class ComponentsPrometheusMetricsUtil(TypedComponentsStatusMetricsUtil): prometheus_metrics.append('\n') prometheus_metrics_text = '\n'.join(prometheus_metrics) return prometheus_metrics_text + + +def is_session_approver(session_id, user_id): + ticket = TicketSession.get_ticket_by_session_id(session_id) + if not ticket: + return False + ok = ticket.has_all_assignee(user_id) + return ok diff --git a/apps/tickets/api/__init__.py b/apps/tickets/api/__init__.py index a9d31b436..4aad8f12a 100644 --- a/apps/tickets/api/__init__.py +++ b/apps/tickets/api/__init__.py @@ -3,3 +3,4 @@ from .ticket import * from .comment import * from .common import * +from .relation import * diff --git a/apps/tickets/api/relation.py b/apps/tickets/api/relation.py new file mode 100644 index 000000000..f1fadad48 --- /dev/null +++ b/apps/tickets/api/relation.py @@ -0,0 +1,30 @@ +from rest_framework.mixins import CreateModelMixin +from rest_framework import views +from rest_framework.response import Response +from rest_framework import status + +from common.drf.api import JMSGenericViewSet +from common.permissions import IsOrgAdminOrAppUser +from tickets.models import TicketSession +from tickets.serializers import TicketSessionRelationSerializer +from terminal.serializers import SessionSerializer +from orgs.utils import tmp_to_root_org + + +class TicketSessionRelationViewSet(CreateModelMixin, JMSGenericViewSet): + queryset = TicketSession + serializer_class = TicketSessionRelationSerializer + permission_classes = (IsOrgAdminOrAppUser, ) + + +class TicketSessionApi(views.APIView): + + def get(self, request, *args, **kwargs): + with tmp_to_root_org(): + ticketsession = TicketSession.objects.filter(ticket=self.kwargs['ticket_id']).first() + if not ticketsession: + return Response(status=status.HTTP_404_NOT_FOUND) + + session = ticketsession.session + serializer = SessionSerializer(session) + return Response(serializer.data) diff --git a/apps/tickets/migrations/0012_ticketsession.py b/apps/tickets/migrations/0012_ticketsession.py new file mode 100644 index 000000000..63f19bf38 --- /dev/null +++ b/apps/tickets/migrations/0012_ticketsession.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.13 on 2021-12-29 08:19 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0042_auto_20211229_1619'), + ('tickets', '0011_remove_approvalrule_assignees_display'), + ] + + operations = [ + migrations.CreateModel( + name='TicketSession', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('session', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='ticket_relation', to='terminal.session')), + ('ticket', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='session_relation', to='tickets.ticket')), + ], + ), + ] diff --git a/apps/tickets/models/__init__.py b/apps/tickets/models/__init__.py index fd2bd9057..422a64586 100644 --- a/apps/tickets/models/__init__.py +++ b/apps/tickets/models/__init__.py @@ -3,4 +3,4 @@ from .ticket import * from .comment import * from .flow import * - +from .relation import * diff --git a/apps/tickets/models/relation.py b/apps/tickets/models/relation.py new file mode 100644 index 000000000..ee5889587 --- /dev/null +++ b/apps/tickets/models/relation.py @@ -0,0 +1,14 @@ +from django.db import models +from django.db.models import Model + + +class TicketSession(Model): + ticket = models.ForeignKey('tickets.Ticket', related_name='session_relation', on_delete=models.CASCADE, db_constraint=False) + session = models.ForeignKey('terminal.Session', related_name='ticket_relation', on_delete=models.CASCADE, db_constraint=False) + + @classmethod + def get_ticket_by_session_id(cls, session_id): + relation = cls.objects.filter(session=session_id).first() + if relation: + return relation.ticket + return None diff --git a/apps/tickets/models/ticket.py b/apps/tickets/models/ticket.py index 59646a6ac..0ac415f22 100644 --- a/apps/tickets/models/ticket.py +++ b/apps/tickets/models/ticket.py @@ -13,7 +13,7 @@ from tickets.signals import post_change_ticket_action from tickets.handler import get_ticket_handler from tickets.errors import AlreadyClosed -__all__ = ['Ticket'] +__all__ = ['Ticket', 'TicketStep', 'TicketAssignee'] class TicketStep(CommonModelMixin): diff --git a/apps/tickets/serializers/__init__.py b/apps/tickets/serializers/__init__.py index 4f7ca772b..340fd1496 100644 --- a/apps/tickets/serializers/__init__.py +++ b/apps/tickets/serializers/__init__.py @@ -2,3 +2,4 @@ # from .ticket import * from .comment import * +from .relation import * diff --git a/apps/tickets/serializers/relation.py b/apps/tickets/serializers/relation.py new file mode 100644 index 000000000..f9a06f6b4 --- /dev/null +++ b/apps/tickets/serializers/relation.py @@ -0,0 +1,10 @@ +from rest_framework import serializers + +from tickets.models import TicketSession + + +class TicketSessionRelationSerializer(serializers.ModelSerializer): + + class Meta: + model = TicketSession + fields = ['ticket', 'session'] diff --git a/apps/tickets/urls/api_urls.py b/apps/tickets/urls/api_urls.py index ef72013a1..94d0f1cbb 100644 --- a/apps/tickets/urls/api_urls.py +++ b/apps/tickets/urls/api_urls.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +from django.urls import path + from rest_framework_bulk.routes import BulkRouter from .. import api @@ -10,6 +12,9 @@ router = BulkRouter() router.register('tickets', api.TicketViewSet, 'ticket') router.register('flows', api.TicketFlowViewSet, 'flows') router.register('comments', api.CommentViewSet, 'comment') +router.register('ticket-session-relation', api.TicketSessionRelationViewSet, 'ticket-session-relation') -urlpatterns = [] +urlpatterns = [ + path('tickets//session/', api.TicketSessionApi.as_view(), name='ticket-sesion'), +] urlpatterns += router.urls