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..56cbbd6c3 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,50 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet): return cmd_filter.rules.all() +class CommandConfirmAPI(CreateAPIView): + 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): + pass + 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..1ef14bad0 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,32 @@ 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) + + 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_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, + } + ticket = Ticket.objects.create(**data) + ticket.assignees.set(self.reviewers.all()) + ticket.open(applicant=session.user_obj) + return ticket 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..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): @@ -34,7 +37,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'] @@ -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/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/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 diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 78b235d0e..f47606a0d 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index e24c71c54..7afb36d4f 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-26 10:14+0800\n" +"POT-Creation-Date: 2021-04-27 18:00+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -34,12 +34,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 (数值越小越优先)" @@ -55,7 +55,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 @@ -67,11 +67,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 "允许" @@ -81,7 +81,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 @@ -97,7 +97,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:164 #: users/models/user.py:712 users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 @@ -124,7 +124,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 #: xpack/plugins/change_auth_plan/models.py:282 @@ -132,7 +132,8 @@ msgstr "系统用户" msgid "Asset" msgstr "资产" -#: acls/models/login_asset_acl.py:32 authentication/models.py:45 +#: acls/models/login_asset_acl.py:32 assets/models/cmd_filter.py:62 +#: authentication/models.py:45 msgid "Reviewers" msgstr "审批人" @@ -250,7 +251,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 @@ -320,7 +321,6 @@ msgstr "目标URL" #: users/templates/users/user_otp_check_password.html:13 #: users/templates/users/user_password_update.html:43 #: users/templates/users/user_password_verify.html:18 -#: users/templates/users/user_update.html:20 #: xpack/plugins/change_auth_plan/models.py:68 #: xpack/plugins/change_auth_plan/models.py:190 #: xpack/plugins/change_auth_plan/models.py:285 @@ -480,7 +480,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:576 #: users/serializers/group.py:35 xpack/plugins/change_auth_plan/models.py:81 @@ -585,30 +585,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 "网关" @@ -735,7 +743,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 @@ -1025,7 +1033,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 "远端地址" @@ -1042,7 +1050,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 @@ -1418,7 +1426,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 "关闭" @@ -2944,13 +2952,12 @@ msgstr "较高" #: terminal/const.py:33 users/templates/users/reset_password.html:50 #: users/templates/users/user_password_update.html:104 -#: users/templates/users/user_update.html:57 msgid "Normal" msgstr "正常" #: terminal/const.py:34 msgid "Offline" -msgstr "" +msgstr "离线" #: terminal/exceptions.py:8 msgid "Bulk create not support" @@ -2960,15 +2967,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 "结束日期" @@ -3115,7 +3122,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" @@ -3205,15 +3212,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 "关闭" @@ -3328,6 +3335,36 @@ msgstr "工单申请信息" msgid "Ticket approved info" msgstr "工单批准信息" +#: tickets/handler/command_confirm.py:24 +msgid "Applied run user" +msgstr "申请运行的用户" + +#: tickets/handler/command_confirm.py:25 +msgid "Applied run asset" +msgstr "申请运行的资产" + +#: tickets/handler/command_confirm.py:26 +msgid "Applied run system user" +msgstr "申请运行的系统用户" + +#: tickets/handler/command_confirm.py:27 +msgid "Applied run command" +msgstr "申请运行的命令" + +#: tickets/handler/command_confirm.py:28 +msgid "Applied from session" +msgstr "申请来自会话" + +#: tickets/handler/command_confirm.py:29 +msgid "Applied from command filter rules" +msgstr "申请来自命令过滤规则" + +#: tickets/handler/command_confirm.py:30 +#, fuzzy +#| msgid "Applied from command filter rules" +msgid "Applied from command filter" +msgstr "申请来自命令过滤规则" + #: tickets/handler/login_asset_confirm.py:16 msgid "Applied login user" msgstr "申请登录的用户" @@ -3465,6 +3502,36 @@ 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/command_confirm.py:22 +#, fuzzy +#| msgid "From cmd filter rule" +msgid "From cmd filter" +msgstr "来自命令过滤规则" + #: tickets/serializers/ticket/meta/ticket_type/common.py:11 msgid "Created by ticket ({}-{})" msgstr "通过工单创建 ({}-{})" @@ -3585,7 +3652,7 @@ msgstr "原来密码错误" msgid "Automatically configure and download the SSH key" msgstr "自动配置并下载SSH密钥" -#: users/forms/profile.py:130 users/templates/users/user_update.html:30 +#: users/forms/profile.py:130 msgid "ssh public key" msgstr "SSH公钥" @@ -3821,43 +3888,36 @@ msgstr "重置密码" #: users/templates/users/reset_password.html:23 #: users/templates/users/user_password_update.html:64 -#: users/templates/users/user_update.html:13 msgid "Your password must satisfy" msgstr "您的密码必须满足:" #: users/templates/users/reset_password.html:24 #: users/templates/users/user_password_update.html:65 -#: users/templates/users/user_update.html:14 msgid "Password strength" msgstr "密码强度:" #: users/templates/users/reset_password.html:48 #: users/templates/users/user_password_update.html:102 -#: users/templates/users/user_update.html:55 msgid "Very weak" msgstr "很弱" #: users/templates/users/reset_password.html:49 #: users/templates/users/user_password_update.html:103 -#: users/templates/users/user_update.html:56 msgid "Weak" msgstr "弱" #: users/templates/users/reset_password.html:51 #: users/templates/users/user_password_update.html:105 -#: users/templates/users/user_update.html:58 msgid "Medium" msgstr "一般" #: users/templates/users/reset_password.html:52 #: users/templates/users/user_password_update.html:106 -#: users/templates/users/user_update.html:59 msgid "Strong" msgstr "强" #: users/templates/users/reset_password.html:53 #: users/templates/users/user_password_update.html:107 -#: users/templates/users/user_update.html:60 msgid "Very strong" msgstr "很强" @@ -3933,18 +3993,6 @@ msgstr "重置" msgid "Verify password" msgstr "校验密码" -#: users/templates/users/user_update.html:4 -msgid "Update user" -msgstr "更新用户" - -#: users/templates/users/user_update.html:22 users/views/profile/reset.py:120 -msgid "User auth from {}, go there change password" -msgstr "用户认证源来自 {}, 请去相应系统修改密码" - -#: users/templates/users/user_update.html:32 -msgid "User auth from {}, ssh key login is not supported" -msgstr "用户认证源来自 {}, 不支持使用 SSH Key 登录" - #: users/templates/users/user_verify_mfa.html:11 msgid "" "The account protection has been opened, please complete the following " @@ -4313,6 +4361,10 @@ msgstr "重置密码成功,返回到登录页面" msgid "Token invalid or expired" msgstr "Token错误或失效" +#: users/views/profile/reset.py:120 +msgid "User auth from {}, go there change password" +msgstr "用户认证源来自 {}, 请去相应系统修改密码" + #: xpack/plugins/change_auth_plan/meta.py:9 #: xpack/plugins/change_auth_plan/models.py:89 #: xpack/plugins/change_auth_plan/models.py:184 @@ -4985,6 +5037,12 @@ msgstr "社区版" #~ "corresponding private key." #~ msgstr "新的公钥已设置成功,请下载对应的私钥" +#~ msgid "Update user" +#~ msgstr "更新用户" + +#~ msgid "User auth from {}, ssh key login is not supported" +#~ msgstr "用户认证源来自 {}, 不支持使用 SSH Key 登录" + #~ msgid "(Domain name support)" #~ msgstr "(支持域名)" 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..2d66db2d8 --- /dev/null +++ b/apps/tickets/handler/command_confirm.py @@ -0,0 +1,32 @@ +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_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 = '''{}: {}, + {}: {}, + {}: {}, + {}: {}, + {}: {}, + {}: {}, + '''.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_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/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/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 new file mode 100644 index 000000000..eb631fe98 --- /dev/null +++ b/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py @@ -0,0 +1,26 @@ +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_id = serializers.UUIDField(required=False, label=_('From session')) + 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): + pass 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):