diff --git a/apps/authentication/views/wecom.py b/apps/authentication/views/wecom.py index c41e20338..753f08c6b 100644 --- a/apps/authentication/views/wecom.py +++ b/apps/authentication/views/wecom.py @@ -13,20 +13,17 @@ from authentication.const import ConfirmType from authentication.mixins import AuthMixin from authentication.permissions import UserConfirmation from common.sdk.im.wecom import URL -from common.sdk.im.wecom import WeCom +from common.sdk.im.wecom import WeCom, wecom_tool from common.utils import get_logger -from common.utils.common import get_request_ip from common.utils.django import reverse, get_object_or_none, safe_next_url -from common.utils.random import random_string from common.views.mixins import UserConfirmRequiredExceptionMixin, PermissionsMixin from users.models import User from users.views import UserVerifyPasswordView from .base import BaseLoginCallbackView, BaseBindCallbackView from .mixins import METAMixin, FlashMessageMixin -logger = get_logger(__file__) -WECOM_STATE_SESSION_KEY = '_wecom_state' +logger = get_logger(__file__) class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMessageMixin, View): @@ -45,7 +42,7 @@ class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashM ) def verify_state(self): - return self.verify_state_with_session_key(WECOM_STATE_SESSION_KEY) + return wecom_tool.check_state(self.request.GET.get('state'), self.request) def get_already_bound_response(self, redirect_url): msg = _('WeCom is already bound') @@ -56,13 +53,10 @@ class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashM class WeComQRMixin(WeComBaseMixin, View): def get_qr_url(self, redirect_uri): - state = random_string(16) - self.request.session[WECOM_STATE_SESSION_KEY] = state - params = { 'appid': settings.WECOM_CORPID, 'agentid': settings.WECOM_AGENTID, - 'state': state, + 'state': wecom_tool.gen_state(request=self.request), 'redirect_uri': redirect_uri, } url = URL.QR_CONNECT + '?' + urlencode(params) @@ -74,13 +68,11 @@ class WeComOAuthMixin(WeComBaseMixin, View): def get_oauth_url(self, redirect_uri): if not settings.AUTH_WECOM: return reverse('authentication:login') - state = random_string(16) - self.request.session[WECOM_STATE_SESSION_KEY] = state params = { 'appid': settings.WECOM_CORPID, 'agentid': settings.WECOM_AGENTID, - 'state': state, + 'state': wecom_tool.gen_state(request=self.request), 'redirect_uri': redirect_uri, 'response_type': 'code', 'scope': 'snsapi_base', diff --git a/apps/common/sdk/im/utils.py b/apps/common/sdk/im/utils.py index 86f41cdc3..27c409dbc 100644 --- a/apps/common/sdk/im/utils.py +++ b/apps/common/sdk/im/utils.py @@ -16,12 +16,6 @@ def digest(corp_id, corp_secret): return dist -def update_values(default: dict, others: dict): - for key in default.keys(): - if key in others: - default[key] = others[key] - - def set_default(data: dict, default: dict): for key in default.keys(): if key not in data: diff --git a/apps/common/sdk/im/wecom/__init__.py b/apps/common/sdk/im/wecom/__init__.py index 6b37e0156..d6d1d7dbd 100644 --- a/apps/common/sdk/im/wecom/__init__.py +++ b/apps/common/sdk/im/wecom/__init__.py @@ -1,12 +1,14 @@ from typing import Iterable, AnyStr +from urllib.parse import urlencode from django.conf import settings +from django.core.cache import cache from django.utils.translation import gettext_lazy as _ from rest_framework.exceptions import APIException from common.sdk.im.mixin import RequestMixin, BaseRequest -from common.sdk.im.utils import digest, update_values -from common.utils.common import get_logger +from common.sdk.im.utils import digest +from common.utils import reverse, random_string, get_logger, lazyproperty from users.utils import construct_user_email, flatten_dict, map_attributes logger = get_logger(__name__) @@ -107,15 +109,6 @@ class WeCom(RequestMixin): 对于业务代码,只需要关心由 用户id 或 消息不对 导致的错误,其他错误不予理会 """ users = tuple(users) - - extra_params = { - "safe": 0, - "enable_id_trans": 0, - "enable_duplicate_check": 0, - "duplicate_check_interval": 1800 - } - update_values(extra_params, kwargs) - body = { "touser": '|'.join(users), "msgtype": "text", @@ -123,7 +116,7 @@ class WeCom(RequestMixin): "text": { "content": msg }, - **extra_params + **kwargs } if markdown: body['msgtype'] = 'markdown' @@ -144,15 +137,15 @@ class WeCom(RequestMixin): if 'invaliduser' not in data: return () - invaliduser = data['invaliduser'] - if not invaliduser: + invalid_user = data['invaliduser'] + if not invalid_user: return () - if isinstance(invaliduser, str): - logger.error(f'WeCom send text 200, but invaliduser is not str: invaliduser={invaliduser}') + if isinstance(invalid_user, str): + logger.error(f'WeCom send text 200, but invaliduser is not str: invaliduser={invalid_user}') raise WeComError - invalid_users = invaliduser.split('|') + invalid_users = invalid_user.split('|') return invalid_users def get_user_id_by_code(self, code): @@ -167,13 +160,12 @@ class WeCom(RequestMixin): self._requests.check_errcode_is_0(data) - USER_ID = 'UserId' - OPEN_ID = 'OpenId' - - if USER_ID in data: - return data[USER_ID], USER_ID - elif OPEN_ID in data: - return data[OPEN_ID], OPEN_ID + user_id = 'UserId' + open_id = 'OpenId' + if user_id in data: + return data[user_id], user_id + elif open_id in data: + return data[open_id], open_id else: logger.error(f'WeCom response 200 but get field from json error: fields=UserId|OpenId') raise WeComError @@ -195,3 +187,37 @@ class WeCom(RequestMixin): default_detail = self.default_user_detail(data, user_id) detail = map_attributes(default_detail, info, self.attributes) return detail + + +class WeComTool(object): + WECOM_STATE_SESSION_KEY = '_wecom_state' + WECOM_STATE_VALUE = 'wecom' + + @lazyproperty + def qr_cb_url(self): + return reverse('authentication:wecom-qr-login-callback', external=True) + + def gen_state(self, request=None): + state = random_string(16) + if not request: + cache.set(state, self.WECOM_STATE_VALUE, timeout=60 * 60 * 24) + else: + request.session[self.WECOM_STATE_SESSION_KEY] = state + return state + + def check_state(self, state, request=None): + return cache.get(state) == self.WECOM_STATE_VALUE or \ + request.session[self.WECOM_STATE_SESSION_KEY] == state + + def wrap_redirect_url(self, next_url): + params = { + 'appid': settings.WECOM_CORPID, + 'agentid': settings.WECOM_AGENTID, + 'state': self.gen_state(), + 'redirect_uri': f'{self.qr_cb_url}?next={next_url}', + 'response_type': 'code', 'scope': 'snsapi_base', + } + return URL.OAUTH_CONNECT + '?' + urlencode(params) + '#wechat_redirect' + + +wecom_tool = WeComTool() diff --git a/apps/notifications/notifications.py b/apps/notifications/notifications.py index 13df6932a..d200cc41b 100644 --- a/apps/notifications/notifications.py +++ b/apps/notifications/notifications.py @@ -127,13 +127,16 @@ class Message(metaclass=MessageType): def get_html_msg(self) -> dict: return self.get_common_msg() - def get_markdown_msg(self) -> dict: + @staticmethod + def html_to_markdown(html_msg): h = HTML2Text() - h.body_width = 300 - msg = self.get_html_msg() - content = msg['message'] - msg['message'] = h.handle(content) - return msg + h.body_width = 0 + content = html_msg['message'] + html_msg['message'] = h.handle(content) + return html_msg + + def get_markdown_msg(self) -> dict: + return self.html_to_markdown(self.get_html_msg()) def get_text_msg(self) -> dict: h = HTML2Text() diff --git a/apps/terminal/notifications.py b/apps/terminal/notifications.py index ca49f23f2..d833a5172 100644 --- a/apps/terminal/notifications.py +++ b/apps/terminal/notifications.py @@ -4,6 +4,7 @@ from django.conf import settings from django.template.loader import render_to_string from django.utils.translation import gettext_lazy as _ +from common.sdk.im.wecom import wecom_tool from common.utils import get_logger, reverse from common.utils import lazyproperty from common.utils.timezone import local_now_display @@ -75,53 +76,50 @@ class CommandWarningMessage(CommandAlertMixin, UserMessage): super().__init__(user) self.command = command - def get_html_msg(self) -> dict: - command = self.command - - command_input = command['input'] - user = command['user'] - asset = command['asset'] - account = command.get('_account', '') - cmd_acl = command.get('_cmd_filter_acl') - cmd_group = command.get('_cmd_group') - session_id = command.get('session', '') - risk_level = command['risk_level'] - org_id = command['org_id'] - org_name = command.get('_org_name') or org_id - + def get_session_url(self, external=True): + session_id = self.command.get('session', '') + org_id = self.command['org_id'] + session_url = '' if session_id: session_url = reverse( 'api-terminal:session-detail', kwargs={'pk': session_id}, - external=True, api_to_ui=True + external=external, api_to_ui=True ) + '?oid={}'.format(org_id) session_url = session_url.replace('/terminal/sessions/', '/audit/sessions/sessions/') - else: - session_url = '' + return session_url - # Command ACL - cmd_acl_name = cmd_group_name = '' - if cmd_acl: - cmd_acl_name = cmd_acl.name - if cmd_group: - cmd_group_name = cmd_group.name + def gen_html_string(self, **other_context): + command = self.command + cmd_acl = command.get('_cmd_filter_acl') + cmd_group = command.get('_cmd_group') + org_id = command['org_id'] + org_name = command.get('_org_name') or org_id + cmd_acl_name = cmd_acl.name if cmd_acl else '' + cmd_group_name = cmd_group.name if cmd_group else '' context = { - 'command': command_input, - 'user': user, - 'asset': asset, - 'account': account, + 'command': command['input'], + 'user': command['user'], + 'asset': command['asset'], + 'account': command.get('_account', ''), 'cmd_filter_acl': cmd_acl_name, 'cmd_group': cmd_group_name, - 'session_url': session_url, - 'risk_level': RiskLevelChoices.get_label(risk_level), + 'risk_level': RiskLevelChoices.get_label(command['risk_level']), 'org': org_name, } - + context.update(other_context) message = render_to_string('terminal/_msg_command_warning.html', context) - return { - 'subject': self.subject, - 'message': message - } + return {'subject': self.subject, 'message': message} + + def get_wecom_msg(self): + session_url = wecom_tool.wrap_redirect_url( + self.get_session_url(external=False) + ) + message = self.gen_html_string(session_url=session_url) + return self.html_to_markdown(message) + + def get_html_msg(self) -> dict: + return self.gen_html_string(session_url=self.get_session_url()) class CommandAlertMessage(CommandAlertMixin, SystemMessage): @@ -141,15 +139,18 @@ class CommandAlertMessage(CommandAlertMixin, SystemMessage): command['session'] = Session.objects.first().id return cls(command) - def get_html_msg(self) -> dict: - command = self.command + def get_session_url(self, external=True): session_detail_url = reverse( - 'api-terminal:session-detail', kwargs={'pk': command['session']}, - external=True, api_to_ui=True + 'api-terminal:session-detail', api_to_ui=True, + kwargs={'pk': self.command['session']}, external=external, ) + '?oid={}'.format(self.command['org_id']) session_detail_url = session_detail_url.replace( '/terminal/sessions/', '/audit/sessions/sessions/' ) + return session_detail_url + + def gen_html_string(self, **other_context) -> dict: + command = self.command level = RiskLevelChoices.get_label(command['risk_level']) items = { _("Asset"): command['asset'], @@ -159,14 +160,21 @@ class CommandAlertMessage(CommandAlertMixin, SystemMessage): } context = { 'items': items, - 'session_url': session_detail_url, "command": command['input'], } + context.update(other_context) message = render_to_string('terminal/_msg_command_alert.html', context) - return { - 'subject': self.subject, - 'message': message - } + return {'subject': self.subject, 'message': message} + + def get_wecom_msg(self): + session_url = wecom_tool.wrap_redirect_url( + self.get_session_url(external=False) + ) + message = self.gen_html_string(session_url=session_url) + return self.html_to_markdown(message) + + def get_html_msg(self) -> dict: + return self.gen_html_string(session_url=self.get_session_url()) class CommandExecutionAlert(CommandAlertMixin, SystemMessage): @@ -189,16 +197,20 @@ class CommandExecutionAlert(CommandAlertMixin, SystemMessage): } return cls(cmd) - def get_html_msg(self) -> dict: - command = self.command + def get_asset_urls(self, external=True, tran_func=None): assets_with_url = [] - for asset in command['assets']: + for asset in self.command['assets']: url = reverse( 'assets:asset-detail', kwargs={'pk': asset.id}, - api_to_ui=True, external=True, is_console=True + api_to_ui=True, external=external, is_console=True ) + '?oid={}'.format(asset.org_id) + if tran_func: + url = tran_func(url) assets_with_url.append([asset, url]) + return assets_with_url + def gen_html_string(self, **other_context): + command = self.command level = RiskLevelChoices.get_label(command['risk_level']) items = { @@ -206,17 +218,23 @@ class CommandExecutionAlert(CommandAlertMixin, SystemMessage): _("Level"): level, _("Date"): local_now_display(), } - context = { 'items': items, - 'assets_with_url': assets_with_url, 'command': command['input'], } + context.update(other_context) message = render_to_string('terminal/_msg_command_execute_alert.html', context) - return { - 'subject': self.subject, - 'message': message - } + return {'subject': self.subject, 'message': message} + + def get_wecom_msg(self): + assets_with_url = self.get_asset_urls( + external=False, tran_func=wecom_tool.wrap_redirect_url + ) + message = self.gen_html_string(assets_with_url=assets_with_url) + return self.html_to_markdown(message) + + def get_html_msg(self) -> dict: + return self.gen_html_string(assets_with_url=self.get_asset_urls()) class StorageConnectivityMessage(SystemMessage): diff --git a/apps/tickets/notifications.py b/apps/tickets/notifications.py index e217acf80..6f5b80ee1 100644 --- a/apps/tickets/notifications.py +++ b/apps/tickets/notifications.py @@ -4,12 +4,12 @@ from urllib.parse import urljoin from django.conf import settings from django.core.cache import cache from django.forms import model_to_dict -from django.shortcuts import reverse from django.template.loader import render_to_string from django.utils.translation import gettext_lazy as _ from common.db.encoder import ModelJSONFieldEncoder -from common.utils import get_logger, random_string +from common.sdk.im.wecom import wecom_tool +from common.utils import get_logger, random_string, reverse from notifications.notifications import UserMessage from . import const from .models import Ticket @@ -22,16 +22,13 @@ class BaseTicketMessage(UserMessage): ticket: Ticket content_title: str - @property - def ticket_detail_url(self): - tp = self.ticket.type - return urljoin( - settings.SITE_URL, - const.TICKET_DETAIL_URL.format( - id=str(self.ticket.id), - type=tp - ) + def get_ticket_detail_url(self, external=True): + detail_url = const.TICKET_DETAIL_URL.format( + id=str(self.ticket.id), type=self.ticket.type ) + if not external: + return detail_url + return urljoin(settings.SITE_URL, detail_url) @property def content_title(self): @@ -41,17 +38,31 @@ class BaseTicketMessage(UserMessage): def subject(self): raise NotImplementedError - def get_html_msg(self) -> dict: - context = dict( - title=self.content_title, - content=self.content, - ticket_detail_url=self.ticket_detail_url - ) - message = render_to_string('tickets/_msg_ticket.html', context) - return { - 'subject': self.subject, - 'message': message + def get_html_context(self): + return {'ticket_detail_url': self.get_ticket_detail_url()} + + def get_wecom_context(self): + ticket_detail_url = wecom_tool.wrap_redirect_url( + [self.get_ticket_detail_url(external=False)] + )[0] + return {'ticket_detail_url': ticket_detail_url} + + def gen_html_string(self, **other_context): + context = { + 'title': self.content_title, 'content': self.content, } + context.update(other_context) + message = render_to_string( + 'tickets/_msg_ticket.html', context + ) + return {'subject': self.subject, 'message': message} + + def get_html_msg(self) -> dict: + return self.gen_html_string(**self.get_html_context()) + + def get_wecom_msg(self): + message = self.gen_html_string(**self.get_wecom_context()) + return self.html_to_markdown(message) @classmethod def gen_test_msg(cls): @@ -113,27 +124,21 @@ class TicketAppliedToAssigneeMessage(BaseTicketMessage): ) return title - def get_ticket_approval_url(self): + def get_ticket_approval_url(self, external=True): url = reverse('tickets:direct-approve', kwargs={'token': self.token}) + if not external: + return url return urljoin(settings.SITE_URL, url) - def get_html_msg(self) -> dict: - context = dict( - title=self.content_title, - content=self.content, - ticket_detail_url=self.ticket_detail_url - ) - - ticket_approval_url = self.get_ticket_approval_url() - context.update({'ticket_approval_url': ticket_approval_url}) - message = render_to_string('tickets/_msg_ticket.html', context) - cache.set(self.token, { - 'ticket_id': self.ticket.id, 'approver_id': self.user.id, - 'content': self.content, - }, 3600) - return { - 'subject': self.subject, 'message': message + def get_html_context(self): + context = super().get_html_context() + context['ticket_approval_url'] = self.get_ticket_approval_url() + data = { + 'ticket_id': self.ticket.id, + 'approver_id': self.user.id, 'content': self.content, } + cache.set(self.token, data, 3600) + return context @classmethod def gen_test_msg(cls):