mirror of https://github.com/jumpserver/jumpserver
Merge branch 'v3' of github.com:jumpserver/jumpserver into v3
commit
b1bd57cd76
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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(
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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)()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue