Compare commits

...

12 Commits

Author SHA1 Message Date
fit2bot
cabff2b4aa feat: Update v2.28.3 2022-12-12 17:12:50 +08:00
feng
4ce4bde368 fix: ticket xss inject 2022-12-12 17:03:29 +08:00
halo
809bad271a fix: 密钥指纹参数 2022-12-09 13:41:38 +08:00
Eric
d3bfc03849 fix: 替换解析公钥的方式 2022-12-08 16:57:22 +08:00
Bai
04c0121b37 fix: 降级 Djanog==3.2.15 2022-12-08 14:53:40 +08:00
jiangweidong
b97b50ab31 perf: 支持sentinel开启ssl(Sentinel和Redis公用一套证书,无额外增加配置项) 2022-12-08 12:54:58 +08:00
Eric
d8a8c8153b fix: TraditionalOpenSSL private ssh key 2022-12-08 11:03:52 +08:00
Eric
a68ad7be68 perf: support ed25519 SSH Key
fix: codacy ci
fix: password use bytes
2022-12-08 11:03:52 +08:00
Bai
4041f1aeec fix: 修改 random_string 方法,支持只生成随机数字 2022-12-01 20:13:47 +08:00
feng
59388655ea fix: es 默认存储500 2022-11-18 17:04:43 +08:00
Bai
ef7463c588 fix: flower db file 持久化存储flower信息 2022-11-18 15:36:21 +08:00
Bryan
7e7d6d94e6 fix: 修复 channels-redis 库升级导致 ws 查看任务日志失败的问题; 修改 REDIS_LAYERS_HOST 变量; 修改 Channel SSL 配置项; 2022-11-18 15:26:44 +08:00
15 changed files with 221 additions and 108 deletions

1
GITSHA Normal file
View File

@@ -0,0 +1 @@
4ce4bde3681a785d21b73be5838502d0c763b296

View File

@@ -6,23 +6,21 @@ import uuid
from hashlib import md5 from hashlib import md5
import sshpubkeys import sshpubkeys
from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.db import models from django.db import models
from django.db.models import QuerySet
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from django.db.models import QuerySet
from common.utils import random_string, signer from common.db import fields
from common.utils import random_string
from common.utils import ( from common.utils import (
ssh_key_string_to_obj, ssh_key_gen, get_logger, lazyproperty ssh_key_string_to_obj, ssh_key_gen, get_logger, lazyproperty
) )
from common.utils.encode import ssh_pubkey_gen from common.utils.encode import parse_ssh_public_key_str
from common.validators import alphanumeric
from common.db import fields
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
logger = get_logger(__file__) logger = get_logger(__file__)
@@ -68,7 +66,7 @@ class AuthMixin:
public_key = self.public_key public_key = self.public_key
elif self.private_key: elif self.private_key:
try: try:
public_key = ssh_pubkey_gen(private_key=self.private_key, password=self.password) public_key = parse_ssh_public_key_str(self.private_key, password=self.password)
except IOError as e: except IOError as e:
return str(e) return str(e)
else: else:
@@ -234,4 +232,3 @@ class BaseUser(OrgModelMixin, AuthMixin):
class Meta: class Meta:
abstract = True abstract = True

View File

@@ -1,24 +1,24 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from io import StringIO
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key
from common.drf.fields import EncryptedField
from assets.models import Type from assets.models import Type
from common.drf.fields import EncryptedField
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str, parse_ssh_public_key_str
from .utils import validate_password_for_ansible from .utils import validate_password_for_ansible
class AuthSerializer(serializers.ModelSerializer): class AuthSerializer(serializers.ModelSerializer):
password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024, label=_('Password')) password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024, label=_('Password'))
private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=16384, label=_('Private key')) private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=16384,
label=_('Private key'))
def gen_keys(self, private_key=None, password=None): def gen_keys(self, private_key=None, password=None):
if private_key is None: if private_key is None:
return None, None return None, None
public_key = ssh_pubkey_gen(private_key=private_key, password=password) public_key = parse_ssh_public_key_str(text=private_key, password=password)
return private_key, public_key return private_key, public_key
def save(self, **kwargs): def save(self, **kwargs):
@@ -57,10 +57,7 @@ class AuthSerializerMixin(serializers.ModelSerializer):
if not valid: if not valid:
raise serializers.ValidationError(_("private key invalid or passphrase error")) raise serializers.ValidationError(_("private key invalid or passphrase error"))
private_key = ssh_private_key_gen(private_key, password=passphrase) private_key = parse_ssh_private_key_str(private_key, password=passphrase)
string_io = StringIO()
private_key.write_private_key(string_io)
private_key = string_io.getvalue()
return private_key return private_key
def validate_public_key(self, public_key): def validate_public_key(self, public_key):

View File

@@ -1,16 +1,16 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from django.db.models import Count from django.db.models import Count
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.mixins.serializers import BulkSerializerMixin
from common.utils import ssh_pubkey_gen
from common.drf.fields import EncryptedField from common.drf.fields import EncryptedField
from common.drf.serializers import SecretReadableMixin from common.drf.serializers import SecretReadableMixin
from common.mixins.serializers import BulkSerializerMixin
from common.utils import parse_ssh_public_key_str
from common.validators import alphanumeric_re, alphanumeric_cn_re, alphanumeric_win_re from common.validators import alphanumeric_re, alphanumeric_cn_re, alphanumeric_win_re
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import SystemUser, Asset
from .utils import validate_password_for_ansible
from .base import AuthSerializerMixin from .base import AuthSerializerMixin
from .utils import validate_password_for_ansible
from ..models import SystemUser, Asset
__all__ = [ __all__ = [
'SystemUserSerializer', 'MiniSystemUserSerializer', 'SystemUserSerializer', 'MiniSystemUserSerializer',
@@ -214,7 +214,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
elif attrs.get('private_key'): elif attrs.get('private_key'):
private_key = attrs['private_key'] private_key = attrs['private_key']
password = attrs.get('password') password = attrs.get('password')
public_key = ssh_pubkey_gen(private_key, password=password, username=username) public_key = parse_ssh_public_key_str(private_key, password=password)
attrs['public_key'] = public_key attrs['public_key'] = public_key
return attrs return attrs

View File

@@ -1,6 +1,8 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
def validate_password_for_ansible(password): def validate_password_for_ansible(password):
""" 校验 Ansible 不支持的特殊字符 """ """ 校验 Ansible 不支持的特殊字符 """
@@ -15,3 +17,9 @@ def validate_password_for_ansible(password):
if '"' in password: if '"' in password:
raise serializers.ValidationError(_('Password can not contains `"` ')) raise serializers.ValidationError(_('Password can not contains `"` '))
def validate_ssh_key(ssh_key, passphrase=None):
valid = validate_ssh_private_key(ssh_key, password=passphrase)
if not valid:
raise serializers.ValidationError(_("private key invalid or passphrase error"))
return parse_ssh_private_key_str(ssh_key, passphrase)

View File

@@ -9,6 +9,10 @@ class FlowerService(BaseService):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
@property
def db_file(self):
return os.path.join(BASE_DIR, 'data', 'flower')
@property @property
def cmd(self): def cmd(self):
print("\n- Start Flower as Task Monitor") print("\n- Start Flower as Task Monitor")
@@ -20,11 +24,11 @@ class FlowerService(BaseService):
'-A', 'ops', '-A', 'ops',
'flower', 'flower',
'-logging=info', '-logging=info',
'-db={}'.format(self.db_file),
'--url_prefix=/core/flower', '--url_prefix=/core/flower',
'--auto_refresh=False', '--auto_refresh=False',
'--max_tasks=1000', '--max_tasks=1000',
'--persistent=True', '--persistent=True',
'-db=/opt/jumpserver/data/flower.db',
'--state_save_interval=600000' '--state_save_interval=600000'
] ]
return cmd return cmd

View File

@@ -1,24 +1,23 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import re
import json
from six import string_types
import base64 import base64
import os
import time
import hashlib import hashlib
import json
import os
import re
import time
from io import StringIO from io import StringIO
from itertools import chain
import paramiko import paramiko
import sshpubkeys import sshpubkeys
from cryptography.hazmat.primitives import serialization
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from itsdangerous import ( from itsdangerous import (
TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer, TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer,
BadSignature, SignatureExpired BadSignature, SignatureExpired
) )
from django.conf import settings from six import string_types
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models.fields.files import FileField
from .http import http_date from .http import http_date
@@ -69,22 +68,25 @@ class Signer(metaclass=Singleton):
return None return None
_supported_paramiko_ssh_key_types = (
paramiko.RSAKey,
paramiko.DSSKey,
paramiko.Ed25519Key,
paramiko.ECDSAKey,
)
def ssh_key_string_to_obj(text, password=None): def ssh_key_string_to_obj(text, password=None):
key = None key = None
try: for ssh_key_type in _supported_paramiko_ssh_key_types:
key = paramiko.RSAKey.from_private_key(StringIO(text), password=password) if not isinstance(ssh_key_type, paramiko.PKey):
except paramiko.SSHException: continue
pass try:
else: key = ssh_key_type.from_private_key(StringIO(text), password=password)
return key except paramiko.SSHException:
pass
try: else:
key = paramiko.DSSKey.from_private_key(StringIO(text), password=password) return key
except paramiko.SSHException:
pass
else:
return key
return key return key
@@ -98,7 +100,7 @@ def ssh_private_key_gen(private_key, password=None):
def ssh_pubkey_gen(private_key=None, username='jumpserver', hostname='localhost', password=None): def ssh_pubkey_gen(private_key=None, username='jumpserver', hostname='localhost', password=None):
private_key = ssh_private_key_gen(private_key, password=password) private_key = ssh_private_key_gen(private_key, password=password)
if not isinstance(private_key, (paramiko.RSAKey, paramiko.DSSKey)): if not isinstance(private_key, _supported_paramiko_ssh_key_types):
raise IOError('Invalid private key') raise IOError('Invalid private key')
public_key = "%(key_type)s %(key_content)s %(username)s@%(hostname)s" % { public_key = "%(key_type)s %(key_content)s %(username)s@%(hostname)s" % {
@@ -137,17 +139,63 @@ def ssh_key_gen(length=2048, type='rsa', password=None, username='jumpserver', h
def validate_ssh_private_key(text, password=None): def validate_ssh_private_key(text, password=None):
if isinstance(text, bytes): key = parse_ssh_private_key_str(text, password=password)
try: return bool(key)
text = text.decode("utf-8")
except UnicodeDecodeError:
return False
key = ssh_key_string_to_obj(text, password=password)
if key is None: def parse_ssh_private_key_str(text: bytes, password=None) -> str:
return False private_key = _parse_ssh_private_key(text, password=password)
else: if private_key is None:
return True return ""
private_key_bytes = private_key.private_bytes(serialization.Encoding.PEM,
serialization.PrivateFormat.OpenSSH,
serialization.NoEncryption())
return private_key_bytes.decode('utf-8')
def parse_ssh_public_key_str(text: bytes = "", password=None) -> str:
private_key = _parse_ssh_private_key(text, password=password)
if private_key is None:
return ""
public_key_bytes = private_key.public_key().public_bytes(
serialization.Encoding.OpenSSH,
serialization.PublicFormat.OpenSSH)
return public_key_bytes.decode('utf-8')
def _parse_ssh_private_key(text, password=None):
"""
text: bytes
password: str
return:private key types:
ec.EllipticCurvePrivateKey,
rsa.RSAPrivateKey,
dsa.DSAPrivateKey,
ed25519.Ed25519PrivateKey,
"""
if isinstance(text, str):
try:
text = text.encode("utf-8")
except UnicodeDecodeError:
return None
if password is not None:
if isinstance(password, str):
try:
password = password.encode("utf-8")
except UnicodeDecodeError:
return None
try:
if is_openssh_format_key(text):
return serialization.load_ssh_private_key(text, password=password)
return serialization.load_pem_private_key(text, password=password)
except (ValueError, TypeError):
pass
return None
def is_openssh_format_key(text: bytes):
return text.startswith(b"-----BEGIN OPENSSH PRIVATE KEY-----")
def validate_ssh_public_key(text): def validate_ssh_public_key(text):

View File

@@ -18,25 +18,35 @@ def random_ip():
return socket.inet_ntoa(struct.pack('>I', random.randint(1, 0xffffffff))) return socket.inet_ntoa(struct.pack('>I', random.randint(1, 0xffffffff)))
def random_string(length, lower=True, upper=True, digit=True, special_char=False): def random_string(length: int, lower=True, upper=True, digit=True, special_char=False):
chars = string.ascii_letters args_names = ['lower', 'upper', 'digit', 'special_char']
if digit: args_values = [lower, upper, digit, special_char]
chars += string.digits args_string = [string.ascii_lowercase, string.ascii_uppercase, string.digits, string_punctuation]
args_string_map = dict(zip(args_names, args_string))
kwargs = dict(zip(args_names, args_values))
kwargs_keys = list(kwargs.keys())
kwargs_values = list(kwargs.values())
args_true_count = len([i for i in kwargs_values if i])
assert any(kwargs_values), f'Parameters {kwargs_keys} must have at least one `True`'
assert length >= args_true_count, f'Expected length >= {args_true_count}, bug got {length}'
can_startswith_special_char = args_true_count == 1 and special_char
chars = ''.join([args_string_map[k] for k, v in kwargs.items() if v])
while True: while True:
password = list(random.choice(chars) for i in range(length)) password = list(random.choice(chars) for i in range(length))
if upper and not any(c.upper() for c in password): for k, v in kwargs.items():
continue if v and not (set(password) & set(args_string_map[k])):
if lower and not any(c.lower() for c in password): # 没有包含指定的字符, retry
continue break
if digit and not any(c.isdigit() for c in password): else:
continue if not can_startswith_special_char and password[0] in args_string_map['special_char']:
break # 首位不能为特殊字符, retry
continue
if special_char: else:
spc = random.choice(string_punctuation) # 满足要求终止 while 循环
i = random.choice(range(1, len(password))) break
password[i] = spc
password = ''.join(password) password = ''.join(password)
return password return password

View File

@@ -202,6 +202,7 @@ class Config(dict):
'REDIS_SSL_KEY': None, 'REDIS_SSL_KEY': None,
'REDIS_SSL_CERT': None, 'REDIS_SSL_CERT': None,
'REDIS_SSL_CA': None, 'REDIS_SSL_CA': None,
'REDIS_SSL_REQUIRED': 'none',
# Redis Sentinel # Redis Sentinel
'REDIS_SENTINEL_HOSTS': '', 'REDIS_SENTINEL_HOSTS': '',
'REDIS_SENTINEL_PASSWORD': '', 'REDIS_SENTINEL_PASSWORD': '',

View File

@@ -1,6 +1,9 @@
import os import os
import platform import platform
from redis.sentinel import SentinelManagedSSLConnection
if platform.system() == 'Darwin' and platform.machine() == 'arm64': if platform.system() == 'Darwin' and platform.machine() == 'arm64':
import pymysql import pymysql
@@ -195,7 +198,7 @@ DATABASES = {
} }
} }
DB_CA_PATH = os.path.join(PROJECT_DIR, 'data', 'certs', 'db_ca.pem') DB_CA_PATH = os.path.join(CERTS_DIR, 'db_ca.pem')
DB_USE_SSL = False DB_USE_SSL = False
if CONFIG.DB_ENGINE.lower() == 'mysql': if CONFIG.DB_ENGINE.lower() == 'mysql':
DB_OPTIONS['init_command'] = "SET sql_mode='STRICT_TRANS_TABLES'" DB_OPTIONS['init_command'] = "SET sql_mode='STRICT_TRANS_TABLES'"
@@ -317,10 +320,19 @@ if REDIS_SENTINEL_SERVICE_NAME and REDIS_SENTINELS:
'CLIENT_CLASS': 'django_redis.client.SentinelClient', 'CLIENT_CLASS': 'django_redis.client.SentinelClient',
'SENTINELS': REDIS_SENTINELS, 'PASSWORD': CONFIG.REDIS_PASSWORD, 'SENTINELS': REDIS_SENTINELS, 'PASSWORD': CONFIG.REDIS_PASSWORD,
'SENTINEL_KWARGS': { 'SENTINEL_KWARGS': {
'ssl': REDIS_USE_SSL,
'ssl_cert_reqs': REDIS_SSL_REQUIRED,
"ssl_keyfile": REDIS_SSL_KEY,
"ssl_certfile": REDIS_SSL_CERT,
"ssl_ca_certs": REDIS_SSL_CA,
'password': REDIS_SENTINEL_PASSWORD, 'password': REDIS_SENTINEL_PASSWORD,
'socket_timeout': REDIS_SENTINEL_SOCKET_TIMEOUT 'socket_timeout': REDIS_SENTINEL_SOCKET_TIMEOUT
} }
}) })
if REDIS_USE_SSL:
REDIS_OPTIONS['CONNECTION_POOL_KWARGS'].update({
'connection_class': SentinelManagedSSLConnection
})
DJANGO_REDIS_CONNECTION_FACTORY = 'django_redis.pool.SentinelConnectionFactory' DJANGO_REDIS_CONNECTION_FACTORY = 'django_redis.pool.SentinelConnectionFactory'
else: else:
REDIS_LOCATION_NO_DB = '%(protocol)s://:%(password)s@%(host)s:%(port)s/{}' % { REDIS_LOCATION_NO_DB = '%(protocol)s://:%(password)s@%(host)s:%(port)s/{}' % {

View File

@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import os import os
import ssl from urllib.parse import urlencode
from .base import ( from .base import (
REDIS_SSL_CA, REDIS_SSL_CERT, REDIS_SSL_KEY, REDIS_SSL_REQUIRED, REDIS_USE_SSL, REDIS_SSL_CA, REDIS_SSL_CERT, REDIS_SSL_KEY, REDIS_SSL_REQUIRED, REDIS_USE_SSL,
REDIS_SENTINEL_SERVICE_NAME, REDIS_SENTINELS, REDIS_SENTINEL_PASSWORD, REDIS_PROTOCOL, REDIS_SENTINEL_SERVICE_NAME, REDIS_SENTINELS, REDIS_SENTINEL_PASSWORD,
REDIS_SENTINEL_SOCKET_TIMEOUT REDIS_SENTINEL_SOCKET_TIMEOUT
) )
from ..const import CONFIG, PROJECT_DIR from ..const import CONFIG, PROJECT_DIR
@@ -81,41 +81,54 @@ BOOTSTRAP3 = {
} }
# Django channels support websocket # Django channels support websocket
if not REDIS_USE_SSL: REDIS_LAYERS_HOST = {
redis_ssl = None
else:
redis_ssl = ssl.SSLContext()
redis_ssl.check_hostname = bool(CONFIG.REDIS_SSL_REQUIRED)
if REDIS_SSL_CA:
redis_ssl.load_verify_locations(REDIS_SSL_CA)
if REDIS_SSL_CERT and REDIS_SSL_KEY:
redis_ssl.load_cert_chain(REDIS_SSL_CERT, REDIS_SSL_KEY)
REDIS_HOST = {
'db': CONFIG.REDIS_DB_WS, 'db': CONFIG.REDIS_DB_WS,
'password': CONFIG.REDIS_PASSWORD or None, 'password': CONFIG.REDIS_PASSWORD or None,
'ssl': redis_ssl,
} }
REDIS_LAYERS_SSL_PARAMS = {}
if REDIS_USE_SSL:
REDIS_LAYERS_SSL_PARAMS.update({
'ssl': REDIS_USE_SSL,
'ssl_cert_reqs': REDIS_SSL_REQUIRED,
"ssl_keyfile": REDIS_SSL_KEY,
"ssl_certfile": REDIS_SSL_CERT,
"ssl_ca_certs": REDIS_SSL_CA
})
REDIS_LAYERS_HOST.update(REDIS_LAYERS_SSL_PARAMS)
if REDIS_SENTINEL_SERVICE_NAME and REDIS_SENTINELS: if REDIS_SENTINEL_SERVICE_NAME and REDIS_SENTINELS:
REDIS_HOST['sentinels'] = REDIS_SENTINELS REDIS_LAYERS_HOST['sentinels'] = REDIS_SENTINELS
REDIS_HOST['master_name'] = REDIS_SENTINEL_SERVICE_NAME REDIS_LAYERS_HOST['master_name'] = REDIS_SENTINEL_SERVICE_NAME
REDIS_HOST['sentinel_kwargs'] = { REDIS_LAYERS_HOST['sentinel_kwargs'] = {
'password': REDIS_SENTINEL_PASSWORD, 'password': REDIS_SENTINEL_PASSWORD,
'socket_timeout': REDIS_SENTINEL_SOCKET_TIMEOUT 'socket_timeout': REDIS_SENTINEL_SOCKET_TIMEOUT,
'ssl': REDIS_USE_SSL,
'ssl_cert_reqs': REDIS_SSL_REQUIRED,
"ssl_keyfile": REDIS_SSL_KEY,
"ssl_certfile": REDIS_SSL_CERT,
"ssl_ca_certs": REDIS_SSL_CA
} }
else: else:
REDIS_HOST['address'] = (CONFIG.REDIS_HOST, CONFIG.REDIS_PORT) # More info see: https://github.com/django/channels_redis/issues/334
# REDIS_LAYERS_HOST['address'] = (CONFIG.REDIS_HOST, CONFIG.REDIS_PORT)
REDIS_LAYERS_ADDRESS = '{protocol}://:{password}@{host}:{port}/{db}'.format(
protocol=REDIS_PROTOCOL, password=CONFIG.REDIS_PASSWORD,
host=CONFIG.REDIS_HOST, port=CONFIG.REDIS_PORT, db=CONFIG.REDIS_DB_WS
)
REDIS_LAYERS_SSL_PARAMS.pop('ssl', None)
REDIS_LAYERS_HOST['address'] = '{}?{}'.format(REDIS_LAYERS_ADDRESS, urlencode(REDIS_LAYERS_SSL_PARAMS))
CHANNEL_LAYERS = { CHANNEL_LAYERS = {
'default': { 'default': {
'BACKEND': 'common.cache.RedisChannelLayer', 'BACKEND': 'common.cache.RedisChannelLayer',
'CONFIG': { 'CONFIG': {
"hosts": [REDIS_HOST], "hosts": [REDIS_LAYERS_HOST],
}, },
}, },
} }
ASGI_APPLICATION = 'jumpserver.routing.application' ASGI_APPLICATION = 'jumpserver.routing.application'
# Dump all celery log to here # Dump all celery log to here
@@ -132,13 +145,18 @@ if REDIS_SENTINEL_SERVICE_NAME and REDIS_SENTINELS:
'master_name': REDIS_SENTINEL_SERVICE_NAME, 'master_name': REDIS_SENTINEL_SERVICE_NAME,
'sentinel_kwargs': { 'sentinel_kwargs': {
'password': REDIS_SENTINEL_PASSWORD, 'password': REDIS_SENTINEL_PASSWORD,
'socket_timeout': REDIS_SENTINEL_SOCKET_TIMEOUT 'socket_timeout': REDIS_SENTINEL_SOCKET_TIMEOUT,
'ssl': REDIS_USE_SSL,
'ssl_cert_reqs': REDIS_SSL_REQUIRED,
"ssl_keyfile": REDIS_SSL_KEY,
"ssl_certfile": REDIS_SSL_CERT,
"ssl_ca_certs": REDIS_SSL_CA
} }
} }
CELERY_BROKER_TRANSPORT_OPTIONS = CELERY_RESULT_BACKEND_TRANSPORT_OPTIONS = SENTINEL_OPTIONS CELERY_BROKER_TRANSPORT_OPTIONS = CELERY_RESULT_BACKEND_TRANSPORT_OPTIONS = SENTINEL_OPTIONS
else: else:
CELERY_BROKER_URL = CELERY_BROKER_URL_FORMAT % { CELERY_BROKER_URL = CELERY_BROKER_URL_FORMAT % {
'protocol': 'rediss' if REDIS_USE_SSL else 'redis', 'protocol': REDIS_PROTOCOL,
'password': CONFIG.REDIS_PASSWORD, 'password': CONFIG.REDIS_PASSWORD,
'host': CONFIG.REDIS_HOST, 'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT, 'port': CONFIG.REDIS_PORT,

View File

@@ -19,7 +19,6 @@ from .terminal import Terminal
from .command import Command from .command import Command
from .. import const from .. import const
logger = get_logger(__file__) logger = get_logger(__file__)
@@ -37,10 +36,10 @@ class CommonStorageModelMixin(models.Model):
def set_to_default(self): def set_to_default(self):
self.is_default = True self.is_default = True
self.save() self.save(update_fields=['is_default'])
self.__class__.objects.select_for_update()\ self.__class__.objects.select_for_update() \
.filter(is_default=True)\ .filter(is_default=True) \
.exclude(id=self.id)\ .exclude(id=self.id) \
.update(is_default=False) .update(is_default=False)
@classmethod @classmethod
@@ -128,7 +127,10 @@ class CommandStorage(CommonStorageModelMixin, CommonModelMixin):
def save(self, force_insert=False, force_update=False, using=None, def save(self, force_insert=False, force_update=False, using=None,
update_fields=None): update_fields=None):
super().save() super().save(
force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields
)
if self.type in TYPE_ENGINE_MAPPING: if self.type in TYPE_ENGINE_MAPPING:
engine_mod = import_module(TYPE_ENGINE_MAPPING[self.type]) engine_mod = import_module(TYPE_ENGINE_MAPPING[self.type])

View File

@@ -1,3 +1,5 @@
from html import escape
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.template.loader import render_to_string from django.template.loader import render_to_string
@@ -96,11 +98,19 @@ class BaseHandler:
approve_info = _('{} {} the ticket').format(user_display, state_display) approve_info = _('{} {} the ticket').format(user_display, state_display)
context = self._diff_prev_approve_context(state) context = self._diff_prev_approve_context(state)
context.update({'approve_info': approve_info}) context.update({'approve_info': approve_info})
body = self.reject_html_script(
render_to_string('tickets/ticket_approve_diff.html', context)
)
data = { data = {
'body': render_to_string('tickets/ticket_approve_diff.html', context), 'body': body,
'user': user, 'user': user,
'user_display': str(user), 'user_display': str(user),
'type': 'state', 'type': 'state',
'state': state 'state': state
} }
return self.ticket.comments.create(**data) return self.ticket.comments.create(**data)
@staticmethod
def reject_html_script(unsafe_html):
safe_html = escape(unsafe_html)
return safe_html

View File

@@ -63,7 +63,7 @@ jsonfield2==4.0.0.post0
geoip2==4.5.0 geoip2==4.5.0
ipip-ipdb==1.6.1 ipip-ipdb==1.6.1
# Django environment # Django environment
Django==3.2.16 Django==3.2.15
django-bootstrap3==14.2.0 django-bootstrap3==14.2.0
django-filter==2.4.0 django-filter==2.4.0
django-formtools==2.2 django-formtools==2.2

View File

@@ -26,7 +26,7 @@ connection_params = {
if settings.REDIS_USE_SSL: if settings.REDIS_USE_SSL:
connection_params['ssl'] = settings.REDIS_USE_SSL connection_params['ssl'] = settings.REDIS_USE_SSL
connection_params['ssl_cert_reqs'] = settings.REDIS_SSL_REQUIRED connection_params['ssl_cert_reqs'] = settings.REDIS_SSL_REQUIRED
connection_params['ssl_keyfile'] = settings.REDIS_SSL_KEY connection_params['ssl_keyfile'] = settings.REDIS_SSL_KEY
connection_params['ssl_certfile'] = settings.REDIS_SSL_CERT connection_params['ssl_certfile'] = settings.REDIS_SSL_CERT
connection_params['ssl_ca_certs'] = settings.REDIS_SSL_CA connection_params['ssl_ca_certs'] = settings.REDIS_SSL_CA
@@ -39,6 +39,11 @@ if REDIS_SENTINEL_SERVICE_NAME and REDIS_SENTINELS:
connection_params['sentinels'] = REDIS_SENTINELS connection_params['sentinels'] = REDIS_SENTINELS
sentinel_client = Sentinel( sentinel_client = Sentinel(
**connection_params, sentinel_kwargs={ **connection_params, sentinel_kwargs={
'ssl': settings.REDIS_USE_SSL,
'ssl_cert_reqs': settings.REDIS_SSL_REQUIRED,
'ssl_keyfile': settings.REDIS_SSL_KEY,
'ssl_certfile': settings.REDIS_SSL_CERT,
'ssl_ca_certs': settings.REDIS_SSL_CA,
'password': REDIS_SENTINEL_PASSWORD, 'password': REDIS_SENTINEL_PASSWORD,
'socket_timeout': REDIS_SENTINEL_SOCKET_TIMEOUT 'socket_timeout': REDIS_SENTINEL_SOCKET_TIMEOUT
} }