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