Merge branch 'v3' of github.com:jumpserver/jumpserver into v3

pull/9115/head
ibuler 2022-11-17 20:49:31 +08:00
commit b1bd57cd76
27 changed files with 158 additions and 141 deletions

View File

@ -53,12 +53,13 @@ class LoginACL(BaseACL):
@staticmethod @staticmethod
def match(user, ip): def match(user, ip):
acls = LoginACL.filter_acl(user) acl_qs = LoginACL.filter_acl(user)
if not acls: if not acl_qs:
return return
for acl in acls: for acl in acl_qs:
if acl.is_action(LoginACL.ActionChoices.confirm) and not acl.reviewers.exists(): if acl.is_action(LoginACL.ActionChoices.confirm) and \
not acl.reviewers.exists():
continue continue
ip_group = acl.rules.get('ip_group') ip_group = acl.rules.get('ip_group')
time_periods = acl.rules.get('time_period') time_periods = acl.rules.get('time_period')
@ -79,12 +80,12 @@ class LoginACL(BaseACL):
login_datetime = local_now_display() login_datetime = local_now_display()
data = { data = {
'title': title, 'title': title,
'type': const.TicketType.login_confirm,
'applicant': self.user, 'applicant': self.user,
'apply_login_city': login_city,
'apply_login_ip': login_ip, 'apply_login_ip': login_ip,
'apply_login_datetime': login_datetime,
'org_id': Organization.ROOT_ID, '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) ticket = ApplyLoginTicket.objects.create(**data)
assignees = self.reviewers.all() assignees = self.reviewers.all()

View File

@ -86,12 +86,12 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
title = _('Login asset confirm') + ' ({})'.format(user) title = _('Login asset confirm') + ' ({})'.format(user)
data = { data = {
'title': title, 'title': title,
'type': TicketType.login_asset_confirm, 'org_id': org_id,
'applicant': user, 'applicant': user,
'apply_login_user': user, 'apply_login_user': user,
'apply_login_asset': asset, 'apply_login_asset': asset,
'apply_login_account': str(account), 'apply_login_account': str(account),
'org_id': org_id, 'type': TicketType.login_asset_confirm,
} }
ticket = ApplyLoginAssetTicket.objects.create(**data) ticket = ApplyLoginAssetTicket.objects.create(**data)
ticket.open_by_system(assignees) ticket.open_by_system(assignees)

View File

@ -7,23 +7,22 @@
2. 程序需要, 用户不需要更改的写到settings中 2. 程序需要, 用户不需要更改的写到settings中
3. 程序需要, 用户需要更改的写到本config中 3. 程序需要, 用户需要更改的写到本config中
""" """
import base64
import copy
import errno
import json
import logging
import os import os
import re import re
import sys import sys
import types import types
import errno
import json
import yaml
import copy
import base64
import logging
from importlib import import_module from importlib import import_module
from urllib.parse import urljoin, urlparse 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.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _ 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__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.dirname(BASE_DIR) PROJECT_DIR = os.path.dirname(BASE_DIR)
@ -499,6 +498,9 @@ class Config(dict):
'FORGOT_PASSWORD_URL': '', 'FORGOT_PASSWORD_URL': '',
'HEALTH_CHECK_TOKEN': '', 'HEALTH_CHECK_TOKEN': '',
# Applet 等软件的下载地址
'APPLET_DOWNLOAD_HOST': '',
} }
def __init__(self, *args): def __init__(self, *args):

View File

@ -1,4 +1,5 @@
import os import os
from django.urls import reverse_lazy from django.urls import reverse_lazy
from .. import const from .. import const
@ -36,6 +37,9 @@ DEBUG_DEV = CONFIG.DEBUG_DEV
# Absolute url for some case, for example email link # Absolute url for some case, for example email link
SITE_URL = CONFIG.SITE_URL 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/ # https://docs.djangoproject.com/en/4.1/ref/settings/
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
@ -313,7 +317,6 @@ PASSWORD_HASHERS = [
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
] ]
GMSSL_ENABLED = CONFIG.GMSSL_ENABLED GMSSL_ENABLED = CONFIG.GMSSL_ENABLED
GM_HASHER = 'common.hashers.PBKDF2SM3PasswordHasher' GM_HASHER = 'common.hashers.PBKDF2SM3PasswordHasher'
if GMSSL_ENABLED: if GMSSL_ENABLED:
@ -329,4 +332,3 @@ if os.environ.get('DEBUG_TOOLBAR', False):
DEBUG_TOOLBAR_PANELS = [ DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.profiling.ProfilingPanel', 'debug_toolbar.panels.profiling.ProfilingPanel',
] ]

View File

@ -4,7 +4,7 @@
from rest_framework import viewsets from rest_framework import viewsets
from ..models import AdHoc from ..models import AdHoc
from ..serializers import ( from ..serializers import (
AdHocSerializer, AdhocListSerializer, AdHocSerializer
) )
__all__ = [ __all__ = [
@ -14,9 +14,4 @@ __all__ = [
class AdHocViewSet(viewsets.ModelViewSet): class AdHocViewSet(viewsets.ModelViewSet):
queryset = AdHoc.objects.all() queryset = AdHoc.objects.all()
serializer_class = AdHocSerializer
def get_serializer_class(self):
if self.action != 'list':
return AdhocListSerializer
return AdHocSerializer

View File

@ -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'),
),
]

View File

@ -45,7 +45,7 @@ class Job(BaseCreateUpdateModel):
runas = models.CharField(max_length=128, default='root', verbose_name=_('Runas')) runas = models.CharField(max_length=128, default='root', verbose_name=_('Runas'))
runas_policy = models.CharField(max_length=128, choices=RunasPolicies.choices, default=RunasPolicies.skip, runas_policy = models.CharField(max_length=128, choices=RunasPolicies.choices, default=RunasPolicies.skip,
verbose_name=_('Runas policy')) 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) comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True)
@property @property
@ -55,15 +55,13 @@ class Job(BaseCreateUpdateModel):
def create_execution(self): def create_execution(self):
return self.executions.create() return self.executions.create()
def get_variables(self):
return json.loads(self.variables)
class JobExecution(BaseCreateUpdateModel): class JobExecution(BaseCreateUpdateModel):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
task_id = models.UUIDField(null=True) task_id = models.UUIDField(null=True)
status = models.CharField(max_length=16, verbose_name=_('Status'), default='running') status = models.CharField(max_length=16, verbose_name=_('Status'), default='running')
job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='executions', null=True) 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')) result = models.JSONField(blank=True, null=True, verbose_name=_('Result'))
summary = models.JSONField(default=dict, verbose_name=_('Summary')) summary = models.JSONField(default=dict, verbose_name=_('Summary'))
creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) 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): def get_runner(self):
inv = self.job.inventory inv = self.job.inventory
inv.write_to_file(self.inventory_path) inv.write_to_file(self.inventory_path)
extra_vars = json.loads(self.parameters)
if self.job.type == 'adhoc': if self.job.type == 'adhoc':
runner = AdHocRunner( runner = AdHocRunner(
self.inventory_path, self.job.module, module_args=self.job.args, 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': elif self.job.type == 'playbook':
runner = PlaybookRunner( runner = PlaybookRunner(

View File

@ -14,15 +14,6 @@ class AdHocSerializer(serializers.ModelSerializer):
row_count = serializers.IntegerField(read_only=True) row_count = serializers.IntegerField(read_only=True)
size = 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: class Meta:
model = AdHoc model = AdHoc
fields = ["id", "name", "module", "row_count", "size", "args", "owner", "date_created", "date_updated"] fields = ["id", "name", "module", "row_count", "size", "args", "owner", "date_created", "date_updated"]

View File

@ -14,7 +14,7 @@ class JobSerializer(serializers.ModelSerializer):
model = Job model = Job
fields = [ fields = [
"id", "name", "instant", "type", "module", "args", "playbook", "assets", "runas_policy", "runas", "owner", "id", "name", "instant", "type", "module", "args", "playbook", "assets", "runas_policy", "runas", "owner",
"variables", "parameters_define",
"timeout", "timeout",
"chdir", "chdir",
"comment", "comment",
@ -29,5 +29,5 @@ class JobExecutionSerializer(serializers.ModelSerializer):
read_only_fields = ["id", "task_id", "timedelta", "time_cost", 'is_finished', 'date_start', 'date_created', read_only_fields = ["id", "task_id", "timedelta", "time_cost", 'is_finished', 'date_start', 'date_created',
'is_success', 'task_id', 'short_id'] 'is_success', 'task_id', 'short_id']
fields = read_only_fields + [ fields = read_only_fields + [
"job" "job", "parameters"
] ]

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.db.fields import BitChoices from common.db.fields import BitChoices
@ -32,3 +31,7 @@ class ActionChoices(BitChoices):
def has_perm(cls, action_name, total): def has_perm(cls, action_name, total):
action_value = getattr(cls, action_name) action_value = getattr(cls, action_name)
return action_value & total == action_value 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])

View File

@ -64,17 +64,15 @@ class AssetPermission(OrgModelMixin):
# 特殊的账号: @ALL, @INPUT @USER 默认包含,将来在全局设置中进行控制. # 特殊的账号: @ALL, @INPUT @USER 默认包含,将来在全局设置中进行控制.
accounts = models.JSONField(default=list, verbose_name=_("Accounts")) accounts = models.JSONField(default=list, verbose_name=_("Accounts"))
actions = models.IntegerField(default=ActionChoices.connect, verbose_name=_("Actions")) 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( date_expired = models.DateTimeField(
default=date_expired_default, db_index=True, verbose_name=_('Date expired') 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) 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)() objects = AssetPermissionManager.from_queryset(AssetPermissionQuerySet)()

View File

@ -45,20 +45,26 @@ class DeployAppletHostManager:
def generate_initial_playbook(self): def generate_initial_playbook(self):
site_url = settings.SITE_URL site_url = settings.SITE_URL
download_host = settings.APPLET_DOWNLOAD_HOST
bootstrap_token = settings.BOOTSTRAP_TOKEN bootstrap_token = settings.BOOTSTRAP_TOKEN
host_id = str(self.deployment.host.id) host_id = str(self.deployment.host.id)
if not site_url: if not site_url:
site_url = "http://localhost:8080" site_url = "http://localhost:8080"
if not download_host:
download_host = site_url
options = self.deployment.host.deploy_options options = self.deployment.host.deploy_options
site_url = site_url.rstrip("/")
download_host = download_host.rstrip("/")
def handler(plays): def handler(plays):
for play in plays: for play in plays:
play["vars"].update(options) 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"]["CORE_HOST"] = site_url
play["vars"]["BOOTSTRAP_TOKEN"] = bootstrap_token play["vars"]["BOOTSTRAP_TOKEN"] = bootstrap_token
play["vars"]["HOST_ID"] = host_id play["vars"]["HOST_ID"] = host_id
play["vars"]["HOST_NAME"] = self.deployment.host.name play["vars"]["HOST_NAME"] = self.deployment.host.name
return plays
return self._generate_playbook("playbook.yml", handler) return self._generate_playbook("playbook.yml", handler)

View File

@ -2,7 +2,7 @@
- hosts: all - hosts: all
vars: vars:
DownloadHost: https://demo.jumpserver.org/download APPLET_DOWNLOAD_HOST: https://demo.jumpserver.org
HOST_NAME: test HOST_NAME: test
HOST_ID: 00000000-0000-0000-0000-000000000000 HOST_ID: 00000000-0000-0000-0000-000000000000
CORE_HOST: https://demo.jumpserver.org CORE_HOST: https://demo.jumpserver.org
@ -32,7 +32,7 @@
- name: Download JumpServer Tinker installer (jumpserver) - name: Download JumpServer Tinker installer (jumpserver)
ansible.windows.win_get_url: ansible.windows.win_get_url:
url: "{{ DownloadHost }}/{{ TinkerInstaller }}" url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/{{ TinkerInstaller }}"
dest: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}" dest: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}"
- name: Install JumpServer Tinker (jumpserver) - name: Install JumpServer Tinker (jumpserver)
@ -52,7 +52,7 @@
- name: Download python-3.10.8 - name: Download python-3.10.8
ansible.windows.win_get_url: 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" dest: "{{ ansible_env.TEMP }}\\python-3.10.8-amd64.exe"
- name: Install the python-3.10.8 - name: Install the python-3.10.8
@ -112,27 +112,27 @@
- name: Download pip packages - name: Download pip packages
ansible.windows.win_get_url: ansible.windows.win_get_url:
url: "{{ DownloadHost }}/pip_packages_v0.0.1.zip" url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/pip_packages.zip"
dest: "{{ ansible_env.TEMP }}\\pip_packages_v0.0.1.zip" dest: "{{ ansible_env.TEMP }}\\pip_packages.zip"
- name: Unzip pip_packages - name: Unzip pip_packages
community.windows.win_unzip: community.windows.win_unzip:
src: "{{ ansible_env.TEMP }}\\pip_packages_v0.0.1.zip" src: "{{ ansible_env.TEMP }}\\pip_packages.zip"
dest: "{{ ansible_env.TEMP }}" dest: "{{ ansible_env.TEMP }}\\pip_packages"
- name: Install python requirements offline - name: Install python requirements offline
ansible.windows.win_shell: > ansible.windows.win_shell: >
pip install -r '{{ ansible_env.TEMP }}\pip_packages_v0.0.1\requirements.txt' pip install -r '{{ ansible_env.TEMP }}\pip_packages\requirements.txt'
--no-index --find-links='{{ ansible_env.TEMP }}\pip_packages_v0.0.1' --no-index --find-links='{{ ansible_env.TEMP }}\pip_packages'
- name: Download chromedriver (chrome) - name: Download chromedriver (chrome)
ansible.windows.win_get_url: ansible.windows.win_get_url:
url: "{{ DownloadHost }}/chromedriver_win32.107.zip" url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/chromedriver_win32.zip"
dest: "{{ ansible_env.TEMP }}\\chromedriver_win32.107.zip" dest: "{{ ansible_env.TEMP }}\\chromedriver_win32.zip"
- name: Unzip chromedriver (chrome) - name: Unzip chromedriver (chrome)
community.windows.win_unzip: 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 dest: C:\Program Files\JumpServer\drivers
- name: Set chromedriver on the global system path (chrome) - name: Set chromedriver on the global system path (chrome)
@ -142,7 +142,7 @@
- name: Download chrome msi package (chrome) - name: Download chrome msi package (chrome)
ansible.windows.win_get_url: ansible.windows.win_get_url:
url: "{{ DownloadHost }}/googlechromestandaloneenterprise64.msi" url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/googlechromestandaloneenterprise64.msi"
dest: "{{ ansible_env.TEMP }}\\googlechromestandaloneenterprise64.msi" dest: "{{ ansible_env.TEMP }}\\googlechromestandaloneenterprise64.msi"
- name: Install chrome (chrome) - name: Install chrome (chrome)

View File

@ -107,6 +107,9 @@ class AppletHostDeployment(JMSBaseModel):
comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
task = models.UUIDField(null=True, verbose_name=_('Task')) task = models.UUIDField(null=True, verbose_name=_('Task'))
class Meta:
ordering = ('-date_start',)
def start(self, **kwargs): def start(self, **kwargs):
from ...automations.deploy_applet_host import DeployAppletHostManager from ...automations.deploy_applet_host import DeployAppletHostManager
manager = DeployAppletHostManager(self) manager = DeployAppletHostManager(self)

View File

@ -97,6 +97,10 @@ class ApplyAssetTicketViewSet(TicketViewSet):
serializer_class = serializers.ApplyAssetSerializer serializer_class = serializers.ApplyAssetSerializer
model = ApplyAssetTicket model = ApplyAssetTicket
filterset_class = filters.ApplyAssetTicketFilter filterset_class = filters.ApplyAssetTicketFilter
serializer_classes = {
'open': serializers.ApplyAssetSerializer,
'approve': serializers.ApproveAssetSerializer
}
class ApplyLoginTicketViewSet(TicketViewSet): class ApplyLoginTicketViewSet(TicketViewSet):

View File

@ -14,7 +14,6 @@ class Handler(BaseHandler):
if is_finished: if is_finished:
self._create_asset_permission() self._create_asset_permission()
# permission
def _create_asset_permission(self): def _create_asset_permission(self):
org_id = self.ticket.org_id org_id = self.ticket.org_id
with tmp_to_org(org_id): with tmp_to_org(org_id):
@ -27,6 +26,7 @@ class Handler(BaseHandler):
apply_permission_name = self.ticket.apply_permission_name apply_permission_name = self.ticket.apply_permission_name
apply_actions = self.ticket.apply_actions apply_actions = self.ticket.apply_actions
apply_accounts = self.ticket.apply_accounts
apply_date_start = self.ticket.apply_date_start apply_date_start = self.ticket.apply_date_start
apply_date_expired = self.ticket.apply_date_expired apply_date_expired = self.ticket.apply_date_expired
permission_created_by = '{}:{}'.format( permission_created_by = '{}:{}'.format(
@ -46,19 +46,20 @@ class Handler(BaseHandler):
) )
permission_data = { permission_data = {
'id': self.ticket.id,
'name': apply_permission_name,
'from_ticket': True, 'from_ticket': True,
'comment': str(permission_comment), 'id': self.ticket.id,
'created_by': permission_created_by,
'actions': apply_actions, 'actions': apply_actions,
'accounts': apply_accounts,
'name': apply_permission_name,
'date_start': apply_date_start, 'date_start': apply_date_start,
'date_expired': apply_date_expired, 'date_expired': apply_date_expired,
'comment': str(permission_comment),
'created_by': permission_created_by,
} }
with tmp_to_org(self.ticket.org_id): with tmp_to_org(self.ticket.org_id):
asset_permission = AssetPermission.objects.create(**permission_data) asset_permission = AssetPermission.objects.create(**permission_data)
asset_permission.users.add(self.ticket.applicant)
asset_permission.nodes.set(apply_nodes) asset_permission.nodes.set(apply_nodes)
asset_permission.assets.set(apply_assets) asset_permission.assets.set(apply_assets)
asset_permission.users.add(self.ticket.applicant)
return asset_permission return asset_permission

View File

@ -1,22 +1,6 @@
from django.utils.translation import ugettext as _
from tickets.models import ApplyLoginTicket from tickets.models import ApplyLoginTicket
from .base import BaseHandler from .base import BaseHandler
class Handler(BaseHandler): class Handler(BaseHandler):
ticket: ApplyLoginTicket 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

View File

@ -18,3 +18,6 @@ class ApplyAssetTicket(Ticket):
apply_actions = models.IntegerField(verbose_name=_('Actions'), default=ActionChoices.all()) apply_actions = models.IntegerField(verbose_name=_('Actions'), default=ActionChoices.all())
apply_date_start = models.DateTimeField(verbose_name=_('Date start'), null=True) apply_date_start = models.DateTimeField(verbose_name=_('Date start'), null=True)
apply_date_expired = models.DateTimeField(verbose_name=_('Date expired'), 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)

View File

@ -10,8 +10,8 @@ class ApplyCommandTicket(Ticket):
null=True, verbose_name=_('Run user') null=True, verbose_name=_('Run user')
) )
apply_run_asset = models.CharField(max_length=128, verbose_name=_('Run asset')) 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_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( apply_from_session = models.ForeignKey(
'terminal.Session', on_delete=models.SET_NULL, 'terminal.Session', on_delete=models.SET_NULL,
null=True, verbose_name=_("Session") null=True, verbose_name=_("Session")

View File

@ -24,7 +24,9 @@ from tickets.handlers import get_ticket_handler
from tickets.errors import AlreadyClosed from tickets.errors import AlreadyClosed
from ..flow import TicketFlow from ..flow import TicketFlow
__all__ = ['Ticket', 'TicketStep', 'TicketAssignee', 'SuperTicket', 'SubTicketManager'] __all__ = [
'Ticket', 'TicketStep', 'TicketAssignee', 'SuperTicket', 'SubTicketManager'
]
class TicketStep(CommonModelMixin): class TicketStep(CommonModelMixin):
@ -204,11 +206,11 @@ class StatusMixin:
step_info = { step_info = {
'state': state, 'state': state,
'approval_level': step.level,
'assignees': assignee_ids, 'assignees': assignee_ids,
'processor': processor_id,
'approval_level': step.level,
'assignees_display': assignees_display, 'assignees_display': assignees_display,
'approval_date': str(step.date_updated), 'approval_date': str(step.date_updated),
'processor': processor_id,
'processor_display': processor_display 'processor_display': processor_display
} }
process_map.append(step_info) process_map.append(step_info)
@ -224,15 +226,15 @@ class StatusMixin:
org_id = self.flow.org_id org_id = self.flow.org_id
flow_rules = self.flow.rules.order_by('level') flow_rules = self.flow.rules.order_by('level')
for rule in flow_rules: for rule in flow_rules:
step = TicketStep.objects.create(ticket=self, level=rule.level)
assignees = rule.get_assignees(org_id=org_id) assignees = rule.get_assignees(org_id=org_id)
assignees = self.exclude_applicant(assignees, self.applicant) 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] step_assignees = [TicketAssignee(step=step, assignee=user) for user in assignees]
TicketAssignee.objects.bulk_create(step_assignees) TicketAssignee.objects.bulk_create(step_assignees)
def create_process_steps_by_assignees(self, assignees): def create_process_steps_by_assignees(self, assignees):
assignees = self.exclude_applicant(assignees, self.applicant)
step = TicketStep.objects.create(ticket=self, level=1) 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] ticket_assignees = [TicketAssignee(step=step, assignee=user) for user in assignees]
TicketAssignee.objects.bulk_create(ticket_assignees) TicketAssignee.objects.bulk_create(ticket_assignees)
@ -248,14 +250,13 @@ class StatusMixin:
@property @property
def processor(self): def processor(self):
processor = self.current_step.ticket_assignees \ processor = self.current_step.ticket_assignees \
.exclude(state=StepState.pending) \ .exclude(state=StepState.pending).first()
.first()
return processor.assignee if processor else None return processor.assignee if processor else None
def has_current_assignee(self, assignee): def has_current_assignee(self, assignee):
return self.ticket_steps.filter( return self.ticket_steps.filter(
level=self.approval_step,
ticket_assignees__assignee=assignee, ticket_assignees__assignee=assignee,
level=self.approval_step
).exists() ).exists()
def has_all_assignee(self, assignee): def has_all_assignee(self, assignee):
@ -282,19 +283,19 @@ class Ticket(StatusMixin, CommonModelMixin):
) )
# 申请人 # 申请人
applicant = models.ForeignKey( applicant = models.ForeignKey(
'users.User', related_name='applied_tickets', on_delete=models.SET_NULL, 'users.User', related_name='applied_tickets', null=True,
null=True, verbose_name=_("Applicant") on_delete=models.SET_NULL, verbose_name=_("Applicant")
) )
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
flow = models.ForeignKey( flow = models.ForeignKey(
'TicketFlow', related_name='tickets', on_delete=models.SET_NULL, 'TicketFlow', related_name='tickets', null=True,
null=True, verbose_name=_('TicketFlow') on_delete=models.SET_NULL, verbose_name=_('TicketFlow')
) )
approval_step = models.SmallIntegerField( approval_step = models.SmallIntegerField(
default=TicketLevel.one, choices=TicketLevel.choices, verbose_name=_('Approval step') 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) 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")) meta = models.JSONField(encoder=ModelJSONFieldEncoder, default=dict, verbose_name=_("Meta"))
org_id = models.CharField( org_id = models.CharField(
max_length=36, blank=True, default='', verbose_name=_('Organization'), db_index=True max_length=36, blank=True, default='', verbose_name=_('Organization'), db_index=True
@ -324,7 +325,7 @@ class Ticket(StatusMixin, CommonModelMixin):
@classmethod @classmethod
def get_user_related_tickets(cls, user): def get_user_related_tickets(cls, user):
queries = Q(applicant=user) | Q(ticket_steps__ticket_assignees__assignee=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 return tickets
def get_current_ticket_flow_approve(self): def get_current_ticket_flow_approve(self):
@ -398,15 +399,17 @@ class Ticket(StatusMixin, CommonModelMixin):
value = self.rel_snapshot[name] value = self.rel_snapshot[name]
elif isinstance(field, related.ManyToManyField): elif isinstance(field, related.ManyToManyField):
value = ', '.join(self.rel_snapshot[name]) value = ', '.join(self.rel_snapshot[name])
elif isinstance(value, list):
value = ', '.join(value)
return value return value
def get_local_snapshot(self): def get_local_snapshot(self):
snapshot = {}
excludes = ['ticket_ptr']
fields = self._meta._forward_fields_map fields = self._meta._forward_fields_map
json_data = json.dumps(model_to_dict(self), cls=ModelJSONFieldEncoder) json_data = json.dumps(model_to_dict(self), cls=ModelJSONFieldEncoder)
data = json.loads(json_data) data = json.loads(json_data)
snapshot = {}
local_fields = self._meta.local_fields + self._meta.local_many_to_many 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] item_names = [field.name for field in local_fields if field.name not in excludes]
for name in item_names: for name in item_names:
field = fields[name] field = fields[name]

View File

@ -8,12 +8,10 @@ __all__ = ['ApplyLoginAssetTicket']
class ApplyLoginAssetTicket(Ticket): class ApplyLoginAssetTicket(Ticket):
apply_login_user = models.ForeignKey( apply_login_user = models.ForeignKey(
'users.User', on_delete=models.SET_NULL, null=True, 'users.User', on_delete=models.SET_NULL, null=True, verbose_name=_('Login user'),
verbose_name=_('Login user'),
) )
apply_login_asset = models.ForeignKey( apply_login_asset = models.ForeignKey(
'assets.Asset', on_delete=models.SET_NULL, null=True, 'assets.Asset', on_delete=models.SET_NULL, null=True, verbose_name=_('Login asset'),
verbose_name=_('Login asset'),
) )
apply_login_account = models.CharField( apply_login_account = models.CharField(
max_length=128, default='', verbose_name=_('Login account') max_length=128, default='', verbose_name=_('Login account')

View File

@ -9,7 +9,7 @@ from tickets.models import ApplyAssetTicket
from .common import BaseApplyAssetSerializer from .common import BaseApplyAssetSerializer
from .ticket import TicketApplySerializer from .ticket import TicketApplySerializer
__all__ = ['ApplyAssetSerializer'] __all__ = ['ApplyAssetSerializer', 'ApproveAssetSerializer']
asset_or_node_help_text = _("Select at least one asset or node") asset_or_node_help_text = _("Select at least one asset or node")
@ -22,18 +22,14 @@ class ApplyAssetSerializer(BaseApplyAssetSerializer, TicketApplySerializer):
class Meta(TicketApplySerializer.Meta): class Meta(TicketApplySerializer.Meta):
model = ApplyAssetTicket model = ApplyAssetTicket
fields_mini = ['id', 'title']
writeable_fields = [ writeable_fields = [
'id', 'title', 'apply_nodes', 'apply_assets', 'apply_nodes', 'apply_assets', 'apply_accounts',
'apply_accounts', 'apply_actions', 'org_id', 'comment', 'apply_actions', 'apply_date_start', 'apply_date_expired'
'apply_date_start', 'apply_date_expired'
] ]
fields = TicketApplySerializer.Meta.fields + writeable_fields + ['apply_permission_name', ] read_only_fields = TicketApplySerializer.Meta.read_only_fields + ['apply_permission_name', ]
read_only_fields = list(set(fields) - set(writeable_fields)) fields = TicketApplySerializer.Meta.fields_small + writeable_fields + read_only_fields
ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs
extra_kwargs = { extra_kwargs = {
'apply_nodes': {'required': False},
'apply_assets': {'required': False},
'apply_accounts': {'required': False}, 'apply_accounts': {'required': False},
} }
extra_kwargs.update(ticket_extra_kwargs) extra_kwargs.update(ticket_extra_kwargs)
@ -48,8 +44,7 @@ class ApplyAssetSerializer(BaseApplyAssetSerializer, TicketApplySerializer):
attrs['type'] = 'apply_asset' attrs['type'] = 'apply_asset'
attrs = super().validate(attrs) attrs = super().validate(attrs)
if self.is_final_approval and ( if self.is_final_approval and (
not attrs.get('apply_nodes') not attrs.get('apply_nodes') and not attrs.get('apply_assets')
and not attrs.get('apply_assets')
): ):
raise serializers.ValidationError({ raise serializers.ValidationError({
'apply_nodes': asset_or_node_help_text, 'apply_nodes': asset_or_node_help_text,
@ -62,3 +57,9 @@ class ApplyAssetSerializer(BaseApplyAssetSerializer, TicketApplySerializer):
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
queryset = queryset.prefetch_related('apply_nodes', 'apply_assets') queryset = queryset.prefetch_related('apply_nodes', 'apply_assets')
return queryset return queryset
class ApproveAssetSerializer(ApplyAssetSerializer):
class Meta(ApplyAssetSerializer.Meta):
read_only_fields = TicketApplySerializer.Meta.fields_small + \
ApplyAssetSerializer.Meta.read_only_fields

View File

@ -9,8 +9,8 @@ __all__ = [
class ApplyCommandConfirmSerializer(TicketApplySerializer): class ApplyCommandConfirmSerializer(TicketApplySerializer):
class Meta: class Meta:
model = ApplyCommandTicket model = ApplyCommandTicket
fields = TicketApplySerializer.Meta.fields + [ writeable_fields = [
'apply_run_user', 'apply_run_asset', 'apply_run_account', 'apply_run_user', 'apply_run_asset', 'apply_run_account', 'apply_run_command',
'apply_run_command', 'apply_from_session', 'apply_from_cmd_filter', 'apply_from_session', 'apply_from_cmd_filter', 'apply_from_cmd_filter_rule'
'apply_from_cmd_filter_rule'
] ]
fields = TicketApplySerializer.Meta.fields + writeable_fields

View File

@ -75,10 +75,11 @@ class BaseApplyAssetSerializer(serializers.Serializer):
def create(self, validated_data): def create(self, validated_data):
instance = super().create(validated_data) instance = super().create(validated_data)
name = _('Created by ticket ({}-{})').format(instance.title, str(instance.id)[:4]) 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(): if not self.permission_model.objects.filter(name=name).exists():
instance.apply_permission_name = name instance.apply_permission_name = name
instance.save() instance.save(update_fields=['apply_permission_name'])
return instance return instance
raise serializers.ValidationError(_('Permission named `{}` already exists'.format(name))) raise serializers.ValidationError(_('Permission named `{}` already exists'.format(name)))

View File

@ -9,6 +9,5 @@ __all__ = [
class LoginAssetConfirmSerializer(TicketApplySerializer): class LoginAssetConfirmSerializer(TicketApplySerializer):
class Meta: class Meta:
model = ApplyLoginAssetTicket model = ApplyLoginAssetTicket
fields = TicketApplySerializer.Meta.fields + [ writeable_fields = ['apply_login_user', 'apply_login_asset', 'apply_login_account']
'apply_login_user', 'apply_login_asset', 'apply_login_account' fields = TicketApplySerializer.Meta.fields + writeable_fields
]

View File

@ -7,8 +7,7 @@ __all__ = [
class LoginConfirmSerializer(TicketApplySerializer): class LoginConfirmSerializer(TicketApplySerializer):
class Meta: class Meta(TicketApplySerializer.Meta):
model = ApplyLoginTicket model = ApplyLoginTicket
fields = TicketApplySerializer.Meta.fields + [ writeable_fields = ['apply_login_ip', 'apply_login_city', 'apply_login_datetime']
'apply_login_ip', 'apply_login_city', 'apply_login_datetime' fields = TicketApplySerializer.Meta.fields + writeable_fields
]

View File

@ -22,13 +22,12 @@ class TicketSerializer(OrgResourceModelSerializerMixin):
class Meta: class Meta:
model = Ticket model = Ticket
fields_mini = ['id', 'title'] fields_mini = ['id', 'title']
fields_small = fields_mini + [ fields_small = fields_mini + ['org_id', 'comment']
'type', 'status', 'state', 'approval_step', 'comment', read_only_fields = [
'date_created', 'date_updated', 'org_id', 'rel_snapshot', 'serial_num', 'process_map', 'approval_step', 'type', 'state', 'applicant',
'process_map', 'org_name', 'serial_num' 'status', 'date_created', 'date_updated', 'org_name', 'rel_snapshot'
] ]
fields_fk = ['applicant', ] fields = fields_small + read_only_fields
fields = fields_small + fields_fk
extra_kwargs = { extra_kwargs = {
'type': {'required': True} 'type': {'required': True}
} }
@ -72,8 +71,6 @@ class TicketApplySerializer(TicketSerializer):
if self.instance: if self.instance:
return attrs return attrs
print("Attrs: ", attrs)
ticket_type = attrs.get('type') ticket_type = attrs.get('type')
org_id = attrs.get('org_id') org_id = attrs.get('org_id')
flow = TicketFlow.get_org_related_flows(org_id=org_id) \ flow = TicketFlow.get_org_related_flows(org_id=org_id) \
@ -81,7 +78,7 @@ class TicketApplySerializer(TicketSerializer):
if flow: if flow:
attrs['flow'] = flow attrs['flow'] = flow
return attrs
else: else:
error = _('The ticket flow `{}` does not exist'.format(ticket_type)) error = _('The ticket flow `{}` does not exist'.format(ticket_type))
raise serializers.ValidationError(error) raise serializers.ValidationError(error)
return attrs