diff --git a/apps/acls/models/login_acl.py b/apps/acls/models/login_acl.py index 6fcff0231..71f202b15 100644 --- a/apps/acls/models/login_acl.py +++ b/apps/acls/models/login_acl.py @@ -53,12 +53,13 @@ class LoginACL(BaseACL): @staticmethod def match(user, ip): - acls = LoginACL.filter_acl(user) - if not acls: + acl_qs = LoginACL.filter_acl(user) + if not acl_qs: return - for acl in acls: - if acl.is_action(LoginACL.ActionChoices.confirm) and not acl.reviewers.exists(): + for acl in acl_qs: + if acl.is_action(LoginACL.ActionChoices.confirm) and \ + not acl.reviewers.exists(): continue ip_group = acl.rules.get('ip_group') time_periods = acl.rules.get('time_period') @@ -79,12 +80,12 @@ class LoginACL(BaseACL): login_datetime = local_now_display() data = { 'title': title, - 'type': const.TicketType.login_confirm, 'applicant': self.user, - 'apply_login_city': login_city, 'apply_login_ip': login_ip, - 'apply_login_datetime': login_datetime, 'org_id': Organization.ROOT_ID, + 'apply_login_city': login_city, + 'apply_login_datetime': login_datetime, + 'type': const.TicketType.login_confirm, } ticket = ApplyLoginTicket.objects.create(**data) assignees = self.reviewers.all() diff --git a/apps/acls/models/login_asset_acl.py b/apps/acls/models/login_asset_acl.py index 37bea242a..2ad9363e5 100644 --- a/apps/acls/models/login_asset_acl.py +++ b/apps/acls/models/login_asset_acl.py @@ -86,12 +86,12 @@ class LoginAssetACL(BaseACL, OrgModelMixin): title = _('Login asset confirm') + ' ({})'.format(user) data = { 'title': title, - 'type': TicketType.login_asset_confirm, + 'org_id': org_id, 'applicant': user, 'apply_login_user': user, 'apply_login_asset': asset, 'apply_login_account': str(account), - 'org_id': org_id, + 'type': TicketType.login_asset_confirm, } ticket = ApplyLoginAssetTicket.objects.create(**data) ticket.open_by_system(assignees) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index ca6ca4589..14c758301 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -7,23 +7,22 @@ 2. 程序需要, 用户不需要更改的写到settings中 3. 程序需要, 用户需要更改的写到本config中 """ +import base64 +import copy +import errno +import json +import logging import os import re import sys import types -import errno -import json -import yaml -import copy -import base64 -import logging from importlib import import_module from urllib.parse import urljoin, urlparse -from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT +import yaml from django.urls import reverse_lazy from django.utils.translation import ugettext_lazy as _ - +from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(BASE_DIR) @@ -499,6 +498,9 @@ class Config(dict): 'FORGOT_PASSWORD_URL': '', 'HEALTH_CHECK_TOKEN': '', + + # Applet 等软件的下载地址 + 'APPLET_DOWNLOAD_HOST': '', } def __init__(self, *args): diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index 24bf7b8b3..7aac0e505 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -1,4 +1,5 @@ import os + from django.urls import reverse_lazy from .. import const @@ -36,6 +37,9 @@ DEBUG_DEV = CONFIG.DEBUG_DEV # Absolute url for some case, for example email link SITE_URL = CONFIG.SITE_URL +# Absolute url for downloading applet +APPLET_DOWNLOAD_HOST = CONFIG.APPLET_DOWNLOAD_HOST + # https://docs.djangoproject.com/en/4.1/ref/settings/ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') @@ -313,7 +317,6 @@ PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', ] - GMSSL_ENABLED = CONFIG.GMSSL_ENABLED GM_HASHER = 'common.hashers.PBKDF2SM3PasswordHasher' if GMSSL_ENABLED: @@ -329,4 +332,3 @@ if os.environ.get('DEBUG_TOOLBAR', False): DEBUG_TOOLBAR_PANELS = [ 'debug_toolbar.panels.profiling.ProfilingPanel', ] - diff --git a/apps/ops/api/adhoc.py b/apps/ops/api/adhoc.py index ce7e8ad57..9889349df 100644 --- a/apps/ops/api/adhoc.py +++ b/apps/ops/api/adhoc.py @@ -4,7 +4,7 @@ from rest_framework import viewsets from ..models import AdHoc from ..serializers import ( - AdHocSerializer, AdhocListSerializer, + AdHocSerializer ) __all__ = [ @@ -14,9 +14,4 @@ __all__ = [ class AdHocViewSet(viewsets.ModelViewSet): queryset = AdHoc.objects.all() - - def get_serializer_class(self): - if self.action != 'list': - return AdhocListSerializer - return AdHocSerializer - + serializer_class = AdHocSerializer diff --git a/apps/ops/migrations/0032_auto_20221117_1848.py b/apps/ops/migrations/0032_auto_20221117_1848.py new file mode 100644 index 000000000..ae18c5280 --- /dev/null +++ b/apps/ops/migrations/0032_auto_20221117_1848.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.14 on 2022-11-17 10:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0031_auto_20221116_2024'), + ] + + operations = [ + migrations.RemoveField( + model_name='job', + name='variables', + ), + migrations.AddField( + model_name='job', + name='parameters_define', + field=models.JSONField(default=dict, verbose_name='Parameters define'), + ), + migrations.AddField( + model_name='jobexecution', + name='parameters', + field=models.JSONField(default=dict, verbose_name='Parameters'), + ), + ] diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py index b330a4bae..8897d6107 100644 --- a/apps/ops/models/job.py +++ b/apps/ops/models/job.py @@ -45,7 +45,7 @@ class Job(BaseCreateUpdateModel): runas = models.CharField(max_length=128, default='root', verbose_name=_('Runas')) runas_policy = models.CharField(max_length=128, choices=RunasPolicies.choices, default=RunasPolicies.skip, verbose_name=_('Runas policy')) - variables = models.JSONField(default=dict, verbose_name=_('Variables')) + parameters_define = models.JSONField(default=dict, verbose_name=_('Parameters define')) comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True) @property @@ -55,15 +55,13 @@ class Job(BaseCreateUpdateModel): def create_execution(self): return self.executions.create() - def get_variables(self): - return json.loads(self.variables) - class JobExecution(BaseCreateUpdateModel): id = models.UUIDField(default=uuid.uuid4, primary_key=True) task_id = models.UUIDField(null=True) status = models.CharField(max_length=16, verbose_name=_('Status'), default='running') job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='executions', null=True) + parameters = models.JSONField(default=dict, verbose_name=_('Parameters')) result = models.JSONField(blank=True, null=True, verbose_name=_('Result')) summary = models.JSONField(default=dict, verbose_name=_('Summary')) creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) @@ -74,11 +72,12 @@ class JobExecution(BaseCreateUpdateModel): def get_runner(self): inv = self.job.inventory inv.write_to_file(self.inventory_path) + extra_vars = json.loads(self.parameters) if self.job.type == 'adhoc': runner = AdHocRunner( self.inventory_path, self.job.module, module_args=self.job.args, - pattern="all", project_dir=self.private_dir, extra_vars=self.job.get_variables() + pattern="all", project_dir=self.private_dir, extra_vars=extra_vars, ) elif self.job.type == 'playbook': runner = PlaybookRunner( diff --git a/apps/ops/serializers/adhoc.py b/apps/ops/serializers/adhoc.py index 2120c8c50..f5d8d4780 100644 --- a/apps/ops/serializers/adhoc.py +++ b/apps/ops/serializers/adhoc.py @@ -14,15 +14,6 @@ class AdHocSerializer(serializers.ModelSerializer): row_count = serializers.IntegerField(read_only=True) size = serializers.IntegerField(read_only=True) - class Meta: - model = AdHoc - fields = ["id", "name", "module", "owner", "row_count", "size", "date_created", "date_updated"] - - -class AdhocListSerializer(AdHocSerializer): - row_count = serializers.IntegerField(read_only=True) - size = serializers.IntegerField(read_only=True) - class Meta: model = AdHoc fields = ["id", "name", "module", "row_count", "size", "args", "owner", "date_created", "date_updated"] diff --git a/apps/ops/serializers/job.py b/apps/ops/serializers/job.py index 9b8823bcb..2771e2a7f 100644 --- a/apps/ops/serializers/job.py +++ b/apps/ops/serializers/job.py @@ -14,7 +14,7 @@ class JobSerializer(serializers.ModelSerializer): model = Job fields = [ "id", "name", "instant", "type", "module", "args", "playbook", "assets", "runas_policy", "runas", "owner", - "variables", + "parameters_define", "timeout", "chdir", "comment", @@ -29,5 +29,5 @@ class JobExecutionSerializer(serializers.ModelSerializer): read_only_fields = ["id", "task_id", "timedelta", "time_cost", 'is_finished', 'date_start', 'date_created', 'is_success', 'task_id', 'short_id'] fields = read_only_fields + [ - "job" + "job", "parameters" ] diff --git a/apps/perms/const.py b/apps/perms/const.py index cecaa5e3a..50141f386 100644 --- a/apps/perms/const.py +++ b/apps/perms/const.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # -from django.db import models from django.utils.translation import ugettext_lazy as _ from common.db.fields import BitChoices @@ -32,3 +31,7 @@ class ActionChoices(BitChoices): def has_perm(cls, action_name, total): action_value = getattr(cls, action_name) return action_value & total == action_value + + @classmethod + def display(cls, value): + return ', '.join([str(c.label) for c in cls if c.value & value == c.value]) diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index 1b9f068b0..6f48c05d1 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -64,17 +64,15 @@ class AssetPermission(OrgModelMixin): # 特殊的账号: @ALL, @INPUT @USER 默认包含,将来在全局设置中进行控制. accounts = models.JSONField(default=list, verbose_name=_("Accounts")) actions = models.IntegerField(default=ActionChoices.connect, verbose_name=_("Actions")) - is_active = models.BooleanField(default=True, verbose_name=_('Active')) - date_start = models.DateTimeField( - default=timezone.now, db_index=True, verbose_name=_("Date start") - ) + date_start = models.DateTimeField(default=timezone.now, db_index=True, verbose_name=_("Date start")) date_expired = models.DateTimeField( default=date_expired_default, db_index=True, verbose_name=_('Date expired') ) - created_by = models.CharField(max_length=128, blank=True, verbose_name=_('Created by')) - date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) - from_ticket = models.BooleanField(default=False, verbose_name=_('From ticket')) comment = models.TextField(verbose_name=_('Comment'), blank=True) + is_active = models.BooleanField(default=True, verbose_name=_('Active')) + from_ticket = models.BooleanField(default=False, verbose_name=_('From ticket')) + date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) + created_by = models.CharField(max_length=128, blank=True, verbose_name=_('Created by')) objects = AssetPermissionManager.from_queryset(AssetPermissionQuerySet)() diff --git a/apps/terminal/automations/deploy_applet_host/__init__.py b/apps/terminal/automations/deploy_applet_host/__init__.py index be5847aa7..c5e903f35 100644 --- a/apps/terminal/automations/deploy_applet_host/__init__.py +++ b/apps/terminal/automations/deploy_applet_host/__init__.py @@ -45,20 +45,26 @@ class DeployAppletHostManager: def generate_initial_playbook(self): site_url = settings.SITE_URL + download_host = settings.APPLET_DOWNLOAD_HOST bootstrap_token = settings.BOOTSTRAP_TOKEN host_id = str(self.deployment.host.id) if not site_url: site_url = "http://localhost:8080" + if not download_host: + download_host = site_url options = self.deployment.host.deploy_options + site_url = site_url.rstrip("/") + download_host = download_host.rstrip("/") def handler(plays): for play in plays: play["vars"].update(options) - play["vars"]["DownloadHost"] = site_url + "/download" + play["vars"]["APPLET_DOWNLOAD_HOST"] = download_host play["vars"]["CORE_HOST"] = site_url play["vars"]["BOOTSTRAP_TOKEN"] = bootstrap_token play["vars"]["HOST_ID"] = host_id play["vars"]["HOST_NAME"] = self.deployment.host.name + return plays return self._generate_playbook("playbook.yml", handler) diff --git a/apps/terminal/automations/deploy_applet_host/playbook.yml b/apps/terminal/automations/deploy_applet_host/playbook.yml index 3fec8999c..3d86ea52a 100644 --- a/apps/terminal/automations/deploy_applet_host/playbook.yml +++ b/apps/terminal/automations/deploy_applet_host/playbook.yml @@ -2,7 +2,7 @@ - hosts: all vars: - DownloadHost: https://demo.jumpserver.org/download + APPLET_DOWNLOAD_HOST: https://demo.jumpserver.org HOST_NAME: test HOST_ID: 00000000-0000-0000-0000-000000000000 CORE_HOST: https://demo.jumpserver.org @@ -32,7 +32,7 @@ - name: Download JumpServer Tinker installer (jumpserver) ansible.windows.win_get_url: - url: "{{ DownloadHost }}/{{ TinkerInstaller }}" + url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/{{ TinkerInstaller }}" dest: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}" - name: Install JumpServer Tinker (jumpserver) @@ -52,7 +52,7 @@ - name: Download python-3.10.8 ansible.windows.win_get_url: - url: "{{ DownloadHost }}/python-3.10.8-amd64.exe" + url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/python-3.10.8-amd64.exe" dest: "{{ ansible_env.TEMP }}\\python-3.10.8-amd64.exe" - name: Install the python-3.10.8 @@ -112,27 +112,27 @@ - name: Download pip packages ansible.windows.win_get_url: - url: "{{ DownloadHost }}/pip_packages_v0.0.1.zip" - dest: "{{ ansible_env.TEMP }}\\pip_packages_v0.0.1.zip" + url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/pip_packages.zip" + dest: "{{ ansible_env.TEMP }}\\pip_packages.zip" - name: Unzip pip_packages community.windows.win_unzip: - src: "{{ ansible_env.TEMP }}\\pip_packages_v0.0.1.zip" - dest: "{{ ansible_env.TEMP }}" + src: "{{ ansible_env.TEMP }}\\pip_packages.zip" + dest: "{{ ansible_env.TEMP }}\\pip_packages" - name: Install python requirements offline ansible.windows.win_shell: > - pip install -r '{{ ansible_env.TEMP }}\pip_packages_v0.0.1\requirements.txt' - --no-index --find-links='{{ ansible_env.TEMP }}\pip_packages_v0.0.1' + pip install -r '{{ ansible_env.TEMP }}\pip_packages\requirements.txt' + --no-index --find-links='{{ ansible_env.TEMP }}\pip_packages' - name: Download chromedriver (chrome) ansible.windows.win_get_url: - url: "{{ DownloadHost }}/chromedriver_win32.107.zip" - dest: "{{ ansible_env.TEMP }}\\chromedriver_win32.107.zip" + url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/chromedriver_win32.zip" + dest: "{{ ansible_env.TEMP }}\\chromedriver_win32.zip" - name: Unzip chromedriver (chrome) community.windows.win_unzip: - src: "{{ ansible_env.TEMP }}\\chromedriver_win32.107.zip" + src: "{{ ansible_env.TEMP }}\\chromedriver_win32.zip" dest: C:\Program Files\JumpServer\drivers - name: Set chromedriver on the global system path (chrome) @@ -142,7 +142,7 @@ - name: Download chrome msi package (chrome) ansible.windows.win_get_url: - url: "{{ DownloadHost }}/googlechromestandaloneenterprise64.msi" + url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/googlechromestandaloneenterprise64.msi" dest: "{{ ansible_env.TEMP }}\\googlechromestandaloneenterprise64.msi" - name: Install chrome (chrome) diff --git a/apps/terminal/models/applet/host.py b/apps/terminal/models/applet/host.py index 83d9e3a41..020c4d98a 100644 --- a/apps/terminal/models/applet/host.py +++ b/apps/terminal/models/applet/host.py @@ -107,6 +107,9 @@ class AppletHostDeployment(JMSBaseModel): comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) task = models.UUIDField(null=True, verbose_name=_('Task')) + class Meta: + ordering = ('-date_start',) + def start(self, **kwargs): from ...automations.deploy_applet_host import DeployAppletHostManager manager = DeployAppletHostManager(self) diff --git a/apps/tickets/api/ticket.py b/apps/tickets/api/ticket.py index 6216f49be..a256c51b5 100644 --- a/apps/tickets/api/ticket.py +++ b/apps/tickets/api/ticket.py @@ -97,6 +97,10 @@ class ApplyAssetTicketViewSet(TicketViewSet): serializer_class = serializers.ApplyAssetSerializer model = ApplyAssetTicket filterset_class = filters.ApplyAssetTicketFilter + serializer_classes = { + 'open': serializers.ApplyAssetSerializer, + 'approve': serializers.ApproveAssetSerializer + } class ApplyLoginTicketViewSet(TicketViewSet): diff --git a/apps/tickets/handlers/apply_asset.py b/apps/tickets/handlers/apply_asset.py index 3f2051120..2ad50b6df 100644 --- a/apps/tickets/handlers/apply_asset.py +++ b/apps/tickets/handlers/apply_asset.py @@ -14,7 +14,6 @@ class Handler(BaseHandler): if is_finished: self._create_asset_permission() - # permission def _create_asset_permission(self): org_id = self.ticket.org_id with tmp_to_org(org_id): @@ -27,6 +26,7 @@ class Handler(BaseHandler): apply_permission_name = self.ticket.apply_permission_name apply_actions = self.ticket.apply_actions + apply_accounts = self.ticket.apply_accounts apply_date_start = self.ticket.apply_date_start apply_date_expired = self.ticket.apply_date_expired permission_created_by = '{}:{}'.format( @@ -46,19 +46,20 @@ class Handler(BaseHandler): ) permission_data = { - 'id': self.ticket.id, - 'name': apply_permission_name, 'from_ticket': True, - 'comment': str(permission_comment), - 'created_by': permission_created_by, + 'id': self.ticket.id, 'actions': apply_actions, + 'accounts': apply_accounts, + 'name': apply_permission_name, 'date_start': apply_date_start, 'date_expired': apply_date_expired, + 'comment': str(permission_comment), + 'created_by': permission_created_by, } with tmp_to_org(self.ticket.org_id): asset_permission = AssetPermission.objects.create(**permission_data) - asset_permission.users.add(self.ticket.applicant) asset_permission.nodes.set(apply_nodes) asset_permission.assets.set(apply_assets) + asset_permission.users.add(self.ticket.applicant) return asset_permission diff --git a/apps/tickets/handlers/login_confirm.py b/apps/tickets/handlers/login_confirm.py index ad33ce476..e6498314e 100644 --- a/apps/tickets/handlers/login_confirm.py +++ b/apps/tickets/handlers/login_confirm.py @@ -1,22 +1,6 @@ -from django.utils.translation import ugettext as _ from tickets.models import ApplyLoginTicket from .base import BaseHandler class Handler(BaseHandler): ticket: ApplyLoginTicket - - def _construct_meta_body_of_open(self): - apply_login_ip = self.ticket.apply_login_ip - apply_login_city = self.ticket.apply_login_city - apply_login_datetime = self.ticket.apply_login_datetime - applied_body = ''' - {}: {} - {}: {} - {}: {} - '''.format( - _("Applied login IP"), apply_login_ip, - _("Applied login city"), apply_login_city, - _("Applied login datetime"), apply_login_datetime, - ) - return applied_body diff --git a/apps/tickets/models/ticket/apply_asset.py b/apps/tickets/models/ticket/apply_asset.py index 1e46cc130..2fde56125 100644 --- a/apps/tickets/models/ticket/apply_asset.py +++ b/apps/tickets/models/ticket/apply_asset.py @@ -18,3 +18,6 @@ class ApplyAssetTicket(Ticket): apply_actions = models.IntegerField(verbose_name=_('Actions'), default=ActionChoices.all()) apply_date_start = models.DateTimeField(verbose_name=_('Date start'), null=True) apply_date_expired = models.DateTimeField(verbose_name=_('Date expired'), null=True) + + def get_apply_actions_display(self): + return ActionChoices.display(self.apply_actions) diff --git a/apps/tickets/models/ticket/command_confirm.py b/apps/tickets/models/ticket/command_confirm.py index 601102ba3..ac70eaadb 100644 --- a/apps/tickets/models/ticket/command_confirm.py +++ b/apps/tickets/models/ticket/command_confirm.py @@ -10,8 +10,8 @@ class ApplyCommandTicket(Ticket): null=True, verbose_name=_('Run user') ) apply_run_asset = models.CharField(max_length=128, verbose_name=_('Run asset')) - apply_run_account = models.CharField(max_length=128, default='', verbose_name=_('Run account')) apply_run_command = models.CharField(max_length=4096, verbose_name=_('Run command')) + apply_run_account = models.CharField(max_length=128, default='', verbose_name=_('Run account')) apply_from_session = models.ForeignKey( 'terminal.Session', on_delete=models.SET_NULL, null=True, verbose_name=_("Session") diff --git a/apps/tickets/models/ticket/general.py b/apps/tickets/models/ticket/general.py index e58ffa04a..605990040 100644 --- a/apps/tickets/models/ticket/general.py +++ b/apps/tickets/models/ticket/general.py @@ -24,7 +24,9 @@ from tickets.handlers import get_ticket_handler from tickets.errors import AlreadyClosed from ..flow import TicketFlow -__all__ = ['Ticket', 'TicketStep', 'TicketAssignee', 'SuperTicket', 'SubTicketManager'] +__all__ = [ + 'Ticket', 'TicketStep', 'TicketAssignee', 'SuperTicket', 'SubTicketManager' +] class TicketStep(CommonModelMixin): @@ -204,11 +206,11 @@ class StatusMixin: step_info = { 'state': state, - 'approval_level': step.level, 'assignees': assignee_ids, + 'processor': processor_id, + 'approval_level': step.level, 'assignees_display': assignees_display, 'approval_date': str(step.date_updated), - 'processor': processor_id, 'processor_display': processor_display } process_map.append(step_info) @@ -224,15 +226,15 @@ class StatusMixin: org_id = self.flow.org_id flow_rules = self.flow.rules.order_by('level') for rule in flow_rules: - step = TicketStep.objects.create(ticket=self, level=rule.level) assignees = rule.get_assignees(org_id=org_id) assignees = self.exclude_applicant(assignees, self.applicant) + step = TicketStep.objects.create(ticket=self, level=rule.level) step_assignees = [TicketAssignee(step=step, assignee=user) for user in assignees] TicketAssignee.objects.bulk_create(step_assignees) def create_process_steps_by_assignees(self, assignees): - assignees = self.exclude_applicant(assignees, self.applicant) step = TicketStep.objects.create(ticket=self, level=1) + assignees = self.exclude_applicant(assignees, self.applicant) ticket_assignees = [TicketAssignee(step=step, assignee=user) for user in assignees] TicketAssignee.objects.bulk_create(ticket_assignees) @@ -248,14 +250,13 @@ class StatusMixin: @property def processor(self): processor = self.current_step.ticket_assignees \ - .exclude(state=StepState.pending) \ - .first() + .exclude(state=StepState.pending).first() return processor.assignee if processor else None def has_current_assignee(self, assignee): return self.ticket_steps.filter( + level=self.approval_step, ticket_assignees__assignee=assignee, - level=self.approval_step ).exists() def has_all_assignee(self, assignee): @@ -282,19 +283,19 @@ class Ticket(StatusMixin, CommonModelMixin): ) # 申请人 applicant = models.ForeignKey( - 'users.User', related_name='applied_tickets', on_delete=models.SET_NULL, - null=True, verbose_name=_("Applicant") + 'users.User', related_name='applied_tickets', null=True, + on_delete=models.SET_NULL, verbose_name=_("Applicant") ) - comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) flow = models.ForeignKey( - 'TicketFlow', related_name='tickets', on_delete=models.SET_NULL, - null=True, verbose_name=_('TicketFlow') + 'TicketFlow', related_name='tickets', null=True, + on_delete=models.SET_NULL, verbose_name=_('TicketFlow') ) approval_step = models.SmallIntegerField( default=TicketLevel.one, choices=TicketLevel.choices, verbose_name=_('Approval step') ) - serial_num = models.CharField(_('Serial number'), max_length=128, unique=True, null=True) + comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) rel_snapshot = models.JSONField(verbose_name=_('Relation snapshot'), default=dict) + serial_num = models.CharField(_('Serial number'), max_length=128, unique=True, null=True) meta = models.JSONField(encoder=ModelJSONFieldEncoder, default=dict, verbose_name=_("Meta")) org_id = models.CharField( max_length=36, blank=True, default='', verbose_name=_('Organization'), db_index=True @@ -324,7 +325,7 @@ class Ticket(StatusMixin, CommonModelMixin): @classmethod def get_user_related_tickets(cls, user): queries = Q(applicant=user) | Q(ticket_steps__ticket_assignees__assignee=user) - tickets = cls.objects.all().filter(queries).distinct() + tickets = cls.objects.filter(queries).distinct() return tickets def get_current_ticket_flow_approve(self): @@ -398,15 +399,17 @@ class Ticket(StatusMixin, CommonModelMixin): value = self.rel_snapshot[name] elif isinstance(field, related.ManyToManyField): value = ', '.join(self.rel_snapshot[name]) + elif isinstance(value, list): + value = ', '.join(value) return value def get_local_snapshot(self): + snapshot = {} + excludes = ['ticket_ptr'] fields = self._meta._forward_fields_map json_data = json.dumps(model_to_dict(self), cls=ModelJSONFieldEncoder) data = json.loads(json_data) - snapshot = {} local_fields = self._meta.local_fields + self._meta.local_many_to_many - excludes = ['ticket_ptr'] item_names = [field.name for field in local_fields if field.name not in excludes] for name in item_names: field = fields[name] diff --git a/apps/tickets/models/ticket/login_asset_confirm.py b/apps/tickets/models/ticket/login_asset_confirm.py index 2b97cd7a2..8761bc7fe 100644 --- a/apps/tickets/models/ticket/login_asset_confirm.py +++ b/apps/tickets/models/ticket/login_asset_confirm.py @@ -8,12 +8,10 @@ __all__ = ['ApplyLoginAssetTicket'] class ApplyLoginAssetTicket(Ticket): apply_login_user = models.ForeignKey( - 'users.User', on_delete=models.SET_NULL, null=True, - verbose_name=_('Login user'), + 'users.User', on_delete=models.SET_NULL, null=True, verbose_name=_('Login user'), ) apply_login_asset = models.ForeignKey( - 'assets.Asset', on_delete=models.SET_NULL, null=True, - verbose_name=_('Login asset'), + 'assets.Asset', on_delete=models.SET_NULL, null=True, verbose_name=_('Login asset'), ) apply_login_account = models.CharField( max_length=128, default='', verbose_name=_('Login account') diff --git a/apps/tickets/serializers/ticket/apply_asset.py b/apps/tickets/serializers/ticket/apply_asset.py index 69b4371f4..4e7fd0f6c 100644 --- a/apps/tickets/serializers/ticket/apply_asset.py +++ b/apps/tickets/serializers/ticket/apply_asset.py @@ -9,7 +9,7 @@ from tickets.models import ApplyAssetTicket from .common import BaseApplyAssetSerializer from .ticket import TicketApplySerializer -__all__ = ['ApplyAssetSerializer'] +__all__ = ['ApplyAssetSerializer', 'ApproveAssetSerializer'] asset_or_node_help_text = _("Select at least one asset or node") @@ -22,18 +22,14 @@ class ApplyAssetSerializer(BaseApplyAssetSerializer, TicketApplySerializer): class Meta(TicketApplySerializer.Meta): model = ApplyAssetTicket - fields_mini = ['id', 'title'] writeable_fields = [ - 'id', 'title', 'apply_nodes', 'apply_assets', - 'apply_accounts', 'apply_actions', 'org_id', 'comment', - 'apply_date_start', 'apply_date_expired' + 'apply_nodes', 'apply_assets', 'apply_accounts', + 'apply_actions', 'apply_date_start', 'apply_date_expired' ] - fields = TicketApplySerializer.Meta.fields + writeable_fields + ['apply_permission_name', ] - read_only_fields = list(set(fields) - set(writeable_fields)) + read_only_fields = TicketApplySerializer.Meta.read_only_fields + ['apply_permission_name', ] + fields = TicketApplySerializer.Meta.fields_small + writeable_fields + read_only_fields ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs extra_kwargs = { - 'apply_nodes': {'required': False}, - 'apply_assets': {'required': False}, 'apply_accounts': {'required': False}, } extra_kwargs.update(ticket_extra_kwargs) @@ -48,8 +44,7 @@ class ApplyAssetSerializer(BaseApplyAssetSerializer, TicketApplySerializer): attrs['type'] = 'apply_asset' attrs = super().validate(attrs) if self.is_final_approval and ( - not attrs.get('apply_nodes') - and not attrs.get('apply_assets') + not attrs.get('apply_nodes') and not attrs.get('apply_assets') ): raise serializers.ValidationError({ 'apply_nodes': asset_or_node_help_text, @@ -62,3 +57,9 @@ class ApplyAssetSerializer(BaseApplyAssetSerializer, TicketApplySerializer): def setup_eager_loading(cls, queryset): queryset = queryset.prefetch_related('apply_nodes', 'apply_assets') return queryset + + +class ApproveAssetSerializer(ApplyAssetSerializer): + class Meta(ApplyAssetSerializer.Meta): + read_only_fields = TicketApplySerializer.Meta.fields_small + \ + ApplyAssetSerializer.Meta.read_only_fields diff --git a/apps/tickets/serializers/ticket/command_confirm.py b/apps/tickets/serializers/ticket/command_confirm.py index fced49976..5b3a39b0e 100644 --- a/apps/tickets/serializers/ticket/command_confirm.py +++ b/apps/tickets/serializers/ticket/command_confirm.py @@ -9,8 +9,8 @@ __all__ = [ class ApplyCommandConfirmSerializer(TicketApplySerializer): class Meta: model = ApplyCommandTicket - fields = TicketApplySerializer.Meta.fields + [ - 'apply_run_user', 'apply_run_asset', 'apply_run_account', - 'apply_run_command', 'apply_from_session', 'apply_from_cmd_filter', - 'apply_from_cmd_filter_rule' + writeable_fields = [ + 'apply_run_user', 'apply_run_asset', 'apply_run_account', 'apply_run_command', + 'apply_from_session', 'apply_from_cmd_filter', 'apply_from_cmd_filter_rule' ] + fields = TicketApplySerializer.Meta.fields + writeable_fields diff --git a/apps/tickets/serializers/ticket/common.py b/apps/tickets/serializers/ticket/common.py index 7cbaea697..1af361693 100644 --- a/apps/tickets/serializers/ticket/common.py +++ b/apps/tickets/serializers/ticket/common.py @@ -75,10 +75,11 @@ class BaseApplyAssetSerializer(serializers.Serializer): def create(self, validated_data): instance = super().create(validated_data) name = _('Created by ticket ({}-{})').format(instance.title, str(instance.id)[:4]) - with tmp_to_org(instance.org_id): + org_id = instance.org_id + with tmp_to_org(org_id): if not self.permission_model.objects.filter(name=name).exists(): instance.apply_permission_name = name - instance.save() + instance.save(update_fields=['apply_permission_name']) return instance raise serializers.ValidationError(_('Permission named `{}` already exists'.format(name))) diff --git a/apps/tickets/serializers/ticket/login_asset_confirm.py b/apps/tickets/serializers/ticket/login_asset_confirm.py index 43d54d327..4d3db5fc6 100644 --- a/apps/tickets/serializers/ticket/login_asset_confirm.py +++ b/apps/tickets/serializers/ticket/login_asset_confirm.py @@ -9,6 +9,5 @@ __all__ = [ class LoginAssetConfirmSerializer(TicketApplySerializer): class Meta: model = ApplyLoginAssetTicket - fields = TicketApplySerializer.Meta.fields + [ - 'apply_login_user', 'apply_login_asset', 'apply_login_account' - ] + writeable_fields = ['apply_login_user', 'apply_login_asset', 'apply_login_account'] + fields = TicketApplySerializer.Meta.fields + writeable_fields diff --git a/apps/tickets/serializers/ticket/login_confirm.py b/apps/tickets/serializers/ticket/login_confirm.py index e760c653f..128ac5971 100644 --- a/apps/tickets/serializers/ticket/login_confirm.py +++ b/apps/tickets/serializers/ticket/login_confirm.py @@ -7,8 +7,7 @@ __all__ = [ class LoginConfirmSerializer(TicketApplySerializer): - class Meta: + class Meta(TicketApplySerializer.Meta): model = ApplyLoginTicket - fields = TicketApplySerializer.Meta.fields + [ - 'apply_login_ip', 'apply_login_city', 'apply_login_datetime' - ] + writeable_fields = ['apply_login_ip', 'apply_login_city', 'apply_login_datetime'] + fields = TicketApplySerializer.Meta.fields + writeable_fields diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py index f201c2edc..7bb168791 100644 --- a/apps/tickets/serializers/ticket/ticket.py +++ b/apps/tickets/serializers/ticket/ticket.py @@ -22,13 +22,12 @@ class TicketSerializer(OrgResourceModelSerializerMixin): class Meta: model = Ticket fields_mini = ['id', 'title'] - fields_small = fields_mini + [ - 'type', 'status', 'state', 'approval_step', 'comment', - 'date_created', 'date_updated', 'org_id', 'rel_snapshot', - 'process_map', 'org_name', 'serial_num' + fields_small = fields_mini + ['org_id', 'comment'] + read_only_fields = [ + 'serial_num', 'process_map', 'approval_step', 'type', 'state', 'applicant', + 'status', 'date_created', 'date_updated', 'org_name', 'rel_snapshot' ] - fields_fk = ['applicant', ] - fields = fields_small + fields_fk + fields = fields_small + read_only_fields extra_kwargs = { 'type': {'required': True} } @@ -72,8 +71,6 @@ class TicketApplySerializer(TicketSerializer): if self.instance: return attrs - print("Attrs: ", attrs) - ticket_type = attrs.get('type') org_id = attrs.get('org_id') flow = TicketFlow.get_org_related_flows(org_id=org_id) \ @@ -81,7 +78,7 @@ class TicketApplySerializer(TicketSerializer): if flow: attrs['flow'] = flow + return attrs else: error = _('The ticket flow `{}` does not exist'.format(ticket_type)) raise serializers.ValidationError(error) - return attrs