From 708a87c903384865adf966d37b02306474eada1b Mon Sep 17 00:00:00 2001
From: jiangweidong <80373698+F2C-Jiang@users.noreply.github.com>
Date: Tue, 9 Aug 2022 16:09:20 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81CMPPv2.0=E5=8D=8F?=
=?UTF-8?q?=E8=AE=AE=E7=9F=AD=E4=BF=A1=E7=BD=91=E5=85=B3=20(#8591)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: 支持CMPPv2.0协议短信网关
* 修改翻译
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
---
apps/common/sdk/sms/base.py | 4 +
apps/common/sdk/sms/cmpp2.py | 322 ++++++++++++++++++++++++++
apps/common/sdk/sms/endpoint.py | 3 +-
apps/jumpserver/conf.py | 9 +
apps/locale/ja/LC_MESSAGES/django.po | 153 +++++++-----
apps/locale/zh/LC_MESSAGES/django.po | 152 +++++++-----
apps/settings/api/__init__.py | 2 -
apps/settings/api/alibaba_sms.py | 58 -----
apps/settings/api/settings.py | 1 +
apps/settings/api/sms.py | 118 +++++++++-
apps/settings/api/tencent_sms.py | 63 -----
apps/settings/serializers/auth/sms.py | 33 ++-
apps/settings/serializers/settings.py | 3 +-
apps/settings/urls/api_urls.py | 3 +-
14 files changed, 686 insertions(+), 238 deletions(-)
create mode 100644 apps/common/sdk/sms/cmpp2.py
delete mode 100644 apps/settings/api/alibaba_sms.py
delete mode 100644 apps/settings/api/tencent_sms.py
diff --git a/apps/common/sdk/sms/base.py b/apps/common/sdk/sms/base.py
index 4d02370b1..77dcc669a 100644
--- a/apps/common/sdk/sms/base.py
+++ b/apps/common/sdk/sms/base.py
@@ -17,4 +17,8 @@ class BaseSMSClient:
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
raise NotImplementedError
+ @staticmethod
+ def need_pre_check():
+ return True
+
diff --git a/apps/common/sdk/sms/cmpp2.py b/apps/common/sdk/sms/cmpp2.py
new file mode 100644
index 000000000..8a6a4fe32
--- /dev/null
+++ b/apps/common/sdk/sms/cmpp2.py
@@ -0,0 +1,322 @@
+import hashlib
+import socket
+import struct
+import time
+
+from django.conf import settings
+
+from common.utils import get_logger
+from common.exceptions import JMSException
+from .base import BaseSMSClient
+
+
+logger = get_logger(__file__)
+
+
+CMPP_CONNECT = 0x00000001 # 请求连接
+CMPP_CONNECT_RESP = 0x80000001 # 请求连接应答
+CMPP_TERMINATE = 0x00000002 # 终止连接
+CMPP_TERMINATE_RESP = 0x80000002 # 终止连接应答
+CMPP_SUBMIT = 0x00000004 # 提交短信
+CMPP_SUBMIT_RESP = 0x80000004 # 提交短信应答
+CMPP_DELIVER = 0x00000005 # 短信下发
+CMPP_DELIVER_RESP = 0x80000005 # 下发短信应答
+
+
+class CMPPBaseRequestInstance(object):
+ def __init__(self):
+ self.command_id = ''
+ self.body = b''
+ self.length = 0
+
+ def get_header(self, sequence_id):
+ length = struct.pack('!L', 12 + self.length)
+ command_id = struct.pack('!L', self.command_id)
+ sequence_id = struct.pack('!L', sequence_id)
+ return length + command_id + sequence_id
+
+ def get_message(self, sequence_id):
+ return self.get_header(sequence_id) + self.body
+
+
+class CMPPConnectRequestInstance(CMPPBaseRequestInstance):
+ def __init__(self, sp_id, sp_secret):
+ if len(sp_id) != 6:
+ raise ValueError("sp_id and sp_secret are both 6 bits")
+
+ super().__init__()
+
+ source_addr = sp_id.encode('utf-8')
+ sp_secret = sp_secret.encode('utf-8')
+ version = struct.pack('!B', 0x02)
+ timestamp = struct.pack('!L', int(self.get_now()))
+ authenticator_source = source_addr + 9 * b'\x00' + sp_secret + self.get_now().encode('utf-8')
+ auth_source_md5 = hashlib.md5(authenticator_source).digest()
+ self.body = source_addr + auth_source_md5 + version + timestamp
+ self.length = len(self.body)
+ self.command_id = CMPP_CONNECT
+
+ @staticmethod
+ def get_now():
+ return time.strftime('%m%d%H%M%S', time.localtime(time.time()))
+
+
+class CMPPSubmitRequestInstance(CMPPBaseRequestInstance):
+ def __init__(self, msg_src, dest_terminal_id, msg_content, src_id,
+ service_id='', dest_usr_tl=1):
+ if len(msg_content) >= 70:
+ raise JMSException('The message length should be within 70 characters')
+ if len(dest_terminal_id) > 100:
+ raise JMSException('The number of users receiving information should be less than 100')
+
+ super().__init__()
+
+ msg_id = 8 * b'\x00'
+ pk_total = struct.pack('!B', 1)
+ pk_number = struct.pack('!B', 1)
+ registered_delivery = struct.pack('!B', 0)
+ msg_level = struct.pack('!B', 0)
+ service_id = ((10 - len(service_id)) * '\x00' + service_id).encode('utf-8')
+ fee_user_type = struct.pack('!B', 2)
+ fee_terminal_id = ('0' * 21).encode('utf-8')
+ tp_pid = struct.pack('!B', 0)
+ tp_udhi = struct.pack('!B', 0)
+ msg_fmt = struct.pack('!B', 8)
+ fee_type = '01'.encode('utf-8')
+ fee_code = '000000'.encode('utf-8')
+ valid_time = ('\x00' * 17).encode('utf-8')
+ at_time = ('\x00' * 17).encode('utf-8')
+ src_id = ((21 - len(src_id)) * '\x00' + src_id).encode('utf-8')
+ reserve = b'\x00' * 8
+ _msg_length = struct.pack('!B', len(msg_content) * 2)
+ _msg_src = msg_src.encode('utf-8')
+ _dest_usr_tl = struct.pack('!B', dest_usr_tl)
+ _msg_content = msg_content.encode('utf-16-be')
+ _dest_terminal_id = b''.join([
+ (i + (21 - len(i)) * '\x00').encode('utf-8') for i in dest_terminal_id
+ ])
+ self.length = 126 + 21 * dest_usr_tl + len(_msg_content)
+ self.command_id = CMPP_SUBMIT
+ self.body = msg_id + pk_total + pk_number + registered_delivery \
+ + msg_level + service_id + fee_user_type + fee_terminal_id \
+ + tp_pid + tp_udhi + msg_fmt + _msg_src + fee_type + fee_code \
+ + valid_time + at_time + src_id + _dest_usr_tl + _dest_terminal_id \
+ + _msg_length + _msg_content + reserve
+
+
+class CMPPTerminateRequestInstance(CMPPBaseRequestInstance):
+ def __init__(self):
+ super().__init__()
+ self.body = b''
+ self.command_id = CMPP_TERMINATE
+
+
+class CMPPDeliverRespRequestInstance(CMPPBaseRequestInstance):
+ def __init__(self, msg_id, result=0):
+ super().__init__()
+ msg_id = struct.pack('!Q', msg_id)
+ result = struct.pack('!B', result)
+ self.length = len(self.body)
+ self.body = msg_id + result
+
+
+class CMPPResponseInstance(object):
+ def __init__(self):
+ self.command_id = None
+ self.length = None
+ self.response_handler_map = {
+ CMPP_CONNECT_RESP: self.connect_response_parse,
+ CMPP_SUBMIT_RESP: self.submit_response_parse,
+ CMPP_DELIVER: self.deliver_request_parse,
+ }
+
+ @staticmethod
+ def connect_response_parse(body):
+ status, = struct.unpack('!B', body[0:1])
+ authenticator_ISMG = body[1:17]
+ version, = struct.unpack('!B', body[17:18])
+ return {
+ 'Status': status,
+ 'AuthenticatorISMG': authenticator_ISMG,
+ 'Version': version
+ }
+
+ @staticmethod
+ def submit_response_parse(body):
+ msg_id = body[:8]
+ result = struct.unpack('!B', body[8:9])
+ return {
+ 'Msg_Id': msg_id, 'Result': result[0]
+ }
+
+ @staticmethod
+ def deliver_request_parse(body):
+ msg_id, = struct.unpack('!Q', body[0:8])
+ dest_id = body[8:29]
+ service_id = body[29:39]
+ tp_pid = struct.unpack('!B', body[39:40])
+ tp_udhi = struct.unpack('!B', body[40:41])
+ msg_fmt = struct.unpack('!B', body[41:42])
+ src_terminal_id = body[42:63]
+ registered_delivery = struct.unpack('!B', body[63:64])
+ msg_length = struct.unpack('!B', body[64:65])
+ msg_content = body[65:msg_length[0]+65]
+ return {
+ 'Msg_Id': msg_id, 'Dest_Id': dest_id, 'Service_Id': service_id,
+ 'TP_pid': tp_pid, 'TP_udhi': tp_udhi, 'Msg_Fmt': msg_fmt,
+ 'Src_terminal_Id': src_terminal_id, 'Registered_Delivery': registered_delivery,
+ 'Msg_Length': msg_length, 'Msg_content': msg_content
+ }
+
+ def parse_header(self, data):
+ self.command_id, = struct.unpack('!L', data[4:8])
+ sequence_id, = struct.unpack('!L', data[8:12])
+ return {
+ 'length': self.length,
+ 'command_id': hex(self.command_id),
+ 'sequence_id': sequence_id
+ }
+
+ def parse_body(self, body):
+ response_body_func = self.response_handler_map.get(self.command_id)
+ if response_body_func is None:
+ raise JMSException('Unable to parse the returned result: %s' % body)
+ return response_body_func(body)
+
+ def parse(self, data):
+ self.length, = struct.unpack('!L', data[0:4])
+ header = self.parse_header(data)
+ body = self.parse_body(data[12:self.length])
+ return header, body
+
+
+class CMPPClient(object):
+ def __init__(self, host, port, sp_id, sp_secret, src_id, service_id):
+ self.ip = host
+ self.port = port
+ self.sp_id = sp_id
+ self.sp_secret = sp_secret
+ self.src_id = src_id
+ self.service_id = service_id
+ self._sequence_id = 0
+ self._is_connect = False
+ self._times = 3
+ self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self._connect()
+
+ @property
+ def sequence_id(self):
+ s = self._sequence_id
+ self._sequence_id += 1
+ return s
+
+ def _connect(self):
+ self.__socket.settimeout(5)
+ for i in range(self._times):
+ try:
+ self.__socket.connect((self.ip, self.port))
+ except socket.timeout:
+ time.sleep(1)
+ else:
+ self._is_connect = True
+ break
+
+ def send(self, instance):
+ if isinstance(instance, CMPPBaseRequestInstance):
+ message = instance.get_message(sequence_id=self.sequence_id)
+ else:
+ message = instance
+ self.__socket.send(message)
+
+ def recv(self):
+ raw_length = self.__socket.recv(4)
+ length, = struct.unpack('!L', raw_length)
+ header, body = CMPPResponseInstance().parse(
+ raw_length + self.__socket.recv(length - 4)
+ )
+ return header, body
+
+ def close(self):
+ if self._is_connect:
+ terminate_request = CMPPTerminateRequestInstance()
+ self.send(terminate_request)
+ self.__socket.close()
+
+ def _cmpp_connect(self):
+ connect_request = CMPPConnectRequestInstance(self.sp_id, self.sp_secret)
+ self.send(connect_request)
+ header, body = self.recv()
+ if body['Status'] != 0:
+ raise JMSException('CMPPv2.0 authentication failed: %s' % body)
+
+ def _cmpp_send_sms(self, dest, sign_name, template_code, template_param):
+ """
+ 优先发送template_param中message的信息
+ 若该内容不存在,则根据template_code构建验证码发送
+ """
+ message = template_param.get('message')
+ if message is None:
+ code = template_param.get('code')
+ message = template_code.replace('{code}', code)
+ msg = '【%s】 %s' % (sign_name, message)
+ submit_request = CMPPSubmitRequestInstance(
+ msg_src=self.sp_id, src_id=self.src_id, msg_content=msg,
+ dest_usr_tl=len(dest), dest_terminal_id=dest,
+ service_id=self.service_id
+ )
+ self.send(submit_request)
+ header, body = self.recv()
+ command_id = header.get('command_id')
+ if command_id == CMPP_DELIVER:
+ deliver_request = CMPPDeliverRespRequestInstance(
+ msg_id=body['Msg_Id'], result=body['Result']
+ )
+ self.send(deliver_request)
+
+ def send_sms(self, dest, sign_name, template_code, template_param):
+ try:
+ self._cmpp_connect()
+ self._cmpp_send_sms(dest, sign_name, template_code, template_param)
+ except Exception as e:
+ logger.error('CMPPv2.0 Error: %s', e)
+ self.close()
+ raise JMSException(e)
+
+
+class CMPP2SMS(BaseSMSClient):
+ SIGN_AND_TMPL_SETTING_FIELD_PREFIX = 'CMPP2'
+
+ @classmethod
+ def new_from_settings(cls):
+ return cls(
+ host=settings.CMPP2_HOST, port=settings.CMPP2_PORT,
+ sp_id=settings.CMPP2_SP_ID, sp_secret=settings.CMPP2_SP_SECRET,
+ service_id=settings.CMPP2_SERVICE_ID, src_id=getattr(settings, 'CMPP2_SRC_ID', ''),
+ )
+
+ def __init__(self, host: str, port: int, sp_id: str, sp_secret: str, service_id: str, src_id=''):
+ try:
+ self.client = CMPPClient(
+ host=host, port=port, sp_id=sp_id, sp_secret=sp_secret, src_id=src_id, service_id=service_id
+ )
+ except socket.timeout:
+ self.client = None
+ logger.warning('CMPPv2.0 connect remote time out.')
+
+ @staticmethod
+ def need_pre_check():
+ return False
+
+ def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
+ try:
+ logger.info(f'CMPPv2.0 sms send: '
+ f'phone_numbers={phone_numbers} '
+ f'sign_name={sign_name} '
+ f'template_code={template_code} '
+ f'template_param={template_param}')
+ self.client.send_sms(phone_numbers, sign_name, template_code, template_param)
+ except Exception as e:
+ raise JMSException(e)
+
+
+client = CMPP2SMS
diff --git a/apps/common/sdk/sms/endpoint.py b/apps/common/sdk/sms/endpoint.py
index 610bf2d99..3bcaa8559 100644
--- a/apps/common/sdk/sms/endpoint.py
+++ b/apps/common/sdk/sms/endpoint.py
@@ -15,6 +15,7 @@ logger = get_logger(__name__)
class BACKENDS(TextChoices):
ALIBABA = 'alibaba', _('Alibaba cloud')
TENCENT = 'tencent', _('Tencent cloud')
+ CMPP2 = 'cmpp2', _('CMPP v2.0')
class SMS:
@@ -43,7 +44,7 @@ class SMS:
sign_name = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_SIGN_NAME')
template_code = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_TEMPLATE_CODE')
- if not (sign_name and template_code):
+ if self.client.need_pre_check() and not (sign_name and template_code):
raise JMSException(
code='verify_code_sign_tmpl_invalid',
detail=_('SMS verification code signature or template invalid')
diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py
index 78e73ba6c..7b26b4ee2 100644
--- a/apps/jumpserver/conf.py
+++ b/apps/jumpserver/conf.py
@@ -376,6 +376,15 @@ class Config(dict):
'TENCENT_VERIFY_SIGN_NAME': '',
'TENCENT_VERIFY_TEMPLATE_CODE': '',
+ 'CMPP2_HOST': '',
+ 'CMPP2_PORT': 7890,
+ 'CMPP2_SP_ID': '',
+ 'CMPP2_SP_SECRET': '',
+ 'CMPP2_SRC_ID': '',
+ 'CMPP2_SERVICE_ID': '',
+ 'CMPP2_VERIFY_SIGN_NAME': '',
+ 'CMPP2_VERIFY_TEMPLATE_CODE': '{code}',
+
# Email
'EMAIL_CUSTOM_USER_CREATED_SUBJECT': _('Create account successfully'),
'EMAIL_CUSTOM_USER_CREATED_HONORIFIC': _('Hello'),
diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po
index a1ae87cba..fb5b39b2f 100644
--- a/apps/locale/ja/LC_MESSAGES/django.po
+++ b/apps/locale/ja/LC_MESSAGES/django.po
@@ -88,8 +88,8 @@ msgstr "ログイン確認"
#: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:20
#: assets/models/cmd_filter.py:30 assets/models/label.py:15 audits/models.py:37
#: audits/models.py:62 audits/models.py:87 audits/serializers.py:100
-#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:220
-#: perms/models/base.py:84 rbac/builtin.py:120 rbac/models/rolebinding.py:41
+#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:214
+#: perms/models/base.py:84 rbac/builtin.py:117 rbac/models/rolebinding.py:41
#: terminal/backends/command/models.py:20
#: terminal/backends/command/serializers.py:13 terminal/models/session.py:44
#: terminal/models/sharing.py:33 terminal/notifications.py:91
@@ -393,8 +393,8 @@ msgstr "クラスター"
#: applications/serializers/attrs/application_category/db.py:11
#: ops/models/adhoc.py:157 settings/serializers/auth/radius.py:14
-#: terminal/models/endpoint.py:11
-#: xpack/plugins/cloud/serializers/account_attrs.py:72
+#: settings/serializers/auth/sms.py:52 terminal/models/endpoint.py:11
+#: xpack/plugins/cloud/serializers/account_attrs.py:70
msgid "Host"
msgstr "ホスト"
@@ -407,8 +407,8 @@ msgstr "ホスト"
#: applications/serializers/attrs/application_type/redis.py:10
#: applications/serializers/attrs/application_type/sqlserver.py:10
#: assets/models/asset.py:214 assets/models/domain.py:62
-#: settings/serializers/auth/radius.py:15
-#: xpack/plugins/cloud/serializers/account_attrs.py:73
+#: settings/serializers/auth/radius.py:15 settings/serializers/auth/sms.py:53
+#: xpack/plugins/cloud/serializers/account_attrs.py:71
msgid "Port"
msgstr "ポート"
@@ -591,7 +591,7 @@ msgstr "ホスト名生"
#: assets/models/asset.py:215 assets/serializers/account.py:16
#: assets/serializers/asset.py:65 perms/serializers/asset/user_permission.py:41
-#: xpack/plugins/cloud/models.py:107 xpack/plugins/cloud/serializers/task.py:43
+#: xpack/plugins/cloud/models.py:107 xpack/plugins/cloud/serializers/task.py:42
msgid "Protocols"
msgstr "プロトコル"
@@ -990,7 +990,7 @@ msgid "Parent key"
msgstr "親キー"
#: assets/models/node.py:559 assets/serializers/system_user.py:267
-#: xpack/plugins/cloud/models.py:96 xpack/plugins/cloud/serializers/task.py:70
+#: xpack/plugins/cloud/models.py:96 xpack/plugins/cloud/serializers/task.py:69
msgid "Node"
msgstr "ノード"
@@ -2318,7 +2318,7 @@ msgstr "コードエラー"
#: authentication/templates/authentication/_msg_reset_password.html:3
#: authentication/templates/authentication/_msg_rest_password_success.html:2
#: authentication/templates/authentication/_msg_rest_public_key_success.html:2
-#: jumpserver/conf.py:323 ops/tasks.py:145 ops/tasks.py:148
+#: jumpserver/conf.py:316 ops/tasks.py:145 ops/tasks.py:148
#: perms/templates/perms/_msg_item_permissions_expire.html:3
#: perms/templates/perms/_msg_permed_items_expire.html:3
#: tickets/templates/tickets/approve_check_password.html:33
@@ -2723,11 +2723,15 @@ msgstr "アリ雲"
msgid "Tencent cloud"
msgstr "テンセント雲"
-#: common/sdk/sms/endpoint.py:28
+#: common/sdk/sms/endpoint.py:18
+msgid "CMPP v2.0"
+msgstr "CMPP v2.0"
+
+#: common/sdk/sms/endpoint.py:29
msgid "SMS provider not support: {}"
msgstr "SMSプロバイダーはサポートしていません: {}"
-#: common/sdk/sms/endpoint.py:49
+#: common/sdk/sms/endpoint.py:50
msgid "SMS verification code signature or template invalid"
msgstr "SMS検証コードの署名またはテンプレートが無効"
@@ -2763,11 +2767,11 @@ msgstr "特殊文字を含むべきではない"
msgid "The mobile phone number format is incorrect"
msgstr "携帯電話番号の形式が正しくありません"
-#: jumpserver/conf.py:322
+#: jumpserver/conf.py:315
msgid "Create account successfully"
msgstr "アカウントを正常に作成"
-#: jumpserver/conf.py:324
+#: jumpserver/conf.py:317
msgid "Your account has been created successfully"
msgstr "アカウントが正常に作成されました"
@@ -3026,8 +3030,8 @@ msgstr "組織のリソース ({}) は削除できません"
msgid "App organizations"
msgstr "アプリ組織"
-#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:85
-#: orgs/models.py:217 rbac/const.py:7 rbac/models/rolebinding.py:48
+#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:80
+#: orgs/models.py:211 rbac/const.py:7 rbac/models/rolebinding.py:48
#: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62
#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:71
msgid "Organization"
@@ -3271,27 +3275,27 @@ msgstr "{} 少なくとも1つのシステムロール"
msgid "RBAC"
msgstr "RBAC"
-#: rbac/builtin.py:111
+#: rbac/builtin.py:108
msgid "SystemAdmin"
msgstr "システム管理者"
-#: rbac/builtin.py:114
+#: rbac/builtin.py:111
msgid "SystemAuditor"
msgstr "システム監査人"
-#: rbac/builtin.py:117
+#: rbac/builtin.py:114
msgid "SystemComponent"
msgstr "システムコンポーネント"
-#: rbac/builtin.py:123
+#: rbac/builtin.py:120
msgid "OrgAdmin"
msgstr "組織管理者"
-#: rbac/builtin.py:126
+#: rbac/builtin.py:123
msgid "OrgAuditor"
msgstr "監査員を組織する"
-#: rbac/builtin.py:129
+#: rbac/builtin.py:126
msgid "OrgUser"
msgstr "組織ユーザー"
@@ -3463,13 +3467,8 @@ msgstr "権限ツリーの表示"
msgid "Execute batch command"
msgstr "バッチ実行コマンド"
-#: settings/api/alibaba_sms.py:31 settings/api/tencent_sms.py:35
-msgid "test_phone is required"
-msgstr "携帯番号をテストこのフィールドは必須です"
-
-#: settings/api/alibaba_sms.py:52 settings/api/dingtalk.py:31
-#: settings/api/feishu.py:36 settings/api/tencent_sms.py:57
-#: settings/api/wecom.py:37
+#: settings/api/dingtalk.py:31 settings/api/feishu.py:36
+#: settings/api/sms.py:131 settings/api/wecom.py:37
msgid "Test success"
msgstr "テストの成功"
@@ -3497,6 +3496,14 @@ msgstr "Ldapユーザーを取得するにはNone"
msgid "Imported {} users successfully (Organization: {})"
msgstr "{} 人のユーザーを正常にインポートしました (組織: {})"
+#: settings/api/sms.py:113
+msgid "Invalid SMS platform"
+msgstr "無効なショートメッセージプラットフォーム"
+
+#: settings/api/sms.py:119
+msgid "test_phone is required"
+msgstr "携帯番号をテストこのフィールドは必須です"
+
#: settings/apps.py:7
msgid "Settings"
msgstr "設定"
@@ -3821,29 +3828,69 @@ msgstr "SP プライベートキー"
msgid "SP cert"
msgstr "SP 証明書"
-#: settings/serializers/auth/sms.py:11
+#: settings/serializers/auth/sms.py:14
msgid "Enable SMS"
msgstr "SMSの有効化"
-#: settings/serializers/auth/sms.py:13
-msgid "SMS provider"
-msgstr "SMSプロバイダ"
+#: settings/serializers/auth/sms.py:16
+msgid "SMS provider / Protocol"
+msgstr "SMSプロバイダ / プロトコル"
-#: settings/serializers/auth/sms.py:18 settings/serializers/auth/sms.py:36
-#: settings/serializers/auth/sms.py:44 settings/serializers/email.py:65
+#: settings/serializers/auth/sms.py:21 settings/serializers/auth/sms.py:39
+#: settings/serializers/auth/sms.py:47 settings/serializers/auth/sms.py:58
+#: settings/serializers/email.py:65
msgid "Signature"
msgstr "署名"
-#: settings/serializers/auth/sms.py:19 settings/serializers/auth/sms.py:37
-#: settings/serializers/auth/sms.py:45
+#: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:40
+#: settings/serializers/auth/sms.py:48
msgid "Template code"
msgstr "テンプレートコード"
-#: settings/serializers/auth/sms.py:23
+#: settings/serializers/auth/sms.py:26
msgid "Test phone"
msgstr "テスト電話"
-#: settings/serializers/auth/sso.py:11
+#: settings/serializers/auth/sms.py:54
+msgid "Gateway account(SP id)"
+msgstr "ゲートウェイアカウント(SP id)"
+
+#: settings/serializers/auth/sms.py:55
+msgid "Gateway password(SP secret)"
+msgstr "ゲートウェイパスワード(SP secret)"
+
+#: settings/serializers/auth/sms.py:56
+msgid "Original number(Src id)"
+msgstr "元の番号(Src id)"
+
+#: settings/serializers/auth/sms.py:57
+msgid "Business type(Service id)"
+msgstr "ビジネス・タイプ(Service id)"
+
+#: settings/serializers/auth/sms.py:60
+msgid "Template"
+msgstr "テンプレート"
+
+#: settings/serializers/auth/sms.py:61
+msgid ""
+"Template need contain {code} and Signature + template length does not exceed "
+"67 words. For example, your verification code is {code}, which is valid for "
+"5 minutes. Please do not disclose it to others."
+msgstr ""
+"テンプレートには{code}を含める必要があり、署名+テンプレートの長さは67ワード未"
+"満です。たとえば、認証コードは{code}で、有効期間は5分です。他の人には言わない"
+"でください。"
+
+#: settings/serializers/auth/sms.py:70
+#, python-brace-format
+msgid "The template needs to contain {code}"
+msgstr "テンプレートには{code}を含める必要があります"
+
+#: settings/serializers/auth/sms.py:73
+msgid "Signature + Template must not exceed 65 words"
+msgstr "署名+テンプレートの長さは65文字以内"
+
+#: settings/serializers/auth/sso.py:12
msgid "Enable SSO auth"
msgstr "SSO Token認証の有効化"
@@ -6460,11 +6507,11 @@ msgstr "クラウドアカウント"
msgid "Test cloud account"
msgstr "クラウドアカウントのテスト"
-#: xpack/plugins/cloud/models.py:85 xpack/plugins/cloud/serializers/task.py:67
+#: xpack/plugins/cloud/models.py:85 xpack/plugins/cloud/serializers/task.py:66
msgid "Account"
msgstr "アカウント"
-#: xpack/plugins/cloud/models.py:88 xpack/plugins/cloud/serializers/task.py:38
+#: xpack/plugins/cloud/models.py:88 xpack/plugins/cloud/serializers/task.py:37
msgid "Regions"
msgstr "リージョン"
@@ -6472,19 +6519,19 @@ msgstr "リージョン"
msgid "Hostname strategy"
msgstr "ホスト名戦略"
-#: xpack/plugins/cloud/models.py:100 xpack/plugins/cloud/serializers/task.py:68
+#: xpack/plugins/cloud/models.py:100 xpack/plugins/cloud/serializers/task.py:67
msgid "Unix admin user"
msgstr "Unix adminユーザー"
-#: xpack/plugins/cloud/models.py:104 xpack/plugins/cloud/serializers/task.py:69
+#: xpack/plugins/cloud/models.py:104 xpack/plugins/cloud/serializers/task.py:68
msgid "Windows admin user"
msgstr "Windows管理者"
-#: xpack/plugins/cloud/models.py:110 xpack/plugins/cloud/serializers/task.py:46
+#: xpack/plugins/cloud/models.py:110 xpack/plugins/cloud/serializers/task.py:45
msgid "IP network segment group"
msgstr "IPネットワークセグメントグループ"
-#: xpack/plugins/cloud/models.py:113 xpack/plugins/cloud/serializers/task.py:72
+#: xpack/plugins/cloud/models.py:113 xpack/plugins/cloud/serializers/task.py:71
msgid "Always update"
msgstr "常に更新"
@@ -6790,11 +6837,9 @@ msgstr "テストポート"
#: xpack/plugins/cloud/serializers/task.py:29
msgid ""
-"Only instances matching the IP range will be synced.
If the instance "
-"contains multiple IP addresses, the first IP address that matches will be "
-"used as the IP for the created asset.
The default value of * means sync "
-"all instances and randomly match IP addresses.
Format for comma-"
-"delimited string, Such as: 192.168.1.0/24, 10.1.1.1-10.1.1.20"
+"The IP address that is first matched to will be used as the IP of the "
+"created asset.
The default * indicates a random match.
Format for "
+"comma-delimited string, Such as: 192.168.1.0/24, 10.1.1.1-10.1.1.20"
msgstr ""
"IP範囲に一致するインスタンスのみが同期されます。
インスタンスに複数のIPア"
"ドレスが含まれている場合、一致する最初のIPアドレスが作成されたアセットのIPと"
@@ -6802,24 +6847,24 @@ msgstr ""
"ドレスをランダムに一致させることを意味します。
形式はコンマ区切りの文字列"
"です。例:192.168.1.0/24,10.1.1.1-10.1.1.20"
-#: xpack/plugins/cloud/serializers/task.py:36
+#: xpack/plugins/cloud/serializers/task.py:35
msgid "History count"
msgstr "実行回数"
-#: xpack/plugins/cloud/serializers/task.py:37
+#: xpack/plugins/cloud/serializers/task.py:36
msgid "Instance count"
msgstr "インスタンス数"
-#: xpack/plugins/cloud/serializers/task.py:66
+#: xpack/plugins/cloud/serializers/task.py:65
msgid "Linux admin user"
msgstr "Linux管理者"
-#: xpack/plugins/cloud/serializers/task.py:71
+#: xpack/plugins/cloud/serializers/task.py:70
#: xpack/plugins/gathered_user/serializers.py:20
msgid "Periodic display"
msgstr "定期的な表示"
-#: xpack/plugins/cloud/utils.py:69
+#: xpack/plugins/cloud/utils.py:68
msgid "Account unavailable"
msgstr "利用できないアカウント"
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index cce71cba7..34da7efb9 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -87,8 +87,8 @@ msgstr "登录复核"
#: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:20
#: assets/models/cmd_filter.py:30 assets/models/label.py:15 audits/models.py:37
#: audits/models.py:62 audits/models.py:87 audits/serializers.py:100
-#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:220
-#: perms/models/base.py:84 rbac/builtin.py:120 rbac/models/rolebinding.py:41
+#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:214
+#: perms/models/base.py:84 rbac/builtin.py:117 rbac/models/rolebinding.py:41
#: terminal/backends/command/models.py:20
#: terminal/backends/command/serializers.py:13 terminal/models/session.py:44
#: terminal/models/sharing.py:33 terminal/notifications.py:91
@@ -388,8 +388,8 @@ msgstr "集群"
#: applications/serializers/attrs/application_category/db.py:11
#: ops/models/adhoc.py:157 settings/serializers/auth/radius.py:14
-#: terminal/models/endpoint.py:11
-#: xpack/plugins/cloud/serializers/account_attrs.py:72
+#: settings/serializers/auth/sms.py:52 terminal/models/endpoint.py:11
+#: xpack/plugins/cloud/serializers/account_attrs.py:70
msgid "Host"
msgstr "主机"
@@ -402,8 +402,8 @@ msgstr "主机"
#: applications/serializers/attrs/application_type/redis.py:10
#: applications/serializers/attrs/application_type/sqlserver.py:10
#: assets/models/asset.py:214 assets/models/domain.py:62
-#: settings/serializers/auth/radius.py:15
-#: xpack/plugins/cloud/serializers/account_attrs.py:73
+#: settings/serializers/auth/radius.py:15 settings/serializers/auth/sms.py:53
+#: xpack/plugins/cloud/serializers/account_attrs.py:71
msgid "Port"
msgstr "端口"
@@ -586,7 +586,7 @@ msgstr "主机名原始"
#: assets/models/asset.py:215 assets/serializers/account.py:16
#: assets/serializers/asset.py:65 perms/serializers/asset/user_permission.py:41
-#: xpack/plugins/cloud/models.py:107 xpack/plugins/cloud/serializers/task.py:43
+#: xpack/plugins/cloud/models.py:107 xpack/plugins/cloud/serializers/task.py:42
msgid "Protocols"
msgstr "协议组"
@@ -985,7 +985,7 @@ msgid "Parent key"
msgstr "ssh私钥"
#: assets/models/node.py:559 assets/serializers/system_user.py:267
-#: xpack/plugins/cloud/models.py:96 xpack/plugins/cloud/serializers/task.py:70
+#: xpack/plugins/cloud/models.py:96 xpack/plugins/cloud/serializers/task.py:69
msgid "Node"
msgstr "节点"
@@ -2293,7 +2293,7 @@ msgstr "代码错误"
#: authentication/templates/authentication/_msg_reset_password.html:3
#: authentication/templates/authentication/_msg_rest_password_success.html:2
#: authentication/templates/authentication/_msg_rest_public_key_success.html:2
-#: jumpserver/conf.py:323 ops/tasks.py:145 ops/tasks.py:148
+#: jumpserver/conf.py:316 ops/tasks.py:145 ops/tasks.py:148
#: perms/templates/perms/_msg_item_permissions_expire.html:3
#: perms/templates/perms/_msg_permed_items_expire.html:3
#: tickets/templates/tickets/approve_check_password.html:33
@@ -2689,11 +2689,15 @@ msgstr "阿里云"
msgid "Tencent cloud"
msgstr "腾讯云"
-#: common/sdk/sms/endpoint.py:28
+#: common/sdk/sms/endpoint.py:18
+msgid "CMPP v2.0"
+msgstr "CMPP v2.0"
+
+#: common/sdk/sms/endpoint.py:29
msgid "SMS provider not support: {}"
msgstr "短信服务商不支持:{}"
-#: common/sdk/sms/endpoint.py:49
+#: common/sdk/sms/endpoint.py:50
msgid "SMS verification code signature or template invalid"
msgstr "短信验证码签名或模版无效"
@@ -2729,11 +2733,11 @@ msgstr "不能包含特殊字符"
msgid "The mobile phone number format is incorrect"
msgstr "手机号格式不正确"
-#: jumpserver/conf.py:322
+#: jumpserver/conf.py:315
msgid "Create account successfully"
msgstr "创建账号成功"
-#: jumpserver/conf.py:324
+#: jumpserver/conf.py:317
msgid "Your account has been created successfully"
msgstr "你的账号已创建成功"
@@ -2986,8 +2990,8 @@ msgstr "组织存在资源 ({}) 不能被删除"
msgid "App organizations"
msgstr "组织管理"
-#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:85
-#: orgs/models.py:217 rbac/const.py:7 rbac/models/rolebinding.py:48
+#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:80
+#: orgs/models.py:211 rbac/const.py:7 rbac/models/rolebinding.py:48
#: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62
#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:71
msgid "Organization"
@@ -3229,27 +3233,27 @@ msgstr "{} 至少有一个系统角色"
msgid "RBAC"
msgstr "RBAC"
-#: rbac/builtin.py:111
+#: rbac/builtin.py:108
msgid "SystemAdmin"
msgstr "系统管理员"
-#: rbac/builtin.py:114
+#: rbac/builtin.py:111
msgid "SystemAuditor"
msgstr "系统审计员"
-#: rbac/builtin.py:117
+#: rbac/builtin.py:114
msgid "SystemComponent"
msgstr "系统组件"
-#: rbac/builtin.py:123
+#: rbac/builtin.py:120
msgid "OrgAdmin"
msgstr "组织管理员"
-#: rbac/builtin.py:126
+#: rbac/builtin.py:123
msgid "OrgAuditor"
msgstr "组织审计员"
-#: rbac/builtin.py:129
+#: rbac/builtin.py:126
msgid "OrgUser"
msgstr "组织用户"
@@ -3420,13 +3424,8 @@ msgstr "查看授权树"
msgid "Execute batch command"
msgstr "执行批量命令"
-#: settings/api/alibaba_sms.py:31 settings/api/tencent_sms.py:35
-msgid "test_phone is required"
-msgstr "测试手机号 该字段是必填项。"
-
-#: settings/api/alibaba_sms.py:52 settings/api/dingtalk.py:31
-#: settings/api/feishu.py:36 settings/api/tencent_sms.py:57
-#: settings/api/wecom.py:37
+#: settings/api/dingtalk.py:31 settings/api/feishu.py:36
+#: settings/api/sms.py:131 settings/api/wecom.py:37
msgid "Test success"
msgstr "测试成功"
@@ -3454,6 +3453,14 @@ msgstr "获取 LDAP 用户为 None"
msgid "Imported {} users successfully (Organization: {})"
msgstr "成功导入 {} 个用户 ( 组织: {} )"
+#: settings/api/sms.py:113
+msgid "Invalid SMS platform"
+msgstr "无效的短信平台"
+
+#: settings/api/sms.py:119
+msgid "test_phone is required"
+msgstr "测试手机号 该字段是必填项。"
+
#: settings/apps.py:7
msgid "Settings"
msgstr "系统设置"
@@ -3778,29 +3785,68 @@ msgstr "SP 密钥"
msgid "SP cert"
msgstr "SP 证书"
-#: settings/serializers/auth/sms.py:11
+#: settings/serializers/auth/sms.py:14
msgid "Enable SMS"
msgstr "启用 SMS"
-#: settings/serializers/auth/sms.py:13
-msgid "SMS provider"
-msgstr "短信服务商"
+#: settings/serializers/auth/sms.py:16
+msgid "SMS provider / Protocol"
+msgstr "短信服务商 / 协议"
-#: settings/serializers/auth/sms.py:18 settings/serializers/auth/sms.py:36
-#: settings/serializers/auth/sms.py:44 settings/serializers/email.py:65
+#: settings/serializers/auth/sms.py:21 settings/serializers/auth/sms.py:39
+#: settings/serializers/auth/sms.py:47 settings/serializers/auth/sms.py:58
+#: settings/serializers/email.py:65
msgid "Signature"
msgstr "签名"
-#: settings/serializers/auth/sms.py:19 settings/serializers/auth/sms.py:37
-#: settings/serializers/auth/sms.py:45
+#: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:40
+#: settings/serializers/auth/sms.py:48
msgid "Template code"
msgstr "模板"
-#: settings/serializers/auth/sms.py:23
+#: settings/serializers/auth/sms.py:26
msgid "Test phone"
msgstr "测试手机号"
-#: settings/serializers/auth/sso.py:11
+#: settings/serializers/auth/sms.py:54
+msgid "Gateway account(SP id)"
+msgstr "网关账号(SP id)"
+
+#: settings/serializers/auth/sms.py:55
+msgid "Gateway password(SP secret)"
+msgstr "网关密码(SP secret)"
+
+#: settings/serializers/auth/sms.py:56
+msgid "Original number(Src id)"
+msgstr "原始号码(Src id)"
+
+#: settings/serializers/auth/sms.py:57
+msgid "Business type(Service id)"
+msgstr "业务类型(Service id)"
+
+#: settings/serializers/auth/sms.py:60
+msgid "Template"
+msgstr "模板"
+
+#: settings/serializers/auth/sms.py:61
+msgid ""
+"Template need contain {code} and Signature + template length does not exceed "
+"67 words. For example, your verification code is {code}, which is valid for "
+"5 minutes. Please do not disclose it to others."
+msgstr ""
+"模板需要包含 {code},并且模板+签名长度不能超过67个字。例如, 您的验证码是 "
+"{code}, 有效期为5分钟。请不要泄露给其他人。"
+
+#: settings/serializers/auth/sms.py:70
+#, python-brace-format
+msgid "The template needs to contain {code}"
+msgstr "模板需要包含 {code}"
+
+#: settings/serializers/auth/sms.py:73
+msgid "Signature + Template must not exceed 65 words"
+msgstr "模板+签名不能超过65个字"
+
+#: settings/serializers/auth/sso.py:12
msgid "Enable SSO auth"
msgstr "启用 SSO Token 认证"
@@ -6365,11 +6411,11 @@ msgstr "云账号"
msgid "Test cloud account"
msgstr "测试云账号"
-#: xpack/plugins/cloud/models.py:85 xpack/plugins/cloud/serializers/task.py:67
+#: xpack/plugins/cloud/models.py:85 xpack/plugins/cloud/serializers/task.py:66
msgid "Account"
msgstr "账号"
-#: xpack/plugins/cloud/models.py:88 xpack/plugins/cloud/serializers/task.py:38
+#: xpack/plugins/cloud/models.py:88 xpack/plugins/cloud/serializers/task.py:37
msgid "Regions"
msgstr "地域"
@@ -6377,19 +6423,19 @@ msgstr "地域"
msgid "Hostname strategy"
msgstr "主机名策略"
-#: xpack/plugins/cloud/models.py:100 xpack/plugins/cloud/serializers/task.py:68
+#: xpack/plugins/cloud/models.py:100 xpack/plugins/cloud/serializers/task.py:67
msgid "Unix admin user"
msgstr "Unix 管理员"
-#: xpack/plugins/cloud/models.py:104 xpack/plugins/cloud/serializers/task.py:69
+#: xpack/plugins/cloud/models.py:104 xpack/plugins/cloud/serializers/task.py:68
msgid "Windows admin user"
msgstr "Windows 管理员"
-#: xpack/plugins/cloud/models.py:110 xpack/plugins/cloud/serializers/task.py:46
+#: xpack/plugins/cloud/models.py:110 xpack/plugins/cloud/serializers/task.py:45
msgid "IP network segment group"
msgstr "IP网段组"
-#: xpack/plugins/cloud/models.py:113 xpack/plugins/cloud/serializers/task.py:72
+#: xpack/plugins/cloud/models.py:113 xpack/plugins/cloud/serializers/task.py:71
msgid "Always update"
msgstr "总是更新"
@@ -6694,34 +6740,32 @@ msgstr "测试端口"
#: xpack/plugins/cloud/serializers/task.py:29
msgid ""
-"Only instances matching the IP range will be synced.
If the instance "
-"contains multiple IP addresses, the first IP address that matches will be "
-"used as the IP for the created asset.
The default value of * means sync "
-"all instances and randomly match IP addresses.
Format for comma-"
-"delimited string, Such as: 192.168.1.0/24, 10.1.1.1-10.1.1.20"
+"The IP address that is first matched to will be used as the IP of the "
+"created asset.
The default * indicates a random match.
Format for "
+"comma-delimited string, Such as: 192.168.1.0/24, 10.1.1.1-10.1.1.20"
msgstr ""
"只有匹配到 IP 段的实例会被同步。
如果实例包含多个 IP 地址,那么第一个匹配"
"到的 IP 地址将被用作创建的资产的 IP。
默认值 * 表示同步所有实例和随机匹配 "
"IP 地址。
格式为以逗号分隔的字符串,例如:192.168.1.0/24,10.1.1.1-10.1.1.20"
-#: xpack/plugins/cloud/serializers/task.py:36
+#: xpack/plugins/cloud/serializers/task.py:35
msgid "History count"
msgstr "执行次数"
-#: xpack/plugins/cloud/serializers/task.py:37
+#: xpack/plugins/cloud/serializers/task.py:36
msgid "Instance count"
msgstr "实例个数"
-#: xpack/plugins/cloud/serializers/task.py:66
+#: xpack/plugins/cloud/serializers/task.py:65
msgid "Linux admin user"
msgstr "Linux 管理员"
-#: xpack/plugins/cloud/serializers/task.py:71
+#: xpack/plugins/cloud/serializers/task.py:70
#: xpack/plugins/gathered_user/serializers.py:20
msgid "Periodic display"
msgstr "定时执行"
-#: xpack/plugins/cloud/utils.py:69
+#: xpack/plugins/cloud/utils.py:68
msgid "Account unavailable"
msgstr "账号无效"
diff --git a/apps/settings/api/__init__.py b/apps/settings/api/__init__.py
index 65438dda1..176a6c2c6 100644
--- a/apps/settings/api/__init__.py
+++ b/apps/settings/api/__init__.py
@@ -5,6 +5,4 @@ from .dingtalk import *
from .feishu import *
from .public import *
from .email import *
-from .alibaba_sms import *
-from .tencent_sms import *
from .sms import *
diff --git a/apps/settings/api/alibaba_sms.py b/apps/settings/api/alibaba_sms.py
deleted file mode 100644
index 8240ba0e0..000000000
--- a/apps/settings/api/alibaba_sms.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from rest_framework.views import Response
-from rest_framework.generics import GenericAPIView
-from rest_framework.exceptions import APIException
-from rest_framework import status
-from django.utils.translation import gettext_lazy as _
-
-from common.sdk.sms.alibaba import AlibabaSMS
-from settings.models import Setting
-from common.exceptions import JMSException
-
-from .. import serializers
-
-
-class AlibabaSMSTestingAPI(GenericAPIView):
- serializer_class = serializers.AlibabaSMSSettingSerializer
- rbac_perms = {
- 'POST': 'settings.change_sms'
- }
-
- def post(self, request):
- serializer = self.serializer_class(data=request.data)
- serializer.is_valid(raise_exception=True)
-
- alibaba_access_key_id = serializer.validated_data['ALIBABA_ACCESS_KEY_ID']
- alibaba_access_key_secret = serializer.validated_data.get('ALIBABA_ACCESS_KEY_SECRET')
- alibaba_verify_sign_name = serializer.validated_data['ALIBABA_VERIFY_SIGN_NAME']
- alibaba_verify_template_code = serializer.validated_data['ALIBABA_VERIFY_TEMPLATE_CODE']
- test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
-
- if not test_phone:
- raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
-
- if not alibaba_access_key_secret:
- secret = Setting.objects.filter(name='ALIBABA_ACCESS_KEY_SECRET').first()
- if secret:
- alibaba_access_key_secret = secret.cleaned_value
-
- alibaba_access_key_secret = alibaba_access_key_secret or ''
-
- try:
- client = AlibabaSMS(
- access_key_id=alibaba_access_key_id,
- access_key_secret=alibaba_access_key_secret
- )
-
- client.send_sms(
- phone_numbers=[test_phone],
- sign_name=alibaba_verify_sign_name,
- template_code=alibaba_verify_template_code,
- template_param={'code': 'test'}
- )
- return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
- except APIException as e:
- try:
- error = e.detail['errmsg']
- except:
- error = e.detail
- return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})
diff --git a/apps/settings/api/settings.py b/apps/settings/api/settings.py
index 4cce5e718..0f487c280 100644
--- a/apps/settings/api/settings.py
+++ b/apps/settings/api/settings.py
@@ -40,6 +40,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
'sms': serializers.SMSSettingSerializer,
'alibaba': serializers.AlibabaSMSSettingSerializer,
'tencent': serializers.TencentSMSSettingSerializer,
+ 'cmpp2': serializers.CMPP2SMSSettingSerializer,
}
rbac_category_permissions = {
diff --git a/apps/settings/api/sms.py b/apps/settings/api/sms.py
index bb30fa3aa..a886a3d8f 100644
--- a/apps/settings/api/sms.py
+++ b/apps/settings/api/sms.py
@@ -1,8 +1,19 @@
-from rest_framework.generics import ListAPIView
+import importlib
+
+from collections import OrderedDict
+
+from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.response import Response
+from rest_framework.exceptions import APIException
+from rest_framework import status
+from django.utils.translation import gettext_lazy as _
from common.sdk.sms import BACKENDS
+from common.exceptions import JMSException
from settings.serializers.sms import SMSBackendSerializer
+from settings.models import Setting
+
+from .. import serializers
class SMSBackendAPI(ListAPIView):
@@ -21,3 +32,108 @@ class SMSBackendAPI(ListAPIView):
]
return Response(data)
+
+
+class SMSTestingAPI(GenericAPIView):
+ backends_serializer = {
+ 'alibaba': serializers.AlibabaSMSSettingSerializer,
+ 'tencent': serializers.TencentSMSSettingSerializer,
+ 'cmpp2': serializers.CMPP2SMSSettingSerializer
+ }
+ rbac_perms = {
+ 'POST': 'settings.change_sms'
+ }
+
+ @staticmethod
+ def get_or_from_setting(key, value=''):
+ if not value:
+ secret = Setting.objects.filter(name=key).first()
+ if secret:
+ value = secret.cleaned_value
+
+ return value or ''
+
+ def get_alibaba_params(self, data):
+ init_params = {
+ 'access_key_id': data['ALIBABA_ACCESS_KEY_ID'],
+ 'access_key_secret': self.get_or_from_setting(
+ 'ALIBABA_ACCESS_KEY_SECRET', data.get('ALIBABA_ACCESS_KEY_SECRET')
+ )
+ }
+ send_sms_params = {
+ 'sign_name': data['ALIBABA_VERIFY_SIGN_NAME'],
+ 'template_code': data['ALIBABA_VERIFY_TEMPLATE_CODE'],
+ 'template_param': {'code': '666666'}
+ }
+ return init_params, send_sms_params
+
+ def get_tencent_params(self, data):
+ init_params = {
+ 'secret_id': data['TENCENT_SECRET_ID'],
+ 'secret_key': self.get_or_from_setting(
+ 'TENCENT_SECRET_KEY', data.get('TENCENT_SECRET_KEY')
+ ),
+ 'sdkappid': data['TENCENT_SDKAPPID']
+ }
+ send_sms_params = {
+ 'sign_name': data['TENCENT_VERIFY_SIGN_NAME'],
+ 'template_code': data['TENCENT_VERIFY_TEMPLATE_CODE'],
+ 'template_param': OrderedDict(code='666666')
+ }
+ return init_params, send_sms_params
+
+ def get_cmpp2_params(self, data):
+ init_params = {
+ 'host': data['CMPP2_HOST'], 'port': data['CMPP2_PORT'],
+ 'sp_id': data['CMPP2_SP_ID'], 'src_id': data['CMPP2_SRC_ID'],
+ 'sp_secret': self.get_or_from_setting(
+ 'CMPP2_SP_SECRET', data.get('CMPP2_SP_SECRET')
+ ),
+ 'service_id': data['CMPP2_SERVICE_ID'],
+ }
+ send_sms_params = {
+ 'sign_name': data['CMPP2_VERIFY_SIGN_NAME'],
+ 'template_code': data['CMPP2_VERIFY_TEMPLATE_CODE'],
+ 'template_param': OrderedDict(code='666666')
+ }
+ return init_params, send_sms_params
+
+ def get_params_by_backend(self, backend, data):
+ """
+ 返回两部分参数
+ 1、实例化参数
+ 2、发送测试短信参数
+ """
+ get_params_func = getattr(self, 'get_%s_params' % backend)
+ return get_params_func(data)
+
+ def post(self, request, backend):
+ serializer_class = self.backends_serializer.get(backend)
+ if serializer_class is None:
+ raise JMSException(_('Invalid SMS platform'))
+ serializer = serializer_class(data=request.data)
+ serializer.is_valid(raise_exception=True)
+
+ test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
+ if not test_phone:
+ raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
+
+ init_params, send_sms_params = self.get_params_by_backend(backend, serializer.validated_data)
+
+ m = importlib.import_module(f'common.sdk.sms.{backend}', __package__)
+ try:
+ client = m.client(**init_params)
+ client.send_sms(
+ phone_numbers=[test_phone],
+ **send_sms_params
+ )
+ status_code = status.HTTP_200_OK
+ data = {'msg': _('Test success')}
+ except APIException as e:
+ try:
+ error = e.detail['errmsg']
+ except:
+ error = e.detail
+ status_code = status.HTTP_400_BAD_REQUEST
+ data = {'error': error}
+ return Response(status=status_code, data=data)
diff --git a/apps/settings/api/tencent_sms.py b/apps/settings/api/tencent_sms.py
deleted file mode 100644
index 83a87a474..000000000
--- a/apps/settings/api/tencent_sms.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from collections import OrderedDict
-
-from rest_framework.views import Response
-from rest_framework.generics import GenericAPIView
-from rest_framework.exceptions import APIException
-from rest_framework import status
-from django.utils.translation import gettext_lazy as _
-
-from common.sdk.sms.tencent import TencentSMS
-from settings.models import Setting
-from common.exceptions import JMSException
-
-from .. import serializers
-
-
-class TencentSMSTestingAPI(GenericAPIView):
- serializer_class = serializers.TencentSMSSettingSerializer
- rbac_perms = {
- 'POST': 'settings.change_sms'
- }
-
- def post(self, request):
- serializer = self.serializer_class(data=request.data)
- serializer.is_valid(raise_exception=True)
-
- tencent_secret_id = serializer.validated_data['TENCENT_SECRET_ID']
- tencent_secret_key = serializer.validated_data.get('TENCENT_SECRET_KEY')
- tencent_verify_sign_name = serializer.validated_data['TENCENT_VERIFY_SIGN_NAME']
- tencent_verify_template_code = serializer.validated_data['TENCENT_VERIFY_TEMPLATE_CODE']
- tencent_sdkappid = serializer.validated_data.get('TENCENT_SDKAPPID')
-
- test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
-
- if not test_phone:
- raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
-
- if not tencent_secret_key:
- secret = Setting.objects.filter(name='TENCENT_SECRET_KEY').first()
- if secret:
- tencent_secret_key = secret.cleaned_value
-
- tencent_secret_key = tencent_secret_key or ''
-
- try:
- client = TencentSMS(
- secret_id=tencent_secret_id,
- secret_key=tencent_secret_key,
- sdkappid=tencent_sdkappid
- )
-
- client.send_sms(
- phone_numbers=[test_phone],
- sign_name=tencent_verify_sign_name,
- template_code=tencent_verify_template_code,
- template_param=OrderedDict(code='666666')
- )
- return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
- except APIException as e:
- try:
- error = e.detail['errmsg']
- except:
- error = e.detail
- return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})
diff --git a/apps/settings/serializers/auth/sms.py b/apps/settings/serializers/auth/sms.py
index cd3bef74c..8875f6437 100644
--- a/apps/settings/serializers/auth/sms.py
+++ b/apps/settings/serializers/auth/sms.py
@@ -4,13 +4,16 @@ from rest_framework import serializers
from common.drf.fields import EncryptedField
from common.sdk.sms import BACKENDS
-__all__ = ['SMSSettingSerializer', 'AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer']
+__all__ = [
+ 'SMSSettingSerializer', 'AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer',
+ 'CMPP2SMSSettingSerializer'
+]
class SMSSettingSerializer(serializers.Serializer):
SMS_ENABLED = serializers.BooleanField(default=False, label=_('Enable SMS'))
SMS_BACKEND = serializers.ChoiceField(
- choices=BACKENDS.choices, default=BACKENDS.ALIBABA, label=_('SMS provider')
+ choices=BACKENDS.choices, default=BACKENDS.ALIBABA, label=_('SMS provider / Protocol')
)
@@ -43,3 +46,29 @@ class TencentSMSSettingSerializer(BaseSMSSettingSerializer):
TENCENT_SDKAPPID = serializers.CharField(max_length=256, required=True, label='SDK app id')
TENCENT_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature'))
TENCENT_VERIFY_TEMPLATE_CODE = serializers.CharField(max_length=256, required=True, label=_('Template code'))
+
+
+class CMPP2SMSSettingSerializer(BaseSMSSettingSerializer):
+ CMPP2_HOST = serializers.CharField(max_length=256, required=True, label=_('Host'))
+ CMPP2_PORT = serializers.IntegerField(default=7890, label=_('Port'))
+ CMPP2_SP_ID = serializers.CharField(max_length=128, required=True, label=_('Gateway account(SP id)'))
+ CMPP2_SP_SECRET = EncryptedField(max_length=256, required=False, label=_('Gateway password(SP secret)'))
+ CMPP2_SRC_ID = serializers.CharField(max_length=256, required=False, label=_('Original number(Src id)'))
+ CMPP2_SERVICE_ID = serializers.CharField(max_length=256, required=True, label=_('Business type(Service id)'))
+ CMPP2_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature'))
+ CMPP2_VERIFY_TEMPLATE_CODE = serializers.CharField(
+ max_length=69, required=True, label=_('Template'),
+ help_text=_('Template need contain {code} and Signature + template length does not exceed 67 words. '
+ 'For example, your verification code is {code}, which is valid for 5 minutes. '
+ 'Please do not disclose it to others.')
+ )
+
+ def validate(self, attrs):
+ sign_name = attrs.get('CMPP2_VERIFY_SIGN_NAME', '')
+ template_code = attrs.get('CMPP2_VERIFY_TEMPLATE_CODE', '')
+ if template_code.find('{code}') == -1:
+ raise serializers.ValidationError(_('The template needs to contain {code}'))
+ if len(sign_name + template_code) > 65:
+ # 保证验证码内容在一条短信中(长度小于70字), 签名两边的括号和空格占3个字,再减去2个即可(验证码占用4个但占位符6个
+ raise serializers.ValidationError(_('Signature + Template must not exceed 65 words'))
+ return attrs
diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py
index 7baa19196..3152a5ef5 100644
--- a/apps/settings/serializers/settings.py
+++ b/apps/settings/serializers/settings.py
@@ -7,7 +7,7 @@ from .auth import (
LDAPSettingSerializer, OIDCSettingSerializer, KeycloakSettingSerializer,
CASSettingSerializer, RadiusSettingSerializer, FeiShuSettingSerializer,
WeComSettingSerializer, DingTalkSettingSerializer, AlibabaSMSSettingSerializer,
- TencentSMSSettingSerializer,
+ TencentSMSSettingSerializer, CMPP2SMSSettingSerializer
)
from .terminal import TerminalSettingSerializer
from .security import SecuritySettingSerializer
@@ -37,6 +37,7 @@ class SettingsSerializer(
CleaningSerializer,
AlibabaSMSSettingSerializer,
TencentSMSSettingSerializer,
+ CMPP2SMSSettingSerializer,
):
# encrypt_fields 现在使用 write_only 来判断了
pass
diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py
index 728baf0ae..ba04f2a4b 100644
--- a/apps/settings/urls/api_urls.py
+++ b/apps/settings/urls/api_urls.py
@@ -16,8 +16,7 @@ urlpatterns = [
path('wecom/testing/', api.WeComTestingAPI.as_view(), name='wecom-testing'),
path('dingtalk/testing/', api.DingTalkTestingAPI.as_view(), name='dingtalk-testing'),
path('feishu/testing/', api.FeiShuTestingAPI.as_view(), name='feishu-testing'),
- path('alibaba/testing/', api.AlibabaSMSTestingAPI.as_view(), name='alibaba-sms-testing'),
- path('tencent/testing/', api.TencentSMSTestingAPI.as_view(), name='tencent-sms-testing'),
+ path('sms//testing/', api.SMSTestingAPI.as_view(), name='sms-testing'),
path('sms/backend/', api.SMSBackendAPI.as_view(), name='sms-backend'),
path('setting/', api.SettingsApi.as_view(), name='settings-setting'),